Ref, Out and In — Passing by Reference

By default, method parameters are passed by value — the method receives a copy of the value (for value types) or a copy of the reference (for reference types). C# provides three parameter modifiers that change this behaviour: ref passes the variable itself by reference, out requires the method to assign the variable before returning, and in passes a read-only reference to avoid copying large structs. These modifiers appear in TryParse patterns, in high-performance code, and occasionally in utility methods — but they should be used sparingly, as returning a tuple is often cleaner.

Ref — Read and Write the Caller’s Variable

// ref — the method can READ and WRITE the caller's original variable
public void Increment(ref int counter)
{
    counter++;   // modifies the caller's variable
}

int score = 10;
Increment(ref score);    // caller must use the ref keyword too
Console.WriteLine(score);   // 11

// Classic use: swap two values
public static void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

int x = 5, y = 10;
Swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}");   // x=10, y=5
Note: Both the caller AND the method declaration must use the ref keyword — the explicit keyword at both sites is intentional, making it clear to anyone reading either the call site or the method signature that pass-by-reference is happening. Variables passed as ref must be initialised before the call — the method may or may not overwrite the value, so the compiler requires it to be assigned first. This differs from out where initialisation before the call is not required.
Tip: In modern C#, you rarely need ref for most application code. Returning a tuple (T newA, T newB) = Swap(a, b) is cleaner than ref parameters in most cases. ref is most valuable in high-performance scenarios (avoiding struct copies in hot paths, modifying array elements via ref locals) and in library code. For typical ASP.NET Core service and controller code, avoid ref parameters — they complicate the call site and make async methods impossible (you cannot use ref/out in async methods).
Warning: ref and out parameters cannot be used in async methods — this is a compiler restriction. Since async methods are used extensively in ASP.NET Core (all database calls are async), you should avoid designing service layer APIs that use ref/out. Use return values and tuples instead. If you find yourself wanting ref in an async context, that is a strong signal to redesign the method to return a value rather than modify a parameter.

Out — The Method Must Assign It

// out — method MUST assign before returning; caller does NOT need to initialise
public bool TryDivide(int dividend, int divisor, out double result)
{
    if (divisor == 0)
    {
        result = 0;     // must assign even when returning false
        return false;
    }
    result = (double)dividend / divisor;
    return true;
}

// Inline declaration (C# 7+)
if (TryDivide(10, 2, out double quotient))
    Console.WriteLine($"Result: {quotient}");   // Result: 5

// Discard the out parameter when you don't need it
if (int.TryParse("42", out _))   // _ discards the parsed value
    Console.WriteLine("Valid integer");

// The Try* pattern throughout .NET BCL
int.TryParse("123",                out int n);
double.TryParse("3.14",            out double d);
DateTime.TryParse("2025-06-15",    out DateTime dt);
Guid.TryParse("abc-123",           out Guid g);

In — Read-Only Reference (Performance)

// in — passes a read-only reference; method cannot modify the variable
// Used to avoid copying large value types (structs) for performance
public static double CalculateDistance(in Vector3D a, in Vector3D b)
{
    double dx = a.X - b.X;
    double dy = a.Y - b.Y;
    double dz = a.Z - b.Z;
    return Math.Sqrt(dx*dx + dy*dy + dz*dz);
    // a and b cannot be reassigned inside this method
}

readonly struct Vector3D(double X, double Y, double Z)
{
    public double X { get; } = X;
    public double Y { get; } = Y;
    public double Z { get; } = Z;
}

var p1 = new Vector3D(0, 0, 0);
var p2 = new Vector3D(3, 4, 0);
double dist = CalculateDistance(in p1, in p2);  // 5.0 (3-4-5 triangle)

Comparison of Parameter Modifiers

Modifier Caller Must Init Method Can Read Method Can Write Use For
(none) Yes Yes Value type: copy only · Ref type: shared ref Default — most methods
ref Yes Yes Yes (the caller’s variable) Swap, high-perf mutation
out No No (until assigned) Yes — MUST assign Try* pattern, multiple returns
in Yes Yes No (read-only ref) Large struct, perf-critical

Common Mistakes

Mistake 1 — Using ref/out in async methods (compile error)

❌ Wrong — cannot use ref or out parameters in async methods:

public async Task ProcessAsync(ref int count)   // compile error!

✅ Correct — return a tuple from async methods instead of using out/ref.

Mistake 2 — Forgetting to assign out parameter in all code paths

❌ Wrong — compile error: out parameter not assigned if divisor != 0 branch not reached:

public bool TryDivide(int a, int b, out double result)
{
    if (b == 0) return false;  // forgot to assign result before returning!
    result = (double)a / b;
    return true;
}

✅ Correct — assign out parameter in every code path before returning:

if (b == 0) { result = 0; return false; }   // ✓ assigned before return

🧠 Test Yourself

Why can you pass an uninitialised variable as an out argument but not as a ref argument?