Back to Blog
C#

C# in vs ref vs out: Parameter Passing Explained

c# in vs ref vs out: Learn how C# in, ref, and out parameter modifiers differ, when to use each, and how they affect method signatures and runtime behavior.

C#parameter modifiersrefoutinmethod parameters
Diagram comparing C# in, ref, and out parameter modifiers with arrows showing data flow.

c# in vs ref vs out requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you pass an argument to a method in C#, the default behavior copies the value. For value types, that means a copy of the data; for reference types, a copy of the reference. But sometimes you need the method to modify the caller's variable, or you want to avoid copying a large struct. C# provides three parameter modifiers—in, ref, and out—to control this behavior. Understanding the differences between them is essential for writing correct and efficient code.

The Default Behavior: Passing by Value

Before diving into the modifiers, it helps to recall what happens without them. Consider this method:

public static void Increment(int number) { number++; } int value = 5; Increment(value); Console.WriteLine(value); // 5

The value variable remains 5 because the method receives a copy of the integer. For a reference type, the reference itself is copied, so the method can modify the object's contents but cannot reassign the caller's variable to a new object. This default behavior is safe and predictable, but it becomes limiting when you need the method to produce a new value or when copying a large struct is expensive.

The out Modifier: Guaranteed Assignment

The out modifier is designed for methods that must produce one or more values. The caller does not need to initialize the variable before passing it, but the method must assign a value to every out parameter before returning. This is a compile-time guarantee.

public static bool TryParseInt(string input, out int result) { if (int.TryParse(input, out result)) { return true; } result = 0; // Must assign even on failure return false; } if (TryParseInt("42", out int number)) { Console.WriteLine(number); // 42 }

Notice that result is assigned in both branches. The compiler enforces this, which prevents using an uninitialized variable. The out modifier is commonly used in the TryParse pattern and in methods that return multiple values without creating a tuple or a custom type.

The ref Modifier: Read-Write Access

The ref modifier gives the method direct access to the caller's variable. The variable must be initialized before the call, and the method can both read and modify it. Unlike out, the method is not required to assign a new value, but it may do so.

public static void Swap(ref int a, ref int b) { int temp = a; a = b; b = temp; } int x = 1, y = 2; Swap(ref x, ref y); Console.WriteLine($"{x} {y}"); // 2 1

Here, the method modifies the original variables. The ref modifier is useful when you want to avoid copying a large struct or when you need to replace the object a reference type variable points to.

The in Modifier: Read-Only Reference

Introduced in C# 7.2, the in modifier passes a read-only reference to the argument. The method can read the value but cannot modify it. The caller does not need to use in explicitly at the call site; the compiler allows omitting it. However, the parameter itself is always passed by reference, which avoids copying for large structs.

public static void Display(in Vector3 vector) { Console.WriteLine($"({vector.X}, {vector.Y}, {vector.Z})"); } var position = new Vector3(1, 2, 3); Display(in position); // 'in' is optional here Display(position); // also valid

The in modifier is a performance optimization for read-only access to large value types. It is not allowed for reference types because passing a reference type by value already passes a reference, so in would be redundant and potentially confusing.

Comparing in, ref, and out in Practice

The following table summarizes the key differences:

ModifierCaller must initialize?Method can read?Method can write?Call site syntaxTypical use
inYesYesNoin optionalRead-only access to large structs
refYesYesYesref requiredModify the caller's variable
outNoNo (must assign)Yes (must assign)out requiredReturn multiple values

Note that out parameters cannot be read before they are assigned, and the method must assign them before every return path. This makes out a strict contract for output values.

Performance and Large Structs

For value types, passing by value copies the entire struct. If the struct is large (e.g., a 64-byte matrix), that copy happens on every call. Using in or ref avoids the copy by passing a reference to the original variable. The in modifier is ideal when the method only needs to read the struct, while ref is necessary if the method must modify it.

public readonly struct Matrix4x4 { public float M11, M12, M13, M14; // ... other fields } public static float Determinant(in Matrix4x4 matrix) { // Read-only access, no copy return matrix.M11 * matrix.M22; // simplified }

Using in here avoids copying the entire matrix on each call. However, be aware that the compiler may still create a defensive copy if the struct has a property or method that modifies it, or if the struct is not declared as readonly. To get the full benefit, define the struct as readonly or ensure the method does not trigger a defensive copy.

Common Pitfalls and How to Avoid Them

One frequent mistake is confusing ref and out when calling a method. The call site syntax must match the declaration exactly. For out, you can use out var to declare the variable inline, but for ref, you must have an existing variable.

Another pitfall is using ref on a reference type when you only need to modify the object's contents. For example:

public static void AddItem(List<int> list, int item) { list.Add(item); // No need for ref }

Passing the list by value is sufficient because the reference is copied, but the object is shared. Using ref would allow reassigning the list variable to a new list, which is rarely needed and can lead to subtle bugs.

When using in, remember that the method cannot assign to the parameter. If you attempt to modify it, the compiler will raise an error. Also, for value types that are not readonly, the compiler may create a defensive copy before calling a non-readonly method, negating the performance benefit. Declare the struct as readonly whenever possible.

Choosing the Right Modifier for Your Method

Use out when the method's primary purpose is to return a value that cannot be expressed as a simple return type, such as in the TryParse pattern. Use ref when the method must modify the caller's variable, either to replace it or to swap values. Use in when you have a large value type that should be passed read-only to avoid copying.

A practical decision rule: if the method needs to write to the parameter, use out if the caller does not need to provide an initial value, otherwise use ref. If the method only reads the parameter and the type is a large struct, use in. For small value types or reference types, stick with the default by-value passing to keep the API simple.

When you design a public API, prefer out and ref only when necessary, as they add complexity at the call site. The in modifier is less intrusive because the caller can omit it, but it still signals that the argument is passed by reference, which may affect overload resolution and API readability.

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