C# & .NET (C# 14, .NET 10 LTS) Module 3 – Control flow and methods
ref, out, in and ref readonly parameters
Pass variables by reference in C# with ref, return extra results with out, pass large structs read-only with in and ref readonly, and write Try methods.
What you will learn
- Pass a variable by reference with ref, and return extra results with out
- Write and call a Try method that returns bool and its value through out
- Choose between in, ref readonly and a plain parameter for a struct, and spot hidden copies
- Fix the compiler errors that ref, out and in parameters cause
- Write a C# 14 lambda whose parameters take out or ref without their types
Before you start
What you’ll learn · Before you start
What you will learn
- Pass a variable by reference with ref, and return extra results with out
- Write and call a Try method that returns bool and its value through out
- Choose between in, ref readonly and a plain parameter for a struct, and spot hidden copies
- Fix the compiler errors that ref, out and in parameters cause
- Write a C# 14 lambda whose parameters take out or ref without their types
Before you start
On this page
Swap(a, b) with a at 1 and b at 2 swaps nothing: the method receives copies of 1 and 2 and swaps the copies.
To change a caller’s variables, or to hand back more than one result, a parameter must refer to the caller’s variable
itself, and C# has four modifiers for that: ref, out, in and ref readonly.
You will swap two variables with ref, hand back extra results with out, write a Try method like int.TryParse,
and choose a modifier from a table of what each one allows.
A plain parameter gets a copy, and ref passes the variable itself
By default C# passes every argument by value: the parameter is a new variable that starts with a copy of the
argument, so the method can change it without touching the caller. This is passing by value: Passing an argument as a copy: the parameter is a new variable that starts with the argument's value, so assigning to it never changes the caller's variable. C# passes every argument this way unless a modifier says otherwise.. A ref parameter is passing by reference: Passing the caller's variable itself, with ref, out, in or ref readonly: the parameter is another name for that variable, so the method works on it rather than on a copy.: it is another name for
the caller’s variable, so whatever the method assigns to it, the caller sees. Both the method and the call write
ref, and the variable must have a value before the call. Step through the two swaps:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- SwapCopies(a, b). SwapCopies(a, b) is called. x gets a copy of the value of a, 1, and y a copy of b, 2: two new variables that belong to the method.
- The copies are swapped. (x, y) = (y, x) swaps the copies, so x is 2 and y is 1. Nothing connects them to a and b any more.
- The copies are gone. SwapCopies returns and its x and y disappear. a is still 1 and b is still 2, as swap.cs printed.
- Swap(ref a, ref b). Swap(ref a, ref b) is called. This time x is not a new variable: it is another name for a, and y is another name for b.
- The swap reaches the caller. The same swap now changes the variables the names refer to, so a becomes 2 and b becomes 1.
- Visible at the call. After the call a is 2 and b is 1. The call had to write ref twice, so anyone reading it can see that a and b may change.
// A plain parameter is a copy of the caller's value; a ref parameter is the caller's variable itself.
int a = 1, b = 2;
SwapCopies(a, b);
Console.WriteLine($"after SwapCopies: a = {a}, b = {b}");
Swap(ref a, ref b);
Console.WriteLine($"after Swap: a = {a}, b = {b}");
int[] seats = [7, 9];
Swap(ref seats[0], ref seats[1]); // array elements are variables too
Console.WriteLine($"seats after Swap: {seats[0]}, {seats[1]}");
static void SwapCopies(int x, int y)
{
(x, y) = (y, x); // swaps the two copies, and nothing else
}
static void Swap(ref int x, ref int y)
{
(x, y) = (y, x);
} Output
after SwapCopies: a = 1, b = 2 after Swap: a = 2, b = 1 seats after Swap: 9, 7
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run swap.cs
SwapCopies swapped its own x and y and left a and b as they were. Swap(ref a, ref b) made x another name
for a and y for b, so the same swap changed the caller’s variables. Array elements are variables too, which is
why Swap(ref seats[0], ref seats[1]) swapped the two seats. A property is not a variable, so it cannot be passed with
ref.
out hands back an extra result, and the Try pattern is built on it
An out parameter is passed by reference as well, but its value flows the other way: the caller need not give it a
value, and the method must assign it before it returns. The call can declare the variable on the spot,
out int number, or throw the result away with out _. Every TryParse method in .NET has one:
// out hands back an extra result. A Try method returns whether it worked, and the value through out.
foreach (string text in new[] { "42", "4x2", "", "2147483648" })
{
bool ok = int.TryParse(text, out int number);
Console.WriteLine($"int.TryParse(\"{text}\"): {ok}, number = {number}");
}
if (TryParseQuantity("12 kg", out int quantity, out string unit))
{
Console.WriteLine($"TryParseQuantity(\"12 kg\"): {quantity} and {unit}");
}
Console.WriteLine($"TryParseQuantity(\"kg\"): {TryParseQuantity("kg", out _, out _)}");
static bool TryParseQuantity(string text, out int quantity, out string unit)
{
string[] parts = text.Split(' ', 2);
if (parts.Length == 2 && int.TryParse(parts[0], out quantity))
{
unit = parts[1];
return true;
}
quantity = 0;
unit = "";
return false;
} Output
int.TryParse("42"): True, number = 42
int.TryParse("4x2"): False, number = 0
int.TryParse(""): False, number = 0
int.TryParse("2147483648"): False, number = 0
TryParseQuantity("12 kg"): 12 and kg
TryParseQuantity("kg"): False
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run tryparse.cs
int.TryParse returned true and 42 for “42”, and false with number set to 0 for the other three: “4x2” is not a
number, the empty string has no digits, and 2147483648 is one more than int.MaxValue. This is the
Try pattern: A method named TrySomething that returns true or false for success and hands back its result through an out parameter, such as int.TryParse, beside a method that throws for bad input. of Microsoft’s design guidelines: the name starts with Try, the method
returns bool for success and hands back the value through out, and a method that throws exists beside it for
callers that expect valid input. TryParseQuantity follows the pattern with two out parameters and assigns both on
every path, because the compiler refuses a method that could return without assigning one of them.
Checkpoint
Check: Copies and ref parameters · out parameters and the Try pattern
in and ref readonly pass a variable for reading only
In parameters and ref readonly parameters pass by reference too, but the method may only read the variable. They
exist for structs, whose every byte a copy moves, while a reference is 8 bytes on a 64-bit process. With in the
call may leave the modifier out, and a constant is passed through a hidden copy. With ref readonly, new in C# 12,
the call writes in or ref, and the compiler warns when it writes neither.
| Modifier | Caller gives it a value first | Method may assign it | The call writes |
|---|---|---|---|
| none | yes | its own copy only | F(x) |
ref |
yes | yes | F(ref x) |
out |
no | must, before it returns | F(out x) or F(out var x) |
in |
yes | no | F(x) or F(in x) |
ref readonly |
yes | no | F(in x) or F(ref x) |
Passing a large struct with in avoids copying it, but calling a method that may change the struct on an in
parameter makes the compiler copy it anyway. Async methods and iterators cannot have ref, out or in parameters at
all, so an async method returns its extra results in a tuple or a record instead. C# 14 lets a lambda’s parameters
carry these modifiers without their types.
Key idea
ref lets a method read and change the caller’s variable, out makes it assign one, and in and ref readonly
let it only read one, so the modifier on a parameter says what may happen to the argument.
Checkpoint
Check: in and ref readonly
in and ref readonly with structs: hidden copies shown, and when they pay Pro: opens with a Pro, Premium or Ultimate pass
in promises that the method will not change the caller’s struct, and the compiler keeps that promise even when it
costs a copy. The runs below make the hidden copy visible, measure how big common structs are, and show the warnings
that ref readonly adds at the call.
In this part:
- A mutable struct behind in: the hidden copy, made visible
- How big a struct must be before in pays
- ref readonly: asking the caller for a variable
4 runnable examples (C#, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
The errors that ref, out and in cause, reproduced and fixed Pro: opens with a Pro, Premium or Ultimate pass
Each of these modifiers comes with rules the compiler enforces, and five of them catch most mistakes. Each project below breaks one rule; the session builds all five, and a table pairs each error with its fix.
In this part:
- Five builds that stop, and what each one means
- Results from async code: a tuple instead of out
7 runnable examples (C#, Python) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Designing Try methods, and C# 14 lambdas with out parameters Pro: opens with a Pro, Premium or Ultimate pass
A Try method is a small contract, and most bugs in them come from breaking one of its terms. The run below follows the
contract with two out parameters and a throwing counterpart, then passes Try methods around as C# 14 lambdas.
In this part:
- What a Try method promises: TryParseTime and its throwing twin
- C# 14: out and ref on lambda parameters without their types
2 runnable examples (C#) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: check the format of an IFSC
The IFSC format
The Reserve Bank of India describes the Indian Financial System Code as 11 characters: 4 letters for the bank, a
fifth character that is always 0 (zero), and 6 for the branch. Your TryParseIfsc accepts lower case, returns both
parts in capitals, sets both outs to “” for anything else, null included, and checks the format only.
ifsc.cs starts with a method that throws, so all eight of its checks fail:
// Practice: write TryParseIfsc, then run this file until all eight checks pass. An IFSC has 11 characters: 4 letters
// for the bank, a fifth character that is always 0 (zero), and 6 letters or digits for the branch. Accept lower case,
// return both parts in capitals, and set both outs to "" when the input is not an IFSC. This checks the format only.
(string? Input, bool Ok, string Bank, string Branch)[] checks =
[
("SBIN0001234", true, "SBIN", "001234"),
("HDFC0ABC123", true, "HDFC", "ABC123"),
("icic0004321", true, "ICIC", "004321"),
("SBIN1001234", false, "", ""),
("SBIN000123", false, "", ""),
("SB1N0001234", false, "", ""),
("SBIN00012$4", false, "", ""),
(null, false, "", ""),
];
int passed = 0;
foreach (var (input, ok, bank, branch) in checks)
{
string shown = input is null ? "null" : $"\"{input}\"";
string verdict;
try
{
bool got = TryParseIfsc(input, out string gotBank, out string gotBranch);
verdict = got == ok && gotBank == bank && gotBranch == branch
? "ok"
: $"FAIL, returned {got} with \"{gotBank}\" and \"{gotBranch}\"";
}
catch (Exception e)
{
verdict = $"FAIL, {e.GetType().Name}";
}
if (verdict == "ok")
{
passed++;
}
Console.WriteLine($"TryParseIfsc({shown}): {verdict}");
}
Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;
static bool TryParseIfsc(string? input, out string bank, out string branch)
{
throw new NotImplementedException("Check the length and each part, then assign bank and branch.");
} Output (exit status 1)
TryParseIfsc("SBIN0001234"): FAIL, NotImplementedException
TryParseIfsc("HDFC0ABC123"): FAIL, NotImplementedException
TryParseIfsc("icic0004321"): FAIL, NotImplementedException
TryParseIfsc("SBIN1001234"): FAIL, NotImplementedException
TryParseIfsc("SBIN000123"): FAIL, NotImplementedException
TryParseIfsc("SB1N0001234"): FAIL, NotImplementedException
TryParseIfsc("SBIN00012$4"): FAIL, NotImplementedException
TryParseIfsc(null): FAIL, NotImplementedException
0 of 8 checks pass
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run ifsc.cs
Write TryParseIfsc, run dotnet run ifsc.cs until it prints 8 of 8 checks pass, then try the questions below:
Practice that adapts to you
Practise this lesson
9 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between ref and out parameters in C#?
Both pass a variable by reference, so the method works on the caller’s variable, not on a copy, and both must be
written at the call as well as in the method. They differ in which way the value flows. A ref argument must already
have a value, and the method may read it and may change it, as Swap(ref a, ref b) does. An out argument need not
have a value, and the method must assign it before it returns, or the build fails; the call can declare it on the
spot, out int number. Use ref when a method updates an existing value, and out when it produces an extra result,
as Try methods do.
-
Fresher to mid
Code review finds bool TryGetDiscount(string code, out decimal discount). It returns false for unknown codes but sets discount to the last valid code's rate, and it also returns false when the pricing database times out. What contract should a Try method keep, and how would you test it?
-
Mid to senior
To save copies, a team changes Process(Reading r) to Process(in Reading r) for a 64-byte struct that is passed millions of times. The benchmark shows no gain, and in one case a loss. Why might in not help, and what would you check and change?
-
Fresher to mid
A parsing method hands back three values through out parameters. Now it must call a web service, so it becomes async, and the build fails with CS1988. Why does the compiler refuse, and what would you return instead?
Key takeaways
- Arguments are passed by value unless a modifier says otherwise, so a method changes only its own copy.
refmakes a parameter another name for the caller’s variable, which must have a value first; the call writesreftoo.- An out parameter makes the method assign the variable before it returns, which is what the Try pattern uses to hand
back a value beside a
bool. - In parameters and ref readonly parameters pass a variable by reference for reading only, which can save copying a large struct.
Terms in this lesson
- passing by reference
- Passing the caller's variable itself, with ref, out, in or ref readonly: the parameter is another name for that variable, so the method works on it rather than on a copy.
- passing by value
- Passing an argument as a copy: the parameter is a new variable that starts with the argument's value, so assigning to it never changes the caller's variable. C# passes every argument this way unless a modifier says otherwise.
- Try pattern
- A method named TrySomething that returns true or false for success and hands back its result through an out parameter, such as int.TryParse, beside a method that throws for bad input.
References
- Method parameters and modifiers (ref, out, in, ref readonly) (Microsoft Learn)
- Overview of methods (passing parameters) (Microsoft Learn)
- Framework Design Guidelines: exceptions and performance (the Try-Parse pattern) (Microsoft Learn)
- Int32.TryParse method (Microsoft Learn)
- Avoid memory allocations and data copies (Microsoft Learn)
- Structure types (readonly structs and readonly members) (Microsoft Learn)
- C# language specification: Expressions (function member invocation) (Microsoft Learn (ECMA-334 draft))
- Lambda expressions (parameters with modifiers, captured variables) (Microsoft Learn)
- What's new in C# 14 (simple lambda parameters with modifiers) (Microsoft Learn)
- Reserve Bank of India: frequently asked questions on NEFT (the IFSC format) (Reserve Bank of India)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress