Understanding the in Parameter Modifier in C

In C#, the in keyword is a parameter modifier used to pass arguments by reference rather than by value, while explicitly enforcing read-only access within the called method. This article covers the fundamental role of the in modifier, how it differs from ref and out, its performance benefits when handling large value types, and key considerations like defensive copies to avoid unintended overhead.

What Is the in Modifier?

Introduced in C# 7.2, the in parameter modifier instructs the compiler to pass an argument by reference while preventing the called method from modifying its value. It combines the performance advantage of pass-by-reference with the safety of pass-by-value immutability.

void DisplayPoint(in GeoPoint point)
{
    // point.X = 10; // Compile-time error: Cannot assign to variable because it is a readonly variable
    Console.WriteLine($"Lat: {point.Latitude}, Long: {point.Longitude}");
}

Primary Roles and Benefits

1. Performance Optimization for Large Structs

When value types (such as struct) are passed to methods by default, the runtime creates a complete copy of the data on the stack. For small primitives (like int or bool), this copy is negligible. However, for large custom structs, copying consumes CPU cycles and increases stack memory usage.

Using in passes a reference (pointer) to the existing memory location instead of copying the struct, dramatically reducing overhead in performance-critical code paths.

2. Enforcing Immutability

Unlike the ref modifier, which allows the invoked method to alter the original data, an in parameter is strictly read-only. Any attempt to reassign the parameter or modify its fields causes a compile-time error. This clearly communicates the method's design intent: the data is strictly an input.

Key Rules and Usage Characteristics

  • Must Be Initialized: Unlike out parameters, any variable passed with the in modifier must be fully initialized before passing it to the method.

  • Optional Call-Site Modifier: At the call site, specifying in is usually optional. The compiler resolves the overload automatically unless ambiguity exists.

    GeoPoint p = new GeoPoint(45.0, 90.0);
    DisplayPoint(p);      // Valid
    DisplayPoint(in p);   // Also valid and explicit
  • Overloading Restrictions: You cannot overload methods where the only difference is between in, out, and ref. However, you can overload a method where one version takes a standard by-value parameter and the other uses in.

The "Defensive Copy" Pitfall

When passing a standard mutable struct using in, the C# compiler prioritizes immutability over performance. If you invoke a method or property on that struct, the compiler cannot guarantee that the member does not mutate internal state. To safeguard immutability, the compiler creates a temporary "defensive copy" of the struct before running the member.

This defensive copying can negate the performance benefits of passing by reference.

How to Prevent Defensive Copies

To avoid this issue, declare the struct as readonly struct:

public readonly struct GeoPoint
{
    public double Latitude { get; }
    public double Longitude { get; }

    public GeoPoint(double lat, double lon)
    {
        Latitude = lat;
        Longitude = lon;
    }
}

When a struct is marked as readonly, the compiler guarantees that no internal member can mutate state, completely eliminating the need for defensive copies when accessed via in.

Comparison: in vs. ref vs. out

Modifier Passed By Caller Must Initialize? Method Can Modify? Call-Site Keyword Required?
in Reference Yes No (Read-Only) Optional
ref Reference Yes Yes (Read/Write) Required
out Reference No Yes (Must Assign) Required

When to Use in

  • When passing value types larger than IntPtr.Size * 2 (typically larger than 16 bytes) in performance-sensitive or high-throughput scenarios.
  • When pairing the modifier with readonly struct definitions to ensure zero-overhead reference passing.
  • Avoid using in for reference types (classes) or basic primitives (int, float, enum), as passing a reference to a reference type or a small value type adds indirection without providing any performance advantage.