C# ref vs in vs out: Choosing the Right Parameter Modifier
c# ref vs in vs out: Understand the differences between ref, in, and out in C#, when to use each, and how they affect performance and code clarity.
When you pass an argument to a method in C#, the default behavior is to pass a copy of the value. For reference types, that means the reference itself is copied, so the method can mutate the object but not reassign the caller's variable. For value types, the entire struct is copied. The ref, in, and out modifiers change this behavior by passing a reference to the original storage location, but each one has a distinct contract. Understanding c# ref vs in vs out is essential for designing APIs that are both efficient and clear about how they treat their inputs and outputs.
What All Three Modifiers Have in Common
All three modifiers cause the argument to be passed by reference, meaning the method operates on the same storage location as the caller's variable. No copy is made of the argument, which can be important for large structs. They also affect how the method can read and write that variable. The compiler enforces different rules for each modifier, and these rules are part of the method signature for overload resolution.
| Modifier | Caller initializes? | Method can read? | Method can write? | Typical use |
|---|---|---|---|---|
| ref | Yes | Yes | Yes | In-place modification |
| in | Yes | Yes | No | Read-only large structs |
| out | No | Only after assignment | Must assign | Returning values |
The ref Modifier: Read-Write Access to the Caller's Variable
ref is the most permissive modifier. The method can read and write the variable, and the caller must initialize the variable before passing it. The method is allowed to assign a new value to the parameter, and that assignment is visible to the caller after the method returns.
public void Increment(ref int number) { number++; } int value = 5; Increment(ref value); Console.WriteLine(value); // 6
The caller must use the ref keyword in both the method definition and the call. This makes the intent explicit: the method is allowed to change the variable. ref is commonly used for swapping values or for methods that need to update a value in place.
The in Modifier: Read-Only Reference for Large Structs
in is similar to ref, but the method is not allowed to assign to the parameter. It is read-only. This is useful when you want to pass a large struct by reference to avoid copying, but you have no intention of modifying it. The modifier was introduced in C# 7.2 specifically for performance-sensitive code that deals with large value types.
public readonly struct Point { public double X { get; } public double Y { get; } } public double Distance(in Point p1, in Point p2) { double dx = p1.X - p2.X; double dy = p1.Y - p2.Y; return Math.Sqrt(dx * dx + dy * dy); }
The compiler enforces that you cannot assign to an in parameter. You also cannot call methods on it that might mutate it unless they are explicitly marked as readonly. The caller does not need to use the in keyword at the call site, though you can include it for clarity. The compiler may also choose to pass by value if the struct is small, because the overhead of indirection can outweigh the copy cost.
The out Modifier: Guaranteed Assignment Before Return
out is used when the method must assign a value to the parameter before returning. The caller does not need to initialize the variable before the call, but the method must assign it. This is common for methods that return multiple values, such as TryParse patterns.
public bool TryParse(string input, out int result) { if (int.TryParse(input, out result)) { return true; } result = 0; // must assign even on failure return false; }
The compiler requires that every code path assigns to an out parameter before the method returns. This guarantees that the caller always receives a value. The out modifier is also used with var declarations in modern C#: TryParse(input, out var result).
Overload Resolution and Modifier Compatibility
The modifiers are part of the method signature. You cannot overload a method solely by changing ref to in or out; they are considered distinct. For example, these two methods are not ambiguous:
public void Process(ref int value) { } public void Process(out int value) { }
But calling Process(ref someInt) and Process(out someInt) would be different calls. The compiler uses the modifier at the call site to determine which overload to use. This means you should choose modifiers carefully because they affect the public API surface.
Performance Considerations: When Passing by Reference Actually Helps
The main performance benefit of ref and in is avoiding copying large structs. For small structs, the copy is often cheaper than the indirection. The JIT compiler may inline and optimize, but the general rule is: use in for large structs that are read-only, and use ref when you need to modify the caller's variable. out is not a performance optimization; it's a contract for returning values.
There is no benchmark data here, but the underlying mechanism is clear: copying a 64-byte struct on every call can add up, especially in tight loops. Passing by reference avoids that copy. However, in parameters can be passed by value if the compiler decides it's cheaper, so you should not assume that in always avoids a copy.
Common Mistakes and Edge Cases
A frequent mistake is using ref when in would be more appropriate, or using out when you could return a tuple. out forces the method to assign a value, which can lead to awkward code if you need to return multiple values from a method that also returns a bool. Tuples are often a cleaner alternative.
Another edge case is using ref with properties. You cannot pass a property as ref, in, or out because properties are methods, not storage locations. You need to use a local variable first.
public class Container { public int Value { get; set; } } var container = new Container(); // Modify(ref container.Value); // error: property cannot be passed by ref int temp = container.Value; Modify(ref temp); container.Value = temp;
Also, in parameters cannot be used with methods that are not readonly, because the compiler cannot guarantee that the method won't modify the struct. This can be restrictive when working with legacy code.
Choosing Between ref, in, and out in Real Code
The choice depends on what the method needs to do with the parameter. If the method must modify the caller's variable, use ref. If the method only reads a large struct and you want to avoid copying, use in. If the method must produce a value that the caller doesn't initialize, use out.
For APIs that return multiple values, consider returning a tuple or a custom result type instead of using out. out is still valuable for TryParse patterns where a bool indicates success and the result is only meaningful when the bool is true. In that case, out clearly communicates that the result is assigned only on success.
When you design a public API, think about the caller's experience. ref and out require the caller to write the keyword at the call site, which makes the data flow explicit. in does not require it, so the caller may not know that a copy is being avoided. That is fine as long as the method contract is clear from its name and documentation.
The final consideration is maintainability. Overusing ref can make code harder to follow because it hides side effects. Prefer returning values or using immutable types when possible. Use in only when profiling shows that copying a large struct is a bottleneck. Use out only when the pattern genuinely matches, such as parsing or deconstruction.