ref, out and in
Normally a method gets a copy of each argument. The ref, out and in modifiers pass the caller's variable itself instead. You'll use out all the time with TryParse-style methods; ref and in are more specialised but worth recognising.
ref: let the method change the caller's variable
With ref, the parameter is another name for the caller's variable. Both the method declaration and the call must say ref, so it's obvious at the call site that the variable may change:
int lives = 3; LoseLife(ref lives); LoseLife(ref lives); Console.WriteLine(lives); int a = 1, b = 2; Swap(ref a, ref b); Console.WriteLine($"a = {a}, b = {b}"); static void LoseLife(ref int count) => count--; static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; }
1 a = 2, b = 1
A ref argument must be a variable that already has a value — you can't pass a literal like ref 5.
out: return extra values
An out parameter is for output only. The caller doesn't need to initialise the variable, but the method must assign it before returning. This is the pattern behind int.TryParse: the bool return value says whether it worked, and the out parameter carries the result:
if (TryDivide(17, 5, out int quotient, out int remainder)) { Console.WriteLine($"17 / 5 = {quotient} remainder {remainder}"); } if (!TryDivide(1, 0, out _, out _)) { Console.WriteLine("Cannot divide by zero"); } static bool TryDivide(int a, int b, out int quotient, out int remainder) { if (b == 0) { quotient = 0; remainder = 0; return false; } quotient = a / b; remainder = a % b; return true; }
17 / 5 = 3 remainder 2 Cannot divide by zero
Two conveniences: out int quotient declares the variable right in the call, and out _ (a discard) ignores a value you don't need.
Forgetting to assign an out parameter is a compile error:
static bool TryFind(string key, out string value) { if (key == "a") value = "apple"; return key == "a"; }
Program.cs(4,5): error CS0177: The out parameter 'value' must be assigned to before control leaves the current method
The Try pattern in the library
Many .NET methods follow the same "Try" pattern:
var stock = new Dictionary<string, int> { ["apples"] = 12 }; if (stock.TryGetValue("apples", out int count)) Console.WriteLine($"apples: {count}"); if (!stock.TryGetValue("pears", out _)) Console.WriteLine("no pears"); if (DateOnly.TryParse("2026-12-25", out DateOnly christmas)) Console.WriteLine(christmas.DayOfWeek); if (Enum.TryParse("Friday", out DayOfWeek day)) Console.WriteLine((int)day);
apples: 12 no pears Friday 5
in: pass read-only by reference
in passes a variable by reference but forbids the method from changing it. Its purpose is performance: a large struct isn't copied. For ordinary types like int or classes, it gives no benefit.
var p = new Point3(1, 2, 3); Console.WriteLine(Length(in p)); static double Length(in Point3 v) { v = new Point3(0, 0, 0); return Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z); } record struct Point3(double X, double Y, double Z);
Program.cs(6,5): error CS8331: Cannot assign to variable 'v' or use it as the right hand side of a ref assignment because it is a readonly variable
Remove the assignment and it compiles. (record struct is covered in lesson 40.)
Comparing the modifiers
| Modifier | Caller must initialise? | Method must assign? | Method can change it? | Typical use |
|---|---|---|---|---|
| (none) | yes | — | only its own copy | Almost everything |
ref | yes | no | yes | Swapping, updating a caller's variable |
out | no | yes | yes | Try methods, extra results |
in | yes | no | no | Passing big structs cheaply |
Prefer return values and tuples
ref and out make data flow harder to follow. Outside the Try pattern, returning a value — or a tuple for several values — is usually clearer:
var (q, r) = Divide(17, 5); Console.WriteLine($"{q} remainder {r}"); Console.WriteLine(Math.DivRem(17, 5)); // the library has this built in static (int Quotient, int Remainder) Divide(int a, int b) => (a / b, a % b);
3 remainder 2 (3, 2)
Try it yourself
Write TryParseTime that turns text like "14:30" into hours and minutes, returning false for invalid input.
14:30 -> 14 h 30 min 9:05 -> 9 h 5 min 25:00 -> invalid noon -> invalid