You've used out since Foundations without asking why the compiler trusts you. Here's precisely what it's checking.
You've written this line more times than you can count, going all the way back to early Foundations:
if (int.TryParse(input, out int number))
{
Console.WriteLine($"Parsed: {number}");
}
Notice something: you never assigned number before this line, and yet the compiler let you read it immediately afterward, inside the if block, with total confidence it holds a real value. That's not luck, and it's not a special case for TryParse — it's a precise, general, compiler-enforced rule about what out parameters guarantee. Advanced Part I's value-vs-reference-types lesson also handed you a working preview of in parameters and promised the full mechanics later, in this Part.
In this lesson, you'll get the precise rules behind out, ref, and in — three ways of passing a parameter that go beyond ordinary pass-by-value — plus a brief look at ref returns and ref locals, and honest guidance on how often any of this actually matters in everyday code.
By default in C#, arguments are passed by value — the method receives a copy, and changes to that copy don't affect the caller's original variable. ref, in, and out are three modifiers that change this default, each in a different, precise way:
out — "I'll give you back a value through this parameter; you don't need to give me one first."ref — "Let me read and write your actual variable, not a copy of it."in — "Let me read your variable without copying it, but I promise not to change it."All three modifiers change a parameter from ordinary pass-by-value into a form of pass-by-reference — the parameter becomes an alias for the caller's actual storage location, rather than an independent copy. What differs between them is the compiler-enforced contract layered on top of that reference:
out — the caller need not initialize the variable before the call; the callee must assign it on every code path before the method returns ("definite assignment").ref — the caller must initialize the variable before the call; the callee may both read and write it freely, with no obligation either way.in — the caller must initialize the variable before the call; the callee may read it but the compiler forbids writing to it.Ordinary pass-by-value handles the overwhelming majority of method calls perfectly well — but it genuinely cannot express a few specific, common needs:
return one thing. TryParse-style methods need to return both "did it succeed?" (the return value) and "what's the parsed result?" — a second output channel is needed.Swap(ref int a, ref int b) utility, or an in-place mutation of a caller-owned value, needs the callee to reach back and change the caller's own storage — not just its own private copy.ref would let the method accidentally mutate the caller's data too, which isn't what you want for a read-only need.out, ref, and in each solve exactly one of these needs, with the compiler enforcing the safety contract that makes the tool trustworthy: out gives you a second, guaranteed-assigned output channel; ref gives you genuine two-way pass-by-reference; in gives you the copy-avoidance of ref with the safety of ordinary pass-by-value, for the read-only case.
bool ok = int.TryParse("42", out int number); // 'number' declared, uninitialized, right here
public bool TryDivide(int a, int b, out int result)
{
if (b == 0)
{
result = 0; // compile error without this line — this path doesn't assign 'result'
return false;
}
result = a / b; // this path assigns it too
return true;
}
if (TryDivide(10, 2, out int quotient))
Console.WriteLine(quotient); // guaranteed assigned, by the compiler's own rule
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"{x}, {y}"); // 2, 1 — the CALLER's actual variables changed
What's happening: a and b inside Swap aren't copies — they're aliases for x and y themselves. Assigning to a inside the method is, physically, assigning to x. This is why the caller must explicitly write ref at the call site too (Swap(ref x, ref y), not Swap(x, y)) — C# deliberately makes this visible at every call, so nobody reading the caller's code is surprised that their variables might change.
public readonly struct Matrix4x4 // 16 floats — 64 bytes, as in Advanced Part I's preview
{
public readonly float M11, M12, M13, M14; // ...and 12 more fields
public float Determinant() => /* reads fields, computes a value */ 0f;
}
float ComputeDeterminant(in Matrix4x4 m)
{
// m.M11 = 0; // compile error: cannot assign to a member of 'm' because it is an 'in' parameter
return m.Determinant(); // reading is completely fine
}
What's happening: the method receives a reference to the caller's actual 64-byte struct — no copy — but the compiler statically rejects any attempt to write through m, whether directly or by calling a member that would mutate it. You get ref's performance (no copy) with plain pass-by-value's safety guarantee (the caller's data can't change).
public readonly struct Point(double X, double Y)
{
public double X { get; } = X;
public double Y { get; } = Y;
}
// out — produce a brand-new value the caller didn't have to provide
bool TryCreatePoint(double x, double y, out Point point)
{
if (double.IsNaN(x) || double.IsNaN(y))
{
point = default;
return false;
}
point = new Point(x, y);
return true;
}
// ref — mutate the caller's own variable in place
void Translate(ref Point point, double dx, double dy)
{
point = new Point(point.X + dx, point.Y + dy); // reassigns the CALLER's variable
}
// in — read a (potentially large) struct without copying it, and without risk of mutation
double DistanceFromOrigin(in Point point)
{
return Math.Sqrt(point.X * point.X + point.Y * point.Y); // read-only access, no copy
}
Three different jobs, three different modifiers: TryCreatePoint hands back a value the caller never had; Translate changes a value the caller already owns; DistanceFromOrigin only needs to look, so it borrows the caller's data instead of copying it.
A financial reporting service processes an array of transaction totals, each represented as a sizable readonly struct Money (currency code plus decimal amount), and needs to compute both a total and a flag for whether any transaction looked suspicious — without incurring a struct copy for every comparison in a large batch:
public readonly struct Money(string currency, decimal amount)
{
public string Currency { get; } = currency;
public decimal Amount { get; } = amount;
}
// 'in' — read each Money value without copying it, for every element in a potentially large batch
bool IsSuspicious(in Money transaction) => transaction.Amount > 10_000m;
// 'out' — report two independent results from one pass, without allocating a tuple or a result object
void Summarize(Money[] transactions, out decimal total, out bool anySuspicious)
{
total = 0m;
anySuspicious = false;
foreach (Money transaction in transactions)
{
total += transaction.Amount;
if (IsSuspicious(transaction)) // implicitly passed by 'in' at the call site — no copy needed
anySuspicious = true;
}
}
Summarize(dailyTransactions, out decimal dayTotal, out bool flagged);
Console.WriteLine($"Total: {dayTotal:C}, Flagged: {flagged}");
in avoids copying Money on every single call to IsSuspicious across a potentially large array — meaningful at volume, exactly the kind of struct-copy cost Advanced Part I's value types lesson flagged. out lets Summarize report two genuinely independent results from a single pass over the data, with the compiler guaranteeing both are actually set before the method returns — no risk of the caller reading an uninitialized "did we find anything suspicious?" flag.
Ordinary pass-by-value is handing someone a photocopy of a letter — they can read it, scribble on their copy, even burn it, and your original letter is completely unaffected. ref is handing someone the actual key to your mailbox: they can take mail out, put new mail in, and every change is real, visible in your mailbox the moment you check it again. in is letting someone look through the glass window of a mailbox they can't open — they can read what's inside without ever touching it, and you never had to photocopy the letter just so they could look. out is handing someone an empty mailbox with an explicit promise: "you must put something in here before you leave" — and the mail carrier (the compiler) genuinely won't let them walk away until they have.
ref, in, and out parameters are all implemented essentially the same way: the caller passes the address of its variable, and the callee receives that address rather than a byte-for-byte copy of the value stored there.out, unrestricted read/write for ref, and read-only enforcement for in — not a difference in the underlying calling convention itself.in genuinely avoids the copy cost of passing a large struct by value: the callee is working through the caller's own memory the whole time, never touching a duplicate.public class Grid
{
private readonly int[] _cells = new int[100];
// Returns a REFERENCE to the actual array element, not a copy of its value
public ref int At(int index) => ref _cells[index];
}
var grid = new Grid();
ref int cell = ref grid.At(5); // 'cell' is an alias for _cells[5] itself
cell = 42; // this writes directly into _cells[5]
Console.WriteLine(grid.At(5)); // 42
ref T — a reference to existing storage, not a copy of a value — and a local variable can be declared ref T to alias that same storage.Grid.At above) allow the caller to mutate an element directly, in place, without a separate SetAt(index, value) method — the same capability array indexing (array[i] = value) already gives you for free, generalized to custom types.They share the underlying reference-passing mechanism, but their contracts point in different directions. ref requires the caller to already have a meaningful value (the callee may or may not use it) and places no assignment obligation on the callee. out requires nothing from the caller but places a strict assignment obligation on the callee. Using ref where you mean out forces callers to needlessly initialize a variable before a call that was always going to overwrite it; using out where you mean ref throws away any initial value the caller had, since out parameters are treated as definitely-unassigned on entry.
Not automatically. For a small struct — a couple of int fields, say — the cost of passing by value is already tiny, and passing by reference instead can sometimes cost more in specific patterns, because the compiler may need to generate a defensive copy anyway when calling a non-readonly member through an in reference (the same defensive-copy mechanism Advanced Part I's readonly struct lesson covered). in earns its keep specifically for larger structs, and pairs best with a readonly struct, which eliminates the defensive-copy concern entirely.
The end result feels similar — a caller walks away with more than one piece of data — but the mechanism is genuinely different: an out parameter is the compiler proving, at compile time, that a specific variable gets written through a reference before the method returns. Tuple returns ((bool, int) TryDivide(...)) are a different, more modern C# idiom for a similar goal, and are often the more natural choice today — out remains common chiefly because of the huge amount of existing framework API (like TryParse) already built around it.
Writing a method with multiple branches and forgetting to assign the out parameter on one of them, expecting the compiler to somehow let it slide.
public bool TryGetValue(string key, out string value)
{
if (_data.ContainsKey(key))
{
value = _data[key];
return true;
}
// compile error: 'value' is not definitely assigned on this path
return false;
}
Assign value = null; (or an appropriate default) on every remaining path before returning — the compiler is doing you a favor by refusing to compile this, since the alternative would be silently handing callers an unassigned variable.
Using ref for a parameter the method only ever overwrites, never reads — forcing every caller to pre-initialize a variable pointlessly.
void Compute(ref int result) => result = 42; // never reads 'result' — 'out' says this intent honestly
int x = 0; // pointless initialization, forced by using 'ref' instead of 'out'
Compute(ref x);
If the method never reads the parameter's incoming value, that's the signal it should be out, not ref — it documents the real contract and removes the caller's unnecessary initialization.
Sprinkling in across parameters of type int, bool, or small structs, or across reference-type parameters, expecting a meaningful speedup.
For a reference type, the "copy" being passed by value is already just a small pointer — in/ref add no benefit there and only add visual noise. For small value types, the copy cost is already negligible. Reserve in for parameters that are genuinely large structs (as a rough guide, meaningfully larger than a machine word or two) on paths that are actually called often enough to matter.
out — a method needs to hand back one or more values the caller didn't have, alongside its main return value (the TryXxx pattern)ref — a method genuinely needs to both read and mutate the caller's own variable in place (a true swap, an in-place transform)in — a parameter is a sizable struct (ideally a readonly struct), on a path called often enough that the copy cost is real and measuredint, a bool, a two-field struct) — the copy cost was never meaningful to begin without when the framework convention (TryXxx) calls for it, and reach for ref/in deliberately, on measured hot paths involving large structs — not as a reflexive habit.
out — caller need not initialize; callee must assign on every path before returning ("definite assignment"), enforced by the compiler — the exact rule behind every TryParse you've ever written.ref — true pass-by-reference: the callee can read and write the caller's actual variable, with changes visible to the caller immediately after the call.in — pass a (typically large) struct by reference to avoid the copy cost, while the compiler prevents the callee from mutating it — ref's performance with pass-by-value's safety.ref returns and ref locals let a method hand back a genuine reference to existing storage, not a copy — a niche, advanced tool for indexer-style, high-performance APIs.You've used out for a long time and just met the precise rule behind it, alongside ref and in. Let's confirm the distinctions are solid.
1. Why does int.TryParse(input, out int number) let you safely read number immediately after the call, even though you never assigned it beforehand?
Correct: B
Why B is correct: This is the precise mechanism the lesson names: the compiler performs static analysis on the body of any method with an out parameter, confirming that every code path assigns it before the method returns. That compile-time guarantee is exactly what makes reading it immediately after the call safe.
Why A is incorrect: There's no automatic zero-initialization for out variables from the caller's side — the guarantee comes entirely from the callee being required to assign it.
Why C is incorrect: Uninitialized local variables of any type, including int, are not safe to read in C# in general — the definite-assignment rule for out parameters is precisely what makes this particular case safe.
Why D is incorrect: TryParse is an ordinary method following the same out parameter rules any developer-written method with an out parameter must follow — no special compiler exemption is involved.
Reinforcement: Definite assignment isn't a TryParse-specific trick — it's the general rule behind every out parameter in C#.
2. A method signature is void Increment(ref int counter). What must be true about the argument at the call site?
Correct: B
Why B is correct: Unlike out, a ref parameter requires the caller to have already assigned a value (since the callee is free to read it, not just write it), and C# requires the ref keyword at the call site too, so the possibility of the callee mutating the caller's variable is visible wherever the call appears.
Why A is incorrect: That's the out rule specifically — ref requires prior initialization precisely because the callee may read the value before writing anything.
Why C is incorrect: ref parameters exist specifically to reference actual variables so the callee can modify them — a constant, which can't be modified, wouldn't satisfy that purpose and isn't accepted as a ref argument.
Why D is incorrect: A ref argument can be a local variable, a field, an array element, or a property with both a getter and setter (via some additional rules) — it's not restricted to newly-declared locals.
Reinforcement: The caller-side ref keyword requirement is a deliberate visibility choice — it makes "this call might change my variable" impossible to miss when reading the calling code.
3. Why does passing a large readonly struct with in avoid the performance cost that plain pass-by-value would incur, while still being safe?
Correct: B
Why B is correct: in uses the same underlying pass-by-reference mechanism as ref — passing an address, not a copy — which eliminates the field-by-field copy cost. Layered on top, the compiler enforces that the callee cannot write through that reference, which is what keeps it just as safe as ordinary pass-by-value from the caller's perspective.
Why A is incorrect: The struct remains a value type throughout — in changes how it's passed to this specific method, not its fundamental type category.
Why C is incorrect: There's no compression involved — the savings come from not copying the data at all, not from copying a smaller version of it.
Why D is incorrect: in works with structs of any size — it's specifically larger structs where the copy-avoidance benefit becomes meaningful; there's no such size restriction on where in can be applied.
Reinforcement: in's value proposition is precisely "ref's performance, pass-by-value's safety" — both halves of that sentence matter equally.
4. A developer writes void Fill(ref string[] items) for a method that only ever assigns a brand-new array to items and never reads the array the caller originally passed in. What's the more accurate parameter modifier, and why?
Correct: B
Why B is correct: This is exactly the "reaching for ref when the intent is out" mistake covered in the lesson — since the method never reads the parameter's incoming value, out more precisely documents the real contract and removes any pointless obligation for the caller to initialize the variable first.
Why A is incorrect: While ref would technically compile and work, it's a less accurate signal of intent than out here, and unnecessarily forces callers to provide (and often waste) an initial value.
Why C is incorrect: in is for read-only access to the caller's data — the exact opposite of what this method does, which is write a new array without ever reading the old one.
Why D is incorrect: Plain pass-by-value passes a copy of the array reference — reassigning that local copy inside the method (items = newArray;) would not change what the caller's own variable points to. A modifier (here, out or ref) is genuinely required to reassign the caller's own variable.
Reinforcement: Choosing between ref and out comes down to one question: does the method ever need to read the parameter's incoming value? If no, out is the more honest, more caller-friendly choice.
5. What is the main reason ref returns and ref locals are described in this lesson as a niche, advanced feature rather than an everyday tool?
Correct: C
Why C is correct: ref returns/locals solve a specific, comparatively rare problem — exposing a direct, mutable reference to existing storage from a method, useful mainly in high-performance, indexer-style library code. Ordinary application code almost always has its needs met by regular methods, auto-properties, and standard indexers, which is exactly why the lesson flags this as niche rather than something to reach for regularly.
Why A is incorrect: They're a fully supported, standard part of the C# language — not deprecated or on any removal path.
Why B is incorrect: They work with any type T, exactly like ref/in/out parameters — the Grid.At example in Under the Hood used int, not string.
Why D is incorrect: ref returns and locals are ordinary, safe C# — no unsafe block or manual pointer arithmetic is required to use them.
Reinforcement: "Niche" here means narrowly-applicable, not deprecated or unsafe — it's a real, useful tool for the specific, comparatively rare scenario it targets.
You now have the precise rules behind out, ref, and in — the exact tool for each job, and honest judgment about when reaching for any of them is actually worth it. This closes out the vocabulary this Part needed — next, you'll see it all put to work together.
dotnetmadeeasy.com — Learn C# and .NET, the right way.