Back to Blog
C#

C# ref vs in vs out: Choosing the Right Parameter Modifier

Understand the differences between C# ref, in, and out parameter modifiers, when to use each, and how they affect performance and code clarity.

C#refinoutparameter modifiersmethod parameters
Comparison of C# ref, in, and out parameter modifiers with arrows showing data flow.

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 a copy of the reference itself, so the method can mutate the object but cannot reassign the caller's variable. For value types, the entire struct is copied. The ref, in, and out modifiers change that behavior by passing a reference to the original storage location, but each modifier creates a different contract. Understanding those contracts is essential for designing APIs that are both efficient and clear about how they treat inputs and outputs.

What All Three Modifiers Have in Common

All three modifiers pass a reference to the caller's storage location rather than a copy. In the normal case, no copy of the argument is made, which can matter for large structs. They also affect how the method can read and write that variable. The compiler enforces different rules for each modifier, and those rules also affect overload resolution.

ModifierCaller initializes?Method can read?Method can write?Typical use
refYesYesYesIn-place modification
inYesYesNoRead-only large structs
outNot requiredOnly after assignmentMust assignReturning 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 method declaration uses ref, and the call site must use ref too. 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 do not intend to modify it. The modifier was introduced in C# 7.2 mainly 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 prevents direct assignment to an in parameter. For a readonly struct, calling its instance methods does not cause a defensive copy. For a non-readonly struct, calling a method that is not marked readonly can force the compiler to make a defensive copy, which may reduce the performance benefit of using in. The caller does not need to use the in keyword at the call site, though including it can make the intent clearer.

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 also works with out var declarations, so callers do not need to declare the variable separately:

if (int.TryParse(input, out var number)) { Console.WriteLine(number); }

Overload Resolution and Modifier Compatibility

Parameter modifiers affect overload resolution. A by-value parameter and a ref parameter of the same type can be separate overloads:

public void Process(int value) { } public void Process(ref int value) { }

Calling Process(someInt) selects the by-value overload; calling Process(ref someInt) selects the ref overload.

This distinction has limits. C# does not allow two overloads that differ only by ref and out. For example, declaring both Process(ref int value) and Process(out int value) is a compile-time error. The same kind of restriction applies to in as well. Choose modifiers carefully because they are part of the public API surface.

Performance Considerations: When Passing by Reference Actually Helps

The main performance benefit of ref and in is avoiding copies when the argument is a large value type. For small structs, copying is often cheaper than the indirection involved in a by-reference call. The JIT may also inline and optimize, but the general rule is: use in for large read-only structs, and use ref when you need to modify the caller's variable. out is primarily a contract for returning values, not a performance optimization.

Copying a large struct on every call can add up, especially in tight loops. Passing by reference avoids that copy. However, in does not guarantee that a copy is avoided in every case: for non-readonly structs, the compiler may introduce a defensive copy, and for small structs the indirection cost can exceed the copy cost. Treat in as a semantic choice first and a performance optimization second, and confirm the benefit with profiling when it matters.

Common Mistakes and Edge Cases

A frequent mistake is using ref when in would be more appropriate, or using out when a tuple would be clearer. 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. Returning a named tuple or a small result type is often cleaner.

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, be careful with in and non-readonly structs. If the struct is not marked readonly, calling a method that is not marked readonly can force the compiler to make a defensive copy. That can make in less useful in legacy code until the struct types are updated.

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 does not 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.

C# ref vs in vs out: Practical Usage and Code Examples | RYUSLOG DEV