Back to Blog
C#

C# Ref Return: Syntax, Usage, and Performance

c# ref return: Learn how C# ref return works, how to use ref locals, and when returning by reference improves performance without copying data.

ref returnref localsC# performanceC# syntaxmemory optimization
Diagram showing a method returning a reference to an array element, with an arrow pointing to the original storage location.

When you return a value from a method in C#, the caller typically receives a copy. For large structs or array elements, that copy adds overhead. The c# ref return feature lets you return a reference to a variable, allowing the caller to read or modify the original storage location directly. This is useful when you want to avoid copying a large value type or when you need to expose an element of a collection for in-place modification without using an indexer or a separate method.

What Is a Ref Return in C#?

A ref return is a method return type that returns a reference to a variable rather than a copy of its value. The method signature uses the ref keyword before the return type, and the return statement must also use ref. The caller can then treat the result as a reference, either by assigning it to a ref local or by using it directly to read or modify the underlying data.

This feature was introduced in C# 7.0 and is part of the broader set of features that enable writing more efficient code when working with value types. It is particularly relevant when dealing with large structs, arrays, or other data structures where copying the entire value would be expensive.

Declaring a Method That Returns by Reference

To declare a ref return method, place ref before the return type in the method signature and before the returned variable in the return statement. The returned variable must be a reference to a storage location that outlives the method call, such as an array element, a field, or an argument passed by reference. You cannot return a local variable because it goes out of scope when the method returns.

Here is a minimal example that returns a reference to the largest element in an integer array:

public ref int FindLargest(int[] numbers) { int largestIndex = 0; for (int i = 1; i < numbers.Length; i++) { if (numbers[i] > numbers[largestIndex]) { largestIndex = i; } } return ref numbers[largestIndex]; }

The method returns a reference to the array element that holds the largest value. If you call this method and assign the result to a regular variable, you get a copy. To work with the reference itself, you need a ref local.

Using Ref Locals to Read and Modify the Returned Reference

A ref local is a local variable that holds a reference to another variable. You declare it with the ref keyword, and you can assign it from a ref return method. Once you have a ref local, you can read or modify the original storage location through it.

int[] scores = { 10, 20, 30 }; ref int maxScore = ref FindLargest(scores); maxScore = 100; // This modifies scores[2] directly Console.WriteLine(scores[2]); // Output: 100

Without the ref keyword on the local variable, maxScore would be a copy, and assigning to it would not affect the array. The ref keyword on both the method and the local is required to preserve the reference semantics.

You can also use a ref return directly without a ref local, but then you must use it as a reference immediately, for example by assigning to an array element or passing it to a method that takes a ref parameter. The most common pattern is to combine a ref return with a ref local to enable direct modification of the underlying data.

Performance and Memory Implications of Ref Returns

The main motivation for using ref returns is to avoid copying large value types. When a method returns a large struct by value, the runtime copies the entire struct to the caller's stack or heap. If that method is called frequently or the struct is particularly large, the copying overhead can become significant. A ref return avoids that copy by handing back a reference to the original storage location.

For example, consider a method that finds a record in an array of large structs. Returning the struct by value would copy the whole record. Returning a reference to the array element lets the caller read or modify the original record without any copying.

public ref LargeStruct FindRecord(LargeStruct[] records, int id) { for (int i = 0; i < records.Length; i++) { if (records[i].Id == id) { return ref records[i]; } } throw new KeyNotFoundException("Record not found"); }

This pattern is especially useful in performance-sensitive code paths, such as game loops, data processing pipelines, or any scenario where you need to update many elements in a collection. The performance benefit comes from eliminating the copy, not from any runtime magic. The actual speedup depends on the size of the type and how often the method is called.

Ref returns also enable in-place modification of collection elements without needing to read, modify, and write back the entire value. This reduces the number of memory accesses and can improve cache locality when working with arrays.

Restrictions and Common Pitfalls

Ref returns come with several restrictions that you must respect. The most important is that you cannot return a reference to a local variable, because the local variable is destroyed when the method exits. Attempting to do so produces a compile-time error.

public ref int InvalidReturn() { int value = 42; return ref value; // Error: cannot return local by reference }

You also cannot return a reference to a property, because properties are methods, not storage locations. Similarly, you cannot return null directly; if you need to signal that no reference is available, you must throw an exception or return a reference to a static sentinel value.

Another pitfall is accidentally creating a copy when you intend to use a reference. If you assign the result of a ref return method to a non-ref variable, you get a copy. The compiler will not warn you about this, so you must be explicit about using ref on the caller side.

Ref returns also interact with the readonly modifier. You cannot return a readonly field by ref unless you use the readonly ref return, which prevents the caller from modifying the referenced value. This is useful when you want to expose a reference to a readonly field without allowing external modification.

When to Use Ref Returns Instead of Regular Returns

Ref returns are not a universal replacement for regular returns. They add complexity to the method signature and to the caller, and they impose restrictions on what you can return. Use them when you have a clear need to avoid copying a large value type or when you want to expose a direct handle to an internal data structure for modification.

A good candidate is a method that returns an element from a collection, especially if the element type is a large struct. Instead of returning the struct by value and then writing it back, you can return a reference and let the caller modify the original. This is common in custom collection implementations or in algorithms that need to update elements in place.

Avoid ref returns for small types like int, bool, or double, where the copy cost is negligible. The added complexity of ref locals and the risk of aliasing are not worth the micro-optimization. Also avoid ref returns when the method might need to return a different storage location based on runtime conditions, such as a fallback value, because you cannot return a local variable.

If you need to return a reference to a field that is not an array element, you can do so, but the field must be a field of the class or struct, not a local variable. This allows you to expose internal state directly, but it also breaks encapsulation. Consider whether the caller really needs direct access or whether a method that performs the update would be safer.

Language Version and Compatibility Notes

The ref return feature is a language feature, not a runtime feature. It requires a compiler that supports C# 7.0 or later. The generated IL uses the ref return type, which is supported by the .NET runtime starting from .NET Framework 4.6.1 and all later versions of .NET Core and .NET 5+. If you are using an older compiler or targeting an older runtime, you may need to upgrade.

In modern C# versions, you can also use ref readonly returns, which allow returning a reference that cannot be modified by the caller. This is useful for exposing internal data without allowing external mutation. The readonly modifier is placed after ref in the method signature.

public ref readonly LargeStruct GetRecord(LargeStruct[] records, int id) { // ... return ref records[i]; }

The caller must use ref readonly local to consume such a return. This pattern is particularly relevant when you want to avoid copying large structs while still preserving immutability from the caller's perspective.

When using ref returns, be aware of the lifetime rules. The reference returned must be valid as long as the caller uses it. For array elements, the array must not be resized or garbage collected while the reference is in use. The compiler enforces some of these rules, but you still need to reason about the underlying storage to avoid dangling references.

Overall, ref returns are a powerful tool for specific scenarios. They let you write high-performance code that avoids unnecessary copies and enables direct manipulation of data structures. Understanding the syntax, restrictions, and tradeoffs will help you decide when to use them and when to stick with regular returns.

c# ref return: Practical Usage and Code Examples | RYUSLOG DEV