Reference and Value Types as Function Parameters Using Ref/Out Keywords

Value types and reference types are very common concepts to discuss in C#. Everybody knows about their behavior. But their 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 concepts of Value and Reference types. But we will discuss these concepts again, briefly, since they will act as a base for our further discussion. The following are a few important points to be mentioned before we proceed.

  • In this discussion, we will refer to the actual data of both the variables as an object and a pointer to an object (or data) as a reference.
  • The value of a Value type variable is the object (or data) itself and the value of a reference type variable 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, that is passed as a parameter to the function and for reference types, it's the reference or pointer to that object, that 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 then 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 as in the following:

Value Types behavior

On the other hand, the 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 variables. Conceptually, their behavior is something as in the following:

Reference type variables

Our next step is to discuss 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 a Value type variables as a parameter to functions

Look at the following code snippet. We simply declare an integer (value type) variable with the 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.
 
Value type variables as a parameter

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.
 
results

Passing reference type as a parameter to the function

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 change the reference to the object, in other words reference or pointer to data, inside the function and the 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. The following ar our cases.
  1. Changing the value of the reference type (that is a 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 the function, inside the function and after the function call is complete. So our code becomes:

    Changing the value of reference type

    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.

    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 the value of the variable (that 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 in other words _classInstance, that 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:

    flow of this code
     
  2. Changing the object (or actual data) pointed by the value of referenceto

    In this case, we will change the object (or the data), inside the function, to which the reference points to and rest of the code remains the same. See the code below:

    Changing the object

    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 class instances, the one which is outside the function call and the other inside the function, are pointing to the same data and the changes are reflected outside the function call as well. See the results below:

    Run

So the preceding process can be represented diagrammatically as:

diagrammatically

So from the code above examples, we can see that:
  1. Changes in the value of a value type variable (which is an object or actual data), inside a function, are not persisted outside the function call.
  2. Changes in the value of a reference type variable (which is the reference or pointer to the object), does 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 (since 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 that 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 the 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, a 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 somerthing before we pass them, but a ref type does not require any initialization. So let's modify the examples one by one again. We will use only a 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 the following:

out or ref keywords
So after the change, run the code and see that the value changed inside the function, persists outside the function as well. This is because, using the ref or out keywords, the memory location of _testValue (say 5001), that contains the value 111, was sent to the function and not a copy of the data. So when the 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.
 
Code

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), using the new operator (the same case as we discussed above: Changing the value of the reference type). Again, we will pass the parameter to the function, using the ref keyword.
 
ref keyword

Run the code and see the results.
 
Output

Remember the case we discussed earlier (changing the value of a reference type), the change did not persist outside the function call. But this time it did. The reason this change happened was that, passing the parameter to the function, using the ref keyword, resulted in passing the memory location of _classInstance (say 7001, that contains the further reference to the object or data, say 9001) and not a copy of the reference to the object (or 9001) that 7001 holds. So the same 7001's value was changed to point to a new address (say 9003), when the 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 a little confusing, so let's convert it into simple language. You need to return two or more 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. I hope you enjoyed reading it...!!!