in Parameters · How it works

6 min read
Mid-level8 min read
Rapid overview

How it works

Basics

Q: What does the 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
ModifierWhat is passedCallee mayCaller mustTypical use
(none)a copy of the valuemodify its copypass any expressiondefault
refreference to caller's variableread and writepass an initialized variable, write refswap, mutate in place
outreference to caller's variablemust assign before returningpass a variable (may be uninitialized), write outTryParse, multiple outputs
inread-only referenceread onlypass anything; in optional; a temporary is created for non-variablesavoid copying large readonly structs
ref readonly (C# 12)read-only referenceread onlypass a variable; warning if you pass a valueAPIs where the reference's identity matters
Q: How does in compare with ref, out and by-value?

A:

Q: Can you pass a constant or an expression to an 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

Q: What does 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.

Q: What is a defensive copy, and why does it matter for 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.

Q: What is aliasing, and how can an 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

Q: Where can 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.

Q: Can you overload methods on 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.

Q: What is the 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

Q: When should you use 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.

Q: How do 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

Q: Is 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.

Q: Do you need to write 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.

Q: Does 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.

Q: What does 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.