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
Factorial = Factorial(MyVar) * (MyVar + 1)
Dim var As Variant
var = 5
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:
Dim error As Boolean
Dim result As Integer
result = Dodgy(42, error)
Function Dodgy(n As Integer, ByRef err As Boolean) As Integer
If n > 100 Then
Dodgy = n
err = True
(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
Function ByrefFunc(ByRef str As String) As Boolean
str = "zzz" 'this change propagates
ByrefFunc = True
Function ByrefFunc2(ByRef str As String) As Boolean
Dim str2 As String
str2 = str
str2 = "xxx" 'this change does not propagate
ByrefFunc2 = True
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)
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"
Sub ByrefSub(ByRef str As String)
str = "zzz"
s1 = "foo"
s2 = "bar"
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:
Dim li As List
Set li = New List
Debug.Print "TestObj1", li.First
Set li = Nothing
Sub TestObj2(ByVal li As List)
Debug.Print "TestObj2", li.GetNth(2)
Set li = Nothing
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…