I recently came across a discussion about ByRef and ByVal, which I found a bit confusing. So I thought I’d summarise my (possibly mis-) understanding here.
ByRef and ByVal are keywords that influence the argument-passing mechanism of a call, which appear before the affected parameter in a procedure header: Function MyFunc (ByVal name As String, …).
If you don’t give either keyword, the default mechanism is ByRef. In effect, the parameter in the called procedure is a synonym for the variable passed as an argument by the calling procedure. Any changes made to the parameter variable in the called procedure are visible to the calling procedure.
The ByVal mechanism is equivalent to the declaration of a separate variable in the called procedure, and assignment of the argument value to that variable. Any changes made to the parameter variable in the called procedure are not visible to the calling procedure.
There’s a page in the VBA Help titled “Passing Arguments Efficiently”. This contains the following example of ByVal passing:
Function Factorial(ByVal MyVar As Integer) MyVar = MyVar - 1 If MyVar = 0 Then Factorial = 1 Exit Function End If Factorial = Factorial(MyVar) * (MyVar + 1) End Function Sub TestFactorial() Dim var As Variant var = 5 Debug.Print Factorial(var) Debug.Print var End Sub
The point is that the value of var in the calling procedure is unaffected by the decrementing in Factorial: it remains as 5.
However, to me, this seems a quite bizarre way of writing a factorial function! Why would you want to decrement MyVar, rather than just passing (MyVar – 1) to the recursive call? Also, what’s wrong with if-then-else?
Incidentally, ByVal is necessary here because we’re passing a Variant argument, which needs to be copied (assigned) to effect the type-conversion. You get an error without the ByVal (that is, if it’s ByRef).
One possible use for argument-modification is to pass back a status value to the caller:
Sub TestDodgy() Dim error As Boolean Dim result As Integer Debug.Print error result = Dodgy(42, error) Debug.Print error End Sub Function Dodgy(n As Integer, ByRef err As Boolean) As Integer If n > 100 Then Dodgy = n Else err = True End If End Function
(The explicit ByRef is just for clarity; it’s the default). This prints out False, then True, illustrating that error (in the caller) and err (in the called) are the same run-time variable. I gather that this technique is used in function-call APIs, which I guess is okay in that context.
However, my feeling is that, in application code, we’d be better off with the error variable at the module-level. Better still, a Class module could make the error status visible as a property. This would allow callers in other modules to follow the pattern: Obj.AttemptX, If Obj.SucceededX Then …
Let’s look at the two mechanisms with a simple example:
Function ByvalFunc(ByVal str As String) As Boolean str = "yyy" 'this change does not propagate ByvalFunc = True End Function Function ByrefFunc(ByRef str As String) As Boolean str = "zzz" 'this change propagates ByrefFunc = True End Function Function ByrefFunc2(ByRef str As String) As Boolean Dim str2 As String str2 = str str2 = "xxx" 'this change does not propagate ByrefFunc2 = True End Function Sub Test() Dim res As Boolean Dim s1 As String, s2 As String, s3 As String, s4 As String s1 = "foo" s2 = "bar" s3 = "baz" s4 = s2 res = ByvalFunc(s1) res = ByrefFunc(s2) res = ByrefFunc2(s3) Debug.Print s1 Debug.Print s2 Debug.Print s3 Debug.Print s4 End Sub
We get: foo, zzz, baz, bar. So the ByRef string s2 is the one that is modified in the called function. Assignment in either the called procedure (s3) or the caller (s4) breaks the link by giving us a new variable.
In the call to ByrefFunc, the caller can force ByVal passing of s2, by using extra parentheses: res = ByrefFunc((s2)). This is rather dodgy syntactically, since in any other context, superfluous parentheses are ignored.
Now comes a twist. Suppose that we have subs, rather than functions:
Sub ByvalSub(ByVal str As String) str = "yyy" End Sub Sub ByrefSub(ByRef str As String) str = "zzz" End Sub Sub Test() ... s1 = "foo" s2 = "bar" ByvalSub (s1) ByrefSub (s2) Debug.Print s1 Debug.Print s2
In neither case does the change in the called sub propagate to the caller. This strikes me as weird. Assuming that you want argument-changeability at all, why would you not want it for subs? Any clarification welcome!
Fortunately, with Objects, things work as you would want. Arguments passed to Property Set or other methods of an object are typically assigned (directly or indirectly) to the private variables of the object. In this respect, they are like str2 in ByRefFunc2, above.
What happens when arguments are objects? Here, there is an extra level of indirection: the value of an object-typed variable is an object-reference, not the object structure itself. So if we pass an object-typed variable ByVal, the called procedure gets a copy of the object-reference; the object does not get copied. This is entirely reasonable, as the object could be large, complex, or a piece of our application. Here’s an example:
Sub TestObj1() Dim li As List Set li = New List li.Add ((42)) TestObj2 li Debug.Print "TestObj1", li.First Set li = Nothing End Sub Sub TestObj2(ByVal li As List) li.Add (66) Debug.Print "TestObj2", li.GetNth(2) Set li = Nothing End Sub
This works, printing out 66, then 42. If TestObj2 had a ByRef parameter (explicitly or by default), then the call to li.first in TestObj1 fails with a ‘No Object’ error.
Why might you want to alter an object-typed parameter? Good question… One possibility is Memory Management. If the object (reference) is passed around ByRef, there is effectively a single shared reference. In this case, whichever procedure sets the reference to nothing deletes the object (I guess we’re thinking of something potentially large, like a Recordset). It might not be clear whose responsibility this is. On the other hand, if the object (reference) is passed around ByVal, then each procedure can set its own local reference to nothing, when it’s finished with it, without worrying about the wider usage – as in the example above. Any comments on this would be welcome.
There are some pages in the MSDN library on this: look at and around this one. This is Visual Basic, not VBA, and I noticed that it says: “The default in Visual Basic is to pass arguments by value”. Since the VBA default is ByRef, this looks like a potential gotcha, if you ever tried to migrate code between the two. Curious…