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:
Is it possible to add the dimmed Integer ByRef?
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.
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
Add method of dictionary just adds a key and item pair to a Dictionary object. In case of dict.Add "check", intCheck the item here is an integer value. which means there is no reference added, just the integer value. If you want to track the original variable back, you will need to wrapp it in a class as the user Comintern has suggested, or you will have to update both, the dictionary and the original integer variable, simultaneously. Example:
Sub checkbyref()
Dim dict As New Scripting.Dictionary
Set dict = New Scripting.Dictionary
Dim intCheck As Integer
intCheck = 5
Debug.Print intCheck 'prints "5"
dict.Add "check", intCheck
' dict("check") = 4
SetDictionaryWithInteger dict, "check", 4, intCheck
Debug.Print dict("check") & "|" & intCheck
' intCheck = 6
SetDictionaryWithInteger dict, "check", 6, intCheck
Debug.Print dict("check") & "|" & intCheck
End Sub
Private Sub SetDictionaryWithInteger( _
dic As Scripting.Dictionary, _
key As String, _
newVal As Integer, _
ByRef source As Integer)
dic(key) = newVal
source = newVal
End Sub
Note: ByRef is the default in Visual Basic, here it is used only to emphasize that the integer value of intCheck will be modified.
And to answer your questions:
1. no, integer is added like a value
2. they are not added differently, they are both added the same, which means its values is added. But in case of intCheck it is the value itself (e.g. 5) and nothing more, but in case of gCal the value is the address to the data structure, so it can be still reached through this address.
Related
my Excel-applicatin has a module with utility functions. One of them adds items to arrays:
Public Sub addToArray(ByRef arr As Variant, item As Variant)
'Sub adds one element to a referenced array
On Error Resume Next
Dim bd As Long
bd = UBound(arr)
If Err.Number = 0 Then
ReDim Preserve arr(bd + 1)
Else
ReDim Preserve arr(0)
End If
arr(UBound(arr)) = item
End Sub
This Sub works perfectly as long as I pass arrays that are not referenced as object members.
addToArray arr, item
works but...
addToArray myObject.arr, item
doesn't...
the second call adds the item to an array but loses the reference to myObject
I can write a workaround by implementing a method in each class (doesn't need object references because it accesses properties of the same object) but that's not how I wanted to solve this problem.
Pls hälp ;)
Unfortunately, this is not possible due to limitations of VBA.
When you're accessing a public variant field of an object, it's get copied by value, so the original reference is not exposed. And if you declared an array (which is internally a reference type) as a public field, you would get the compile error "Constants, fixed-length strings, arrays, user-defined types and Declare statements not allowed as Public members of object modules"
I have a function that returns a Dictionary with pairs key-value. Then I proceed to use on such pair to create an array: I get a value for key "DATA_ITEMS_NUMBER" to determine arrays' max length. However it results in an error...
Function getGlobalVariables()
Dim resultDict As Object
Set resultDict = CreateObject("Scripting.Dictionary")
resultDict.Add "DATA_ITEMS_NUMBER", _
ThisWorkbook.Worksheets("setup").Cells(25, 5).value
Set getGlobalVariables = resultDict
End Function
Function getBudgetItemInfos(infoType As String, year As Integer)
Dim globals As Object
Set globals = getGlobalVariables()
Dim DATA_ITEMS_NUMBER As Integer
DATA_ITEMS_NUMBER = globals("DATA_ITEMS_NUMBER")
Dim resultArray(1 To DATA_ITEMS_NUMBER) As String
...
End Function
The Dim statement isn't executable; you can't put a breakpoint on a Dim statement, it "runs" as soon as the local scope is entered, in "static context", i.e. it doesn't (and can't) know about anything that lives in "execution context", like other local variables' values.
Hence, Dim foo(1 To SomeVariable) is illegal, because SomeVariable is not a constant expression that's known at compile-time: without the execution context, SomeVariable has no value and the array can't be statically sized.
If you want a dynamically-sized array, you need to declare a dynamic array - the ReDim statement is executable:
ReDim resultArray(1 To DATA_ITEMS_NUMBER) As String
Note that a Dim resultArray() statement isn't necessary, since ReDim is going to perform the allocation anyway: you won't get a "variable not declared" compile-time error with a ReDim foo(...) without a preceding Dim foo and Option Explicit specified.
For good form your Function procedures should have an explicit return type though:
'returns a Scripting.Dictionary instance
Function getGlobalVariables() As Object
And
'returns a Variant array
Function getBudgetItemInfos(infoType As String, year As Integer) As Variant
Otherwise (especially for the Object-returning function), you're wrapping your functions' return values in a Variant, and VBA needs to work harder than it should, at the call sites.
I have a Sub with an array of strings as parameter:
Private Sub des(ByVal array() As String)
Dim i As Integer
For i = 0 To UBound(array)
array(i) = "hy"
Next
End Sub
When I call the function inside my main function, the value of str changes even if the array is passed to the function ByVal:
Dim str() As String
str = {"11111", "22222", "33333", "44444", "5555", "66666"}
des(str)
I tried making a copy of the array in the Sub, but it still changes in the main function.
Private Sub des(ByVal array() As String)
Dim i As Integer
Dim array2() As String
array2 = array
For i = 0 To UBound(array)
array(i) = "hy"
Next
End Sub
I read on a site that you cannot pass arrays ByVal. Is this true? If so, how should I proceed?
Try this:
Dim i As Integer
Dim array2() As String
array2 = array.Clone()
For i = 0 To UBound(array2)
array2(i) = "hy"
Next
The key difference is the .Clone(), that actually makes a shallow copy of array, and changing the values in array2 will no longer affect your str value in the main code.
Arrays are reference types. That means that when you pass an Array to your function, what is passed is always a reference, not a copy of the array. The Array in your function refers to the same array object as the Array in your calling code.
The same thing happens when you do the assign (it is not a copy!) in your second example: all you've done is make yet another reference to the same object. That is why Boeckm's solution works -- the Clone() call does make a new array and assign it values which are copies of the values in the original array.
In Visual Basic .NET, regarding arrays as parameters, there are two important rules you have to be aware of:
Arrays themselves can be passed as ByVal and ByRef.
Arrays' elements can always be modified from the function or subroutine.
You already know that you can modify the elements of an array inside a subprocess (subroutine or function), no matter how that array is defined as parameter inside that subprocess.
So, given these two subroutines:
Private Sub desval(ByVal array() As String)
array = {}
End Sub
Private Sub desref(ByRef array() As String)
array = {}
End Sub
And this very simple auxiliary subroutine (here I'll use the Console):
Private Sub printArr(ByVal array() As String)
For Each str In array
Console.WriteLine(str)
Next
End Sub
you can do these simple tests:
Dim arr1() as String = {"abc", "xyz", "asdf"}
Console.WriteLine("Original array:")
printArr(arr1)
Console.WriteLine()
Console.WriteLine("After desval:")
desval(arr1)
printArr(arr1)
Console.WriteLine()
Console.WriteLine("After desref:")
desref(arr1)
printArr(arr1)
I read on a site that you cannot pass arrays ByVal. Is this true?
No, that is not true.
An array in the .NET framework is a reference type. When you create an array, an object of System.Array will be created and its reference is assigned to the reference variable.
When you call a des method, the reference of the array object will be passed. In des method, ByVal parameter is a reference parameter variable of type System.Array, and it receive a copy of reference of an array object.
MSDN article - Passing Arguments by Value and by Reference (Visual Basic)
I have read that String was a "reference type", unlike integers. MS website
I tried to test its behavior.
Sub Main()
Dim s As New TheTest
s.TheString = "42"
Dim z As String = s.GimmeTheString
z = z & "000"
Dim E As String = s.TheString
s.GimmeByref(z)
end sub
Class TheTest
Public TheString As String
Public Function GimmeTheString() As String
Return TheString
End Function
Public Sub GimmeByref(s As String)
s = TheString
End Sub
End Class
So I expected :
z is same reference as TheString, thus TheString would be set to "42000"
Then Z is modified by reference by GimmeByref thus Z is set to whatever TheString is
Actual result:
Z="42000"
E="42"
TheString="42"
What point am I missing?
I also tried adding "ByRef" in GimmeByRef : yes obviously the GimmeByRef does work as expected, but it also does if I put everything as Integer, which are said to be "Value type".
Is there any actual difference between those types?
The confusion comes about because regardless of type, argument passing in VB is pass by value by default.
If you want to pass an argument by reference, you need to specify the argument type as ByRef:
Public Sub GimmeByref(ByRef s As String)
You also need to understand the difference between mutating a value and re-assigning a variable. Doing s = TheString inside the method doesn’t mutate the value of the string, it reassigns s. This can obviously be done regardless of whether a type is a value or reference type.
The difference between value and reference types comes to bear when modifying the value itself, not a variable:
obj.ModifyMe()
Strings in .NET are immutable and thus don’t possess any such methods (same as integers). However, List(Of String), for instance, is a mutable reference type. So if you modify an argument of type List(Of String), even if it is passed by value, then the object itself is modified beyond the scope of the method.
Strings are immutable, every time you do a change it creates a new "reference" like if New was called.
A String object is called immutable (read-only), because its value
cannot be modified after it has been created. Methods that appear to
modify a String object actually return a new String object that
contains the modification. Ref
Your code basically does something like this:
Sub Main()
Dim a, b As String
a = "12"
b = a
a = a & "13"
Console.WriteLine(a) ' 1213
Console.WriteLine(b) ' 12
Console.ReadLine()
End Sub
I'm trying to write a function that would sort a collection of objects. Since the objects are all of the same type (the same user-defined class), their property set is the same. Is it possible to discover the object's properties (through code) so as to put the collection in a bi-dimensional array, each row being for an object, each column for one of its property?
Another solution would be to copy each object from the collection to an array of objects, and sort them by one of their property, whose name is passed to the function as a string. But I don't see how I can point to the object's property using the property's name passed as a string.
For a collection, it is best to sort it by it's Keys (that's what they're there for) -- but in case you don't have the keys list (lost your keys!):
'Give an input "Data As Collection"
Dim vItm As Variant
Dim i As Long, j As Long
Dim vTemp As Variant
For i = 1 To Data.Count – 1
For j = i + 1 To Data.Count
If CompareKeys(Data(i).myMemberKey, Data(j).myMemberKey) Then
'store the lesser item
vTemp = Data(j)
'remove the lesser item
Data.Remove j
're-add the lesser item before the greater Item
Data.Add vTemp, , i
End If
Next j
Next i
Come up with your own CompareKey function which will return true or false if the UDT member variables are >, < or 0 to one another. The reason you have to delete and re-add is because you cannot 'swap' internal members in a vb6/vba collection object.
Best of luck
EDIT:
To access a property you have the name of programmatically (as a string), use VB's CallByName function in the form:
Result = CallByName(MyObject, "MyProperty", vbGet)