As you know, data types in the .NET Framework come in two varieties: value types and reference types. Likewise, you can pass any argument to a subroutine or function using the ByVal or ByRef keyword. In both cases, the term value implies that you are working with the data and the term reference implies that you are working with a pointer (or reference) to the data.
So, when you pass arguments to a subroutine or function, you absolutely must know whether the type is a value type or a reference type. Only then can you determine whether you want to use ByVal or ByRef to pass the data. Why? Well, let me answer this question with several of my own: Can you pass a value type by reference? What if you pass a reference type by value? Or a reference type by reference? As you can see, this can get pretty messy, pretty fast.
To help clear things up, this tip enumerates all the possibilities and describes what's actually happening behind the scenes in each situation. Once you understand that, you'll begin to see what the best approaches are for various situations.
So, here are the possibilities:
- Value type, passed by value:
This is the simplest of all the possibilities. A value type holds the data itself, not a reference to it. And, when that value type is passed by value, a copy of that data is passed. The subroutine/function cannot change the value of this variable back in the calling routine.
Behind the scenes, a copy of the variable's value is passed on the stack.
- Value type, passed by reference: This is also pretty straightforward. The variable holds the actual data, but when it is passed, a reference variable is created that points to the data. That way, changes to the variable are reflected back in the calling routine.
Behind the scenes, this means that a pointer to the data is passed on the stack to the calling routine.
- Reference type, passed by value: A reference type is one that is always manipulated using pointers. So, the value is always in the heap. If you pass a reference type by value, what you are really doing is passing the pointer by value, not the variable itself. This, counter-intuitively, means that you can, in fact, make changes to the data and those changes are reflected back in the calling routine—despite the fact that the data was passed ByVal. So, if ByVal doesn't work like you'd expect with reference types, how do you pass a reference type variable to a subroutine/function and prevent it from being changed back in the calling routine? You can't. (At least not without significant rework...)
- Reference type, passed by reference: As I've already said, a reference type is one that is always referred to using a pointer. So, what happens when you specify that a reference type be passed ByRef? Well, it does exactly what you told it to—it makes a new reference variable that points to the reference variable that points to the data. It's a pointer to a pointer. If you come from a C/C++ background, you know that this type of thing can get really complicated really fast. Fortunately, VB.NET handles all the de-referencing for you and keeps it as simple as possible.
So, the upshot is you can modify the data in a reference type passed by reference. But that's no surprise—you could modify the data in a reference type passed by value. Because of this and because of the unnecessary additional complexity incurred when using pointers to pointers, you should typically avoid this situation altogether. In other words, typically, you should always pass reference variables by value. Just be aware that they can be modified!
That covers most of the common scenarios. There is one more, less-common, scenario where the difference between a reference type passed by value vs. a reference type passed by reference actually makes a difference.
Suppose you pass a reference type by value to a subroutine and that subroutine sets the reference to NULL. Will the reference variable back in the calling routine also be set to NULL? Actually, no. When a reference type is passed by value, you can change the data, but you cannot change the reference itself. (Remember, the reference itself was passed by value.) However, if you pass the reference variable by reference, you can, in fact, set it to NULL and have that reflected back in the calling routine. This circumstance is the rare instance when passing a reference variable by reference would be necessary.
Whew! I know this can be confusing, but the takeaways are pretty straightforward:
- With value-type variables, ByRef and ByVal work as you'd expect—either allowing or disallowing variable data changes to be reflected back in the calling routine.
- With reference variables, changes you make in a subroutine/function will always be reflected back in the calling routine, regardless of whether you pass ByVal or ByRef.
- When working with reference variables, you'll almost always want to pass ByVal to keep things simple.
- The only exception to this rule is in situations where you want to give the subroutine/function the ability to modify the reference itself (like setting it to NULL or pointing it to a different instance).
A Note for VB 6-to-VB.NET Converts
Finally, if you have projects that have been converted from VB 6 to VB.NET, remember that the default in VB 6 was ByRef, whereas in VB.NET it is ByVal. When using automated conversion software, typically all parameters are specified as ByRef. This means that you are likely to find many instances of reference types passed by reference. While this won't cause your code to fail, it is bad programming practice and creates less efficient IL code. You should make a sweep through your code and convert any passed reference variables to ByVal.