in Parameters · How it works
6 min readHow it works
Basics
in modifier on a parameter mean?A: The argument is passed by reference, but read-only. The method sees the caller's actual variable, not a copy, and any attempt to assign to it or to its fields is a compile error.
public readonly struct Matrix4 { /* 16 floats = 64 bytes */ public float M11 { get; init; } /* ... */ }
static float Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44; // reads through a reference
var m = new Matrix4 { /* ... */ };
float t = Trace(in m); // 'in' at the call site is optional: Trace(m) also passes by reference| Modifier | What is passed | Callee may | Caller must | Typical use |
|---|---|---|---|---|
| (none) | a copy of the value | modify its copy | pass any expression | default |
ref | reference to caller's variable | read and write | pass an initialized variable, write ref | swap, mutate in place |
out | reference to caller's variable | must assign before returning | pass a variable (may be uninitialized), write out | TryParse, multiple outputs |
in | read-only reference | read only | pass anything; in optional; a temporary is created for non-variables | avoid copying large readonly structs |
ref readonly (C# 12) | read-only reference | read only | pass a variable; warning if you pass a value | APIs where the reference's identity matters |
in compare with ref, out and by-value?A:
in parameter?A: Yes. If the argument is not a variable (Trace(CreateMatrix()), a literal, a property value), the compiler stores it in a hidden temporary and passes a reference to that. This is what makes in easy to adopt without changing call sites. ref readonly parameters (C# 12) exist for cases where passing a temporary would be a bug, because the method relies on referring to the caller's real variable.
Memory level
in change at the machine level?A: With by-value, the caller copies the entire struct into the callee's argument area (stack or registers) on every call. With in, the caller passes only the address of its variable, and the callee reads fields through that pointer.
by value: Trace(Matrix4 m) in: Trace(in Matrix4 m)
caller frame callee frame caller frame callee frame
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ m: 64 bytes │──► │ m: 64 bytes │ │ m: 64 bytes │◄───│ &m: 8 bytes │
└──────────────┘copy└──────────────┘ └──────────────┘ref └──────────────┘
For a 64-byte struct called millions of times, that is a real saving in copying and stack traffic. For a struct of 16 bytes or less (or an int, double, Guid-sized value), by-value is often faster: small values travel in CPU registers, while in forces the value to live in memory and adds a pointer dereference on every access. Microsoft's guidance: consider in for *readonly structs larger than about IntPtr.Size 2** (16 bytes on 64-bit), and measure.
in?A: The compiler promised the caller that an in argument will not change. If the struct is not readonly, calling one of its methods or property getters could mutate it, so the compiler protects the promise by copying the struct into a temporary before every such call, and calling the member on the copy. That silently brings back the copy you were trying to avoid, once per member call.
public struct Vec3 { public double X, Y, Z; public double Length() => Math.Sqrt(X*X + Y*Y + Z*Z); }
static double Len(in Vec3 v) => v.Length(); // hidden copy of v before calling Length()
public readonly record struct Vec3R(double X, double Y, double Z)
{
public double Length() => Math.Sqrt(X*X + Y*Y + Z*Z);
}
static double LenR(in Vec3R v) => v.Length(); // no copy: the compiler knows Length() cannot mutate
Fixes: declare the struct readonly struct (or readonly record struct), or mark individual members readonly (C# 8), so the compiler can call them directly on the reference. Direct field reads never cause a defensive copy.
in value change during a call?A: in means the method cannot modify the value, not that nobody can. The reference points at the caller's variable, so if that variable is changed through another path during the call (the method mutates the same field through this, another ref parameter points to the same variable, or another thread writes it), the in parameter sees the new value.
class Scene
{
private Vec3R _origin = new(0, 0, 0);
double Distance(in Vec3R from)
{
var before = from.X;
_origin = new Vec3R(10, 0, 0); // 'from' may be a reference to _origin
return from.X - before; // 10 if the caller passed _origin, 0 otherwise
}
double Test() => Distance(in _origin);
}
A by-value parameter is a snapshot and cannot change this way. If you need a stable value, copy it into a local first.
Rules and restrictions
in parameters not be used?A: In async methods and iterators (their parameters are hoisted into a heap-allocated state machine, which cannot hold references to the caller's stack), in lambdas or local functions that capture them, and as params. The same rules apply to ref and out.
in versus by-value?A: Yes: void M(int x) and void M(in int x) can coexist. A call without the in keyword, M(value), binds to the by-value overload; M(in value) picks the in one. This is a rarely used and confusing API design; avoid it.
in modifier in other contexts?A: The same keyword means different things elsewhere: foreach (var x in items) is loop syntax, and interface IComparer<in T> marks a generic type parameter as contravariant. Neither is related to in parameters.
When to use in
in parameters?A: For large, readonly structs passed frequently on performance-sensitive paths: math and geometry types (matrices, quaternions), big value objects, and struct-based messages in high-throughput pipelines. Make the struct readonly first so there are no defensive copies, and confirm the gain with a benchmark. Do not use in for primitives, small structs (≤ 16 bytes), reference types (a class reference is already just a pointer, so in adds a second indirection and prevents nothing useful), or in ordinary business code where readability matters more.
in parameters relate to Span<T>?A: Both avoid copying data, in different ways: in passes one struct by reference; a span passes a view over many elements. APIs that process large buffers of structs usually take ReadOnlySpan<T> (which also prevents modification) instead of many in parameters, and span indexers return ref / ref readonly elements so you can read big structs in place without copying.
Common interview gotchas
in always faster than passing by value?A: No. It helps for large readonly structs and hurts for small ones or primitives, and on a non-readonly struct defensive copies can make it slower than by-value.
in at the call site?A: No, it is optional for in parameters (unlike ref and out). Writing it documents intent and requires the argument to be a variable.
in make a reference-type argument immutable?A: No. For a class, in only stops the method from reassigning the parameter to point at another object. It can still modify the object's fields and properties through the reference.
ref readonly add over in (C# 12)?A: A ref readonly parameter expects a variable: passing an rvalue produces a warning, and callers write ref or in at the call site. Use it for APIs that need to refer to the caller's actual storage (for example, returning or comparing references), where silently creating a temporary would hide a bug.