Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I add Integer Items to VBA Dictionary byRef

Creating a dictionary in VBA and I came across something that I found curious.

When I add an Outlook Calendar Item object to a dictionary it is ByRef, but when I add a dimmed Integer it is ByVal.

My two questions are:

  1. Is it possible to add the dimmed Integer ByRef?
  2. Why are these two items added differently (I know that one is an object and one is base type, looking for a little more detail)?

I looked at this: VB dictionary of Objects ByRef, but it only talks about the object case and not the integer case.

Here is the code showing what happens:

Sub checkbyref()

    Dim gCal As Items
    Dim dict As New Scripting.Dictionary
    Set dict = New Scripting.Dictionary
    Dim intCheck As Integer

    intCheck = 5
    Set gCal = GetFolderPath("\\GoogleSync\GoogleSyncCal").Items 'gets caledar items based on path

    strMeetingStart = "01/5/2019 12:00 AM"
    strGSearch = "[Start] >= '" & strMeetingStart & "'"

    gCal.Sort "[Start]"
    Set gCal = gCal.Restrict(strGSearch)

    Debug.Print intCheck 'prints "5"
    Debug.Print gCal(1).Start 'prints 1/7/2019 9:30:00 AM"

    dict.Add "check", intCheck
    dict.Add "cal", gCal(1)

    'direction 1
    dict("check") = 4
    dict("cal").Start = "1/1/2020 9:00 AM"
    Debug.Print intCheck 'prints "5"
    Debug.Print gCal(1).Start 'prints "1/1/2020 9:00:00 AM"


    'direction 2
    intCheck = 6
    gCal(1).Start = "1/1/2021 9:00 AM"
    Debug.Print dict("check") 'prints "4"
    Debug.Print dict("cal").Start 'prints "1/1/2021 9:00:00 AM"

End Sub

As you can see intCheck is not effected by changes in the dict but gCal(1) is.

like image 579
casewolf Avatar asked Feb 09 '19 16:02

casewolf


1 Answers

tldr; No, you can't add an intrinsic type to a Scripting.Dictionary ByRef. VBA is not the same type of managed environment as .NET (which uses generational garbage collection instead of reference counting), so proper memory management would be impossible. Note that .NET Dictionary's don't work that way with intrinsic types either.


For the second part of your question, the thing to keep in mind is that Dictionary is a COM object - when you reference it in a project (or call CreateObject on one of its types), it starts up a COM server for scrrun.dll to provide Scripting objects to the caller. That means that when you make any call on one of its members, you're passing all of the arguments through the COM marshaller. The Add method is a member of the IDictionary interface, and has this interface description:

[id(0x00000001), helpstring("Add a new key and item to the dictionary."), helpcontext(0x00214b3c)]
HRESULT Add(
                [in] VARIANT* Key, 
                [in] VARIANT* Item);

Note that both the Key and Item are pointers to a Variant. When you pass an Integer (or any other intrinsic type), the run-time is first performing a cast to a Variant, then passing the resulting Variant pointer to the Add method. At this point, the Dictionary is solely responsible for managing the memory of the copy. A VARIANT with an intrinsic type contains the value of the intrinsic in its data area - not a pointer to the underlying memory. This means that when the marshaller casts it, the only thing that ends up getting passed is the value. Contrast this to an Object. An Object wrapped in a Variant has the pointer to its IDispatch interface in the data area, and the marshaller has to increment the reference count when it wraps it.


The inherent issue in passing intrinsic types as pointers is that there is no way for the either side of the COM transaction to know who is responsible for freeing the memory when it goes out of scope. Consider this code:

Dim foo As Scripting.Dictionary  'Module level

Public Sub Bar()
    Dim intrinsic As Integer

    Set foo = New Scripting.Dictionary
    foo.Add "key", intrinsic
End Sub

The problem is that intrinsic is allocated memory on the stack when you execute Bar, but foo isn't freed when the procedure exits - intrinsic is. If the run-time passed intrinsic as a reference, you would be left with a bad pointer stored in foo after that memory was deallocated. If you tried to use it later in another procedure, you would either get a trash value if the memory was re-used, or an access violation.


Now compare this to passing an Object:

Dim foo As Scripting.Dictionary  'Module level

Public Sub Bar()
    Dim obj As SomeClass

    Set foo = New Scripting.Dictionary
    Set obj = New SomeClass
    foo.Add "key", obj
End Sub

Objects in VBA are reference counted, and the reference count determines their life-span. The run-time will only release them when the reference count is zero. In this case, when you Set obj = New SomeClass, it increments the reference count to one. obj (the local variable) holds a pointer to that created object. When you call foo.Add "key", obj, the marshaller wraps the object in a Variant and increments the reference count again to account for its pointer to the object. When the procedure exits, obj loses scope and the reference count is decrement, leaving a count of 1. The run-time knows that something has a pointer to it, so it doesn't tear down the object because there is a possibility that it will be accessed later. It won't decrement the reference count again until foo is destroyed and the last reference to the object is decremented.


As to your first question, the only way to do something like this in VBA would be to provide your own object wrapper to "box" the value:

'BoxedInteger.cls
Option Explicit

Private boxed As Integer

Public Property Get Value() As Integer
    Value = boxed
End Property

Public Property Let Value(rhs As Integer)
    boxed = rhs
End Property

If you wanted to get fancy with it, you could make Value the default member. Then your code would look something more like this:

Dim dict As New Scripting.Dictionary
Set dict = New Scripting.Dictionary
Dim check As BoxedInteger

Set check = New BoxedInteger
check.Value = 5

dict.Add "check", check
like image 149
Comintern Avatar answered Nov 15 '22 09:11

Comintern