C# has always supported the ability to pass by reference using the ref
keyword on method parameters. C# 7 adds the ability to return by reference and to store references in local variables.
The primary reason for using ref returns and ref locals is performance. If you have big structs, you can now reference these directly in safe code to avoid copying. Before C# 7 you had to work with unsafe code and pointers to pinned memory.
A secondary reason for using ref returns and ref locals is to create helper methods that were not possible before C# 7.
There are some restrictions on the usage of ref returns and ref locals to keep things safe:
- returned refs must point at mutable (not readonly) object fields, or have been passed in by reference
- ref locals cannot be mutated to point at a different location
These restrictions ensure that we "never allow an alias to a dead variable", Eric Lippert. Which means that the compiler will make sure that objects returned by reference, will be accessible after the method has returned, and will not be cleaned up by the garbage collector.
How to Use
Ref Returns
To return by reference, add the keyword ref
before the return type on any method signature and after the keyword return
in the method body. For example, the Get
method in Score
returns the private field value
by reference. If value
were readonly
, the compiler would not permit it to be returned by reference.
public class Score
{
private int value = 5;
public ref int Get()
{
return ref this.value;
}
public void Print()
{
Console.WriteLine($"Score: {this.value}");
}
}
Ref Locals
To store a reference into a local variable, define the local variable as a reference by adding the keyword ref
before the variable type and add the keyword ref
before the method call. For example, in the following code sample, highscore
is a ref local. As shown by anotherScore
, you can still get a value (as opposed to a reference) when calling a ref returns method, by omitting the ref
keyword when making the call.
public void test1()
{
var score = new Score();
ref int highscore = ref score.Get();
int anotherScore = score.Get();
score.Print();
Console.WriteLine($"Highscore: {highscore}");
Console.WriteLine($"Another Score: {anotherScore}");
highscore = 10;
anotherScore = 20;
this.change(highscore);
score.Print();
Console.WriteLine($"Highscore: {highscore}");
Console.WriteLine($"Another Score: {anotherScore}");
}
public void change(int value)
{
value = 30;
}
Output:
Score: 5
Highscore: 5
Another Score: 5
Score: 10
Highscore: 10
Another Score: 20
From the output, we see that highscore
does indeed reference the private variable score.value
, as its value has changed too. Whereas anotherScore
contains a copy, as changing its value has no effect on score.value
. Finally, the call to change
shows that when ref locals are accessed without the ref
keyword, they behave just like normal locals and are passed by value to other methods.
Other Uses
Referencing Array Elements
It is also possible to return references into arrays. In this sample code, ThirdElement
is a method which returns a reference to the third element of an array. As test2
shows, modifying the returned value, modifies the array. Note that now value
points to the third position of values
, there is no way to change value
to point at a different position in the array or at a different variable entirely.
public void test2()
{
int[] values = { 1, 2, 3, 4, 5 };
Console.WriteLine(string.Join(",", values));
ref int value = ref ThirdElement(values);
value = 10;
Console.WriteLine(string.Join(",", values));
}
public ref int ThirdElement(int[] array)
{
return ref array[2];
}
Output:
1,2,3,4,5
1,2,10,4,5
You could use this ability to reference array elements to implement an array search helper, which returns a reference to the matching array element, rather than its index.
Referencing Local Variables
We can also reference other local variables, as shown in test3
. However, these references cannot be returned, because they disappear when the method returns.
public void test3()
{
int i = 5;
ref int j = ref i;
j = 10;
Console.WriteLine($"i: {i}");
Console.WriteLine($"j: {j}");
}
Output:
i: 10
j: 10
Assigning Values to Methods
Finally, with ref returns it is now possible to use a method on the left-hand side of an assignment. In test4
, Max
returns a reference to the variable with the maximum value, and therefore, j
, is changed to 20.
public void test4()
{
int i = 5;
int j = 10;
Console.WriteLine($"i: {i}");
Console.WriteLine($"j: {j}");
Max(ref i, ref j) = 20;
Console.WriteLine($"i: {i}");
Console.WriteLine($"j: {j}");
}
public ref int Max(ref int first, ref int second)
{
if(first > second)
return ref first;
return ref second;
}
Output:
i: 5
j: 10
i: 5
j: 20
Conclusion
Ref returns and ref locals are primarily useful for improving performance, but as we've seen with the Max
function and the array search helper, they find a role in creating certain helper methods too.
While you won't use ref returns and ref locals in all your code, they're a nice addition to the language for when you do need them.