Over the years, I’ve noticed that there seems to be a lot of confusion about the difference between passing a parameter ByVal vs. ByRef. So, I thought I’d spend some time writing something on this topic. It should be noted, that while I will be discussing this from the view point of VB.NET, the same basic concepts also apply to VB.CLASSIC – it’s just that some of the terminology is different.
First, let’s cover the definition of these two keywords as they are presented in the Visual Basic documentation.
Specifies that an argument is passed in such a way that the called procedure or property cannot change the value of a variable underlying the argument in the calling code.
Specifies that an argument is passed in such a way that the called procedure can change the value of a variable underlying the argument in the calling code.
On the surface, these two statements seem simple enough. ByVal (short for By Value) means that the called procedure can not alter the value that was passed. In essence, when you pass ByVal – a copy of the original value is made and passed to the procedure. ByRef (short for By Reference) on the other hand is just the opposite. When, a value is passed ByRef, then a reference – aka pointer (in fact it is more accurate to say that it is a C++ reference then a pointer) – to the original value is passed, so any changes made to this value alter the callers state. Just to test this out, lets create a little test code and see what happens:
1: Option Explicit On
2: Option Strict On
3:
4: Module Module1
5:
6: Sub Main()
7: Dim i As Integer = 0
8:
9: ' Pass an integer ByVal
10: Console.WriteLine("Before ByVal Call: i = {0}", i)
11: PassIntByVal(i)
12: Console.WriteLine("After ByVal Call: i = {0}", i)
13:
14: Console.WriteLine()
15:
16: ' Pass an integer ByRef
17: Console.WriteLine("Before ByRef Call: i = {0}", i)
18: PassIntByRef(i)
19: Console.WriteLine("After ByRef Call: i = {0}", i)
20:
21: End Sub
22:
23: Sub PassIntByVal(ByVal i As Integer)
24: i = 5
25: End Sub
26:
27: Sub PassIntByRef(ByRef i As Integer)
28: i = 5
29: End Sub
30:
31: End Module
And our output:
So far so good. It looks exactly like we should expect given the definitions of ByVal and ByRef. So, if things work according to the definition, why the confusion? Well, lets expand this simple example a bit and see if things continue to hold up – this time by creating a structure and putting it to the same test as above:
1: Option Explicit On
2: Option Strict On
3:
4: Module Module1
5:
6: Sub Main()
7: Dim i As Integer = 0
8:
9: ' Pass an integer ByVal
10: Console.WriteLine("Before ByVal Call: i = {0}", i)
11: PassIntByVal(i)
12: Console.WriteLine("After ByVal Call: i = {0}", i)
13:
14: Console.WriteLine()
15:
16: ' Pass an integer ByRef
17: Console.WriteLine("Before ByRef Call: i = {0}", i)
18: PassIntByRef(i)
19: Console.WriteLine("After ByRef Call: i = {0}", i)
20:
21: Console.WriteLine()
22:
23: Dim ts As TestStructure
24:
25: ' Pass a structure ByVal
26: Console.WriteLine("Before ByVal Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
27: PassStructureByVal(ts)
28: Console.WriteLine("After ByVal Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
29:
30: Console.WriteLine()
31:
32: ' Pass a strucutre ByRef
33: Console.WriteLine("Before ByRef Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
34: PassStructureByRef(ts)
35: Console.WriteLine("After ByRef Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
36:
37: End Sub
38:
39: Sub PassIntByVal(ByVal i As Integer)
40: i = 5
41: End Sub
42:
43: Sub PassIntByRef(ByRef i As Integer)
44: i = 5
45: End Sub
46:
47: Sub PassStructureByVal(ByVal s As TestStructure)
48: s.i = 5
49: s.j = 10
50: End Sub
51:
52: Sub PassStructureByRef(ByRef s As TestStructure)
53: s.i = 5
54: s.j = 10
55: End Sub
56:
57: Structure TestStructure
58: Public i As Integer
59: Public j As Integer
60: End Structure
61: End Module
And our output:
The output should be consistent with our expectations. Again, what’s the problem? I’m going to do one more expansion of our code – lets add a custom class into the mix:
1: Option Explicit On
2: Option Strict On
3:
4: Module Module1
5:
6: Sub Main()
7: Dim i As Integer = 0
8:
9: ' Pass an integer ByVal
10: Console.WriteLine("Before ByVal Call: i = {0}", i)
11: PassIntByVal(i)
12: Console.WriteLine("After ByVal Call: i = {0}", i)
13:
14: Console.WriteLine()
15:
16: ' Pass an integer ByRef
17: Console.WriteLine("Before ByRef Call: i = {0}", i)
18: PassIntByRef(i)
19: Console.WriteLine("After ByRef Call: i = {0}", i)
20:
21: Console.WriteLine()
22:
23: Dim ts As TestStructure
24:
25: ' Pass a structure ByVal
26: Console.WriteLine("Before ByVal Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
27: PassStructureByVal(ts)
28: Console.WriteLine("After ByVal Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
29:
30: Console.WriteLine()
31:
32: ' Pass a strucutre ByRef
33: Console.WriteLine("Before ByRef Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
34: PassStructureByRef(ts)
35: Console.WriteLine("After ByRef Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
36:
37: Console.WriteLine()
38:
39: Dim tc As New TestClass
40:
41: ' Pass a class ByVal
42: Console.WriteLine("Before ByVal Call: tc.i = {0}, tc.j={1}", tc.i, tc.j)
43: PassClassByVal(tc)
44: Console.WriteLine("After ByVal Call: tc.i = {0}, tc.j={1}", tc.i, tc.j)
45:
46: Console.WriteLine()
47:
48: ' Pass a class ByRef
49: Console.WriteLine("Before ByRef Call: tc.i = {0}, tc.j={1}", tc.i, tc.j)
50: PassClassByRef(tc)
51: Console.WriteLine("After ByRef Call: tc.i = {0}, tc.j={1}", tc.i, tc.j)
52:
53: End Sub
54:
55: Sub PassIntByVal(ByVal i As Integer)
56: i = 5
57: End Sub
58:
59: Sub PassIntByRef(ByRef i As Integer)
60: i = 5
61: End Sub
62:
63: Sub PassStructureByVal(ByVal s As TestStructure)
64: s.i = 5
65: s.j = 10
66: End Sub
67:
68: Sub PassStructureByRef(ByRef s As TestStructure)
69: s.i = 5
70: s.j = 10
71: End Sub
72:
73: Sub PassClassByVal(ByVal c As TestClass)
74: c.i = 5
75: c.j = 10
76: End Sub
77:
78: Sub PassClassByRef(ByRef c As TestClass)
79: c.i = 6
80: c.j = 11
81: End Sub
82:
83: Structure TestStructure
84: Public i As Integer
85: Public j As Integer
86: End Structure
87:
88: Class TestClass
89: Public i As Integer
90: Public j As Integer
91: End Class
92:
93: End Module
And the output:
Take a look at the lines of output for the tc variable. Have we been mislead by the documentation? Because, on the surface at least, it sure appears as if tc was modified when passed ByVal. The answer to this question is no – ByVal still worked exactly as it was supposed to.
To understand why these results are in fact correct, requires a bit of understanding of the .NET type system. The .NET CTS – or Common Type System – defines two basic type categories (each of which is further subdivided). These categories are Value types and Reference types. Here is what the documentation has to say regarding these two categories:
-
Value types
Value types directly contain their data, and instances of value types are either allocated on the stack or allocated inline in a structure. Value types can be built-in (implemented by the runtime), user-defined, or enumerations. For a list of built-in value types, see the .NET Framework Class Library.
-
Reference types
Reference types store a reference to the value’s memory address, and are allocated on the heap. Reference types can be self-describing types, pointer types, or interface types. The type of a reference type can be determined from values of self-describing types. Self-describing types are further split into arrays and class types. The class types are user-defined classes, boxed value types, and delegates.
Summary: A value type variable directly contains it’s data, a reference type variable does not. In practical terms, this means that the behavior demonstrated by the example code is in fact completely consistent with the documented behavior of ByVal and ByRef. When passing ByVal, the copy that is made is the contents of the variable – in the case of a reference type, the value that the variable contains is not the instance, but the memory address of the instance on the heap. Maybe a small diagram will help clarify:
What this means in practical terms is that, if you pass an object ByVal – the objects properties can be modified, but the reference can not:
1: Option Explicit On
2: Option Strict On
3:
4: Module Module1
5:
6: Sub Main()
7: Dim tc As TestClass = Nothing
8:
9: ' pass by value
10: Console.WriteLine("Passing tc ByVal")
11: CreateTCByVal(tc)
12: Console.WriteLine(If(tc IsNot Nothing, String.Format("tc.i = {0}, tc.j={1}", tc.i, tc.j), "tc is Nothing"))
13:
14: Console.WriteLine()
15:
16: ' pass by reference
17: Console.WriteLine("Passing tc ByRef")
18: CreateTCByRef(tc)
19: Console.WriteLine(If(tc IsNot Nothing, String.Format("tc.i = {0}, tc.j={1}", tc.i, tc.j), "tc is Nothing"))
20: End Sub
21:
22: Sub CreateTCByVal(ByVal c As TestClass)
23: c = New TestClass()
24: c.i = 5
25: c.j = 6
26: End Sub
27:
28: Sub CreateTCByRef(ByRef c As TestClass)
29: c = New TestClass()
30: c.i = 5
31: c.j = 6
32: End Sub
33:
34: Class TestClass
35: Public i As Integer
36: Public j As Integer
37: End Class
38:
39: End Module
And the output:
I hope this brief explanation of ByVal and ByRef can help clear up some of the confusion that seems to exist regarding these to argument passing methods.