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:
ByRef
?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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With