Value types and reference types are very common concepts to discuss in C#. Everybody knows about their behavior. But there behavior becomes very interesting, when passed as parameters to function calls, especially with the use of ref and out keywords. In this article, we will try to study their behavior when we use them as function parameters, both, with and without the use of ref and out.
We all know the basic concept of the Value and Reference types. But we will discuss these concepts again, briefly, as they will act as a base for our further discussion. Few important points to be mentioned, before we move on.
- In this discussion, we will refer the actual data of both the variables as object and pointer to object(or data) as reference.
- The value of a Value type is the object (or data) itself and value of a reference type is
a reference/pointer to the object(or data).
- While passing the value and reference types as parameters to a function, a copy of their values is passed to the function and NOT the original values. So, for value types, it is the object or data they hold, is passed as a parameter to function and for reference types, its the reference or pointer to that object, is passed as a parameter.
C# data types can be of two types – Value Types and Reference Types. Value type variables contain their object(or data) directly. If we copy one value type variable to another, we are actually making a copy of the object for the second variable. Both of them will independently operate on their values. Changes in any one of them, will not affect the other variable. Their behavior is something like below :
On the other hand, value of a Reference type variables is actually a reference to the object. If we copy one reference type variable to another variable, we are actually copying the reference of the object, to the second variable. So both the variables will point to the same object and any change in anyone of them, will be reflected in both the variables. Conceptually, their behavior is something like below :
Our next step is discussing the behavior of passing the value and reference type variables, as a parameter to the functions, and, how they behave, if we make any changes to them, inside the function.
Passing Value type variables as a parameter to the functions :
Look at the following code snippet. We simply declare an integer(value type) variable with value 111 and pass it to a function call. Inside the function, we change the value of the parameter testValue to 999. Then we check its value before the function call, inside the function and after the function call is completed.
Now, Run the code and we can see that, the changes we made in the variable _testValue, are not retained outside the function. This is because, during the function call, a copy of the object was sent and not the actual object or in other words, a copy of the data was sent to the function and not the actual data. So the changes remain local to the function call. See the results below.
Passing reference type as a parameter to the functions :
Because the data of a reference type variable is actually a reference or pointer to the actual data, we will discuss the reference type parameters with 2 possible cases. One is, we will to change the reference to the object i.e. reference or pointer to data, inside the function and other is we change the object(or the actual data), to which the reference points to. So let’s discuss these cases one by one. For this purpose, we will create a class named testClass with an integer and string type properties, named testString and testInteger respectively. Our cases will be
1. Changing the value of reference type (which is reference or pointer to object or data) :
In this case, we will pass a class instance as a parameter to the function. Inside the function, we will re-initialize the instance of the class, with new values to its properties. Then, we will examine the values of the properties before we pass them to function, inside the function and after the function call is complete. So our code becomes :
So, we have initialized a testClass instance and passed it to the function. Inside the function, we re-initialize it, to change the reference, using the new operator. Run the code and see the results.
This change also remains local to the function and is not persisted outside the function call. Again, the reason is the same that during the function call, we passed a copy of value of the variable (which is a reference to the object or data, in this case), to the function. Inside the function, we have changed a copy of its value, to point to a new location, using the new operator. The original variable i.e. _classInstance, which was passed to the function call, keeps its original value and continues pointing to the old data. So the flow of this code can be conceptually represented as :
2. Changing the object (or actual data) pointed by the value of reference :
Run the code and see the results. This time, although a copy of value of variable was sent to the function, the change in object is persisted outside the function call as well. The reason is, that, inside the function, we are changing the object(or actual data), to which the reference is pointing to and NOT the actual reference. So now both the class instances, the one which is outside the function call and other, which is inside the function, are pointing to the same data and the changes are reflected outside the function call as well. See the results below :
So the above process can be represented diagrammatically as :
So from the above code examples, we can see that :
1. Changes in value of a value type variable(which is object or actual data), inside a function, are not persisted outside the function call.
2. Changes in the value of reference type variable (which is the reference or pointer to object), do not persist outside the function call.
3. Changes in the object (or actual data) of the reference type variables, is persisted outside the function call (as both the variables, passed to the function and inside function, point to the same data).
Passing by Reference : Before we move further, we need to discuss briefly about a concept called passing by reference. This is the concept that works behind the logic of ref and out. In our examples above, if we pass the variables using the out or ref keywords, the parameters to the function are passed by reference. In other words, we can say that the memory location to which a variable is representing, is being sent in the function call and not the copy of the values of the variables.
For example, say we have a variable int x = 21. Now if x is representing a memory location 1000, then 1000 contains the data 21 itself. When x was passed to the function calls without out or ref, the value 21 is sent as a copy to the functions. But, when we send it using the ref or out, we pass the actual memory location which x represents, to the function, which in this case is 1000.
Similarly, for any instance of a class, say obj1 is representing the memory location 1002, then 1002 will contain the another memory location, say 3001, pointing to the actual data. So when obj1 is being sent to function call, without the use of ref or out, it will send 3001 as a copy to the function. But, when we send it using ref or out, we pass the actual memory location to the function, which in this case is 1002.
So in both the example above, without ref or out, copy of the actual data (21 and 3001 respectively) was sent to the function calls.
We will now change our code to pass these variables using the ref or out keywords. We can use either of them. The only difference between them is that out parameters require us to initialize before we pass them, but ref type do not require any initialization. So let’s modify the examples one by one again. We will use only ref keyword to discuss the behavior.
Passing Value type variables using out or ref keywords:
The only changes we need to do in this case is to add the ref keyword in the function definition and also pass the parameters to the function with the ref keyword, during the function call. Our code now changes to following :
So after the change, run the code and see that the value changed inside the function, persists out side the function as well. This is because, using the ref or out keywords, memory location of _testValue (say 5001), which contains the value 111, was sent to the function and NOT a copy of the data. So when value was changed to 999, it was updated at the location 5001. So changes in the values were updated at the location 5001 and as a result, changes were reflected in both the variables, original and inside the function.
Passing Reference type variables using ref keyword
For reference type variables, we will be discussing the case where we will be changing the value of the reference type variable or we can say, reference to the object(or actual data), by using new operator (the same case we discussed above – Changing the value of reference type). Again, we will pass the parameter to the function, using the ref keyword.
Run the code and see the results.
Remember the case we discussed earlier (Changing the value of reference type), the change did not persist outside the function call. But this time it did. The reason for this change to happen was that, passing the parameter to the function, using the ref keyword, resulted in passing the memory location of _classInstance(say 7001, which contain the further reference to the object or data, say 9001) and NOT a copy of the reference to object(or 9001) which 7001 holds. So the same 7001‘s value was changed to point to a new address(say 9003), when new operator was applied and the change was reflected in both the instances, original and inside the function.
When to use ref or out keywords ?
The ability of these keywords to affect the original value of the variables, guides their usage. Sounds bit confusing, let’s convert it into simple language. You need to return two or more different types of values from a function. But a function can return only a single or no value at a time. What we do is, we create the function, with out and ref parameters and set the values inside the function. So the parameters with out and ref were sent as an empty containers to the function. The function fills the data inside these variables and return them to you, guided by the basic ability of the keywords, to pass the actual data and not a copy of these variables.
In short, when you need to return multiple values from a function, you can go for the ref or out keywords.
This was about the concept of the using ref and out parameters with the value and reference types. Hope you enjoyed reading it…!!!