Back to Blog
C#

C# in Parameter: Read-Only References for Structs

c# in parameter: Learn how the C# in parameter passes large structs by read-only reference, avoiding copies while keeping immutability.

C#structsmethod parametersread-onlyperformance
Illustration of C# in parameter passing a struct by reference without copying

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

The in parameter modifier in C# lets you pass an argument by reference while guaranteeing the method cannot modify it. It exists primarily to avoid copying large structs when you only need read access. Without in, passing a struct by value copies the entire structure, which can be expensive for types like Vector3, Decimal, or custom data-heavy structs. With in, the caller passes a reference, and the compiler enforces read-only semantics inside the method.

What the in Parameter Actually Does

When you declare a method parameter with in, the argument is passed by reference, but the method receives it as a read-only reference. This means the method can read the value but cannot assign to it, cannot pass it to another method as a ref or out parameter, and cannot call mutating members on it. The syntax is straightforward:

public static double Magnitude(in Vector3 v) { return Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z); }

At the call site, you can pass a variable directly. The in keyword is optional at the call site, but you can include it for clarity:

Vector3 point = new Vector3(3, 4, 5); double mag = Magnitude(in point);

The compiler treats the parameter as a readonly reference. Attempting to assign to v inside the method produces a compile-time error. This is not a runtime check; it is enforced by the type system.

Why Avoid Copying Structs

Structs are value types. When you pass a struct by value to a method, the runtime copies the entire struct onto the stack. For small structs like int or bool, this is trivial. But for larger structs, the copy cost can become significant, especially in hot paths or when the method is called frequently in loops. The in parameter eliminates that copy by passing a reference to the original variable.

Consider a struct that holds many fields:

public struct LargeData { public long Id; public double Value1; public double Value2; public double Value3; public string Name; // ... more fields }

Passing this by value copies all those fields. Using in passes a reference, so the method reads directly from the caller's variable. This is especially beneficial when the struct is larger than a pointer (typically 8 bytes on 64-bit systems).

Rules and Restrictions of in Parameters

The in modifier comes with strict rules that prevent accidental mutation:

  • You cannot assign to an in parameter inside the method.
  • You cannot pass an in parameter as a ref or out argument to another method.
  • You cannot use the in parameter as the receiver of a mutating method call (e.g., a method that modifies the struct's fields).
  • You can call read-only members and properties.
  • You can pass an in parameter to another method that also uses in.

These rules are enforced at compile time. The compiler also prevents you from returning an in parameter by reference unless you use ref readonly (which is a related but distinct feature).

Overload Resolution and in Parameters

Adding an in parameter changes overload resolution. The compiler prefers a by-value parameter over an in parameter if both are applicable, because passing by value is the default and less restrictive. This means that adding an in overload can change which method is called in existing code, potentially silently.

For example:

public static void Process(Vector3 v) { } public static void Process(in Vector3 v) { }

Calling Process(someVector) will choose the by-value version. To force the in version, you must use the in keyword at the call site. This behavior is by design to avoid breaking existing code, but it means you should be careful when adding in overloads to public APIs.

Another subtlety: if you have a method with in and you pass a literal or a computed expression, the compiler may create a temporary variable and pass a reference to it. This is allowed but can negate the performance benefit because the temporary still exists.

Performance: When in Helps and When It Doesn't

The performance benefit of in comes from avoiding a copy. For large structs, this can reduce memory traffic and CPU time. However, in is not a free win. Passing by reference introduces indirection, which can be slower for small structs because the JIT might have to dereference the pointer to read the value. For structs smaller than a pointer, passing by value is often faster because the value fits in a register or a single cache line.

There is also a risk of defensive copies. If you access a field of an in parameter and the compiler cannot prove that the field access is safe, it may create a local copy. This can happen when you call a method on the struct that is not marked readonly. To avoid this, mark the struct itself as readonly or ensure all its methods are readonly. The compiler then avoids defensive copies.

public readonly struct Vector3 { public double X { get; } public double Y { get; } public double Z { get; } // constructor }

Using a readonly struct with in parameters is the most efficient combination because it eliminates the need for defensive copies.

Common Pitfalls and How to Avoid Them

One common mistake is assuming in makes the argument immutable from the caller's perspective. It does not. The caller can still modify the variable after the method returns. in only prevents the method from modifying it during the call.

Another pitfall is using in with properties or expressions. If you pass a property like myObject.Vector, the compiler evaluates the property once and stores it in a temporary variable, then passes a reference to that temporary. This means the method sees a snapshot, not the live property. This can lead to subtle bugs if you expect the method to see changes made to the property during the call (which is impossible anyway, but the snapshot behavior is worth noting).

Finally, be cautious about using in with small structs. The overhead of passing a reference can outweigh the copy cost. Measure your specific scenario. In general, use in for structs that are larger than a pointer and where you need read-only access. For smaller types, passing by value is usually simpler and faster.

Alternatives and Related Features

The in parameter is part of a family of reference-passing modifiers in C#. ref passes by reference with write access, out passes by reference and requires the method to assign a value, and in passes by reference with read-only access. There is also ref readonly, which is used for return values to return a read-only reference to a field or array element.

If you find yourself frequently using in with structs, consider marking the struct as readonly to improve safety and performance. This tells the compiler that no member modifies the struct, so it can avoid defensive copies when accessed through in parameters. The combination of readonly struct and in is the recommended pattern for large, immutable value types.

For methods that need to modify a struct, use ref instead. For methods that need to initialize a struct, use out. The in modifier is specifically for read-only access without copying, and it should be used only when that semantic matches your intent.

c# in parameter: Practical Usage and Code Examples | RYUSLOG DEV