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.