Why Pass by Reference?
If you've ever needed a method to return multiple values or modify a variable directly, you've probably wondered whether to use ref or out. Both let you pass arguments by reference instead of by value, but they serve different purposes and follow different rules.
The ref keyword passes an existing variable's memory address so the method can read and modify it. The out keyword also passes by reference, but it's designed for methods that produce new values without caring about the initial state. Understanding this distinction prevents bugs and makes your intent clearer to other developers.
You'll learn when to use each keyword, how they differ in assignment rules, and practical patterns like TryParse that rely on out. By the end, you'll know which modifier fits your scenario and how to avoid common mistakes that confuse even experienced developers.
Understanding ref Parameters
A ref parameter passes a variable by reference, meaning the method receives the actual memory location instead of a copy. Any changes the method makes affect the original variable. The caller must initialize the variable before passing it, because the method might read its value before modifying it.
This is useful when you want a method to update an existing value or work with large structs without copying them. The keyword appears at both the method declaration and the call site, making it explicit that the variable might change.
void Increment(ref int value)
{
// We can read the current value
Console.WriteLine($"Before: {value}");
// Then modify it
value++;
Console.WriteLine($"After: {value}");
}
// Usage: variable must be initialized
int count = 10;
Increment(ref count);
Console.WriteLine($"Final count: {count}");
Output:
Before: 10
After: 11
Final count: 11
The method reads the initial value of count, increments it, and the change persists after the method returns. Because ref allows both reading and writing, you must initialize the variable before calling the method. Passing an uninitialized variable causes a compiler error.
Understanding out Parameters
An out parameter also passes by reference, but it's specifically designed for methods that produce values. The method doesn't expect the variable to have a meaningful initial value, so you can pass uninitialized variables. In return, the method must assign the parameter before it returns.
This pattern is common in TryParse methods where the method returns a boolean success flag and produces the parsed value via an out parameter. The compiler enforces that every code path assigns the out parameter, guaranteeing callers receive valid data.
bool TryDivide(int numerator, int denominator, out double result)
{
if (denominator == 0)
{
result = 0; // Must assign even when returning false
return false;
}
result = (double)numerator / denominator;
return true;
}
// Usage: variable doesn't need initialization
double answer;
if (TryDivide(10, 3, out answer))
{
Console.WriteLine($"Result: {answer}");
}
else
{
Console.WriteLine("Division failed");
}
Output:
Result: 3.3333333333333335
The TryDivide method assigns result on both success and failure paths. This ensures answer always has a valid value after the call, even though we didn't initialize it beforehand. The compiler verifies this guarantee at compile time.
Inline out Variable Declarations
Since C# 7, you can declare out variables directly in the method call. This reduces boilerplate by eliminating the separate declaration line and keeps the variable scope tight to where it's used. The compiler infers the type from the method signature.
// Old style: declare variable first
int value1;
if (int.TryParse("123", out value1))
{
Console.WriteLine($"Parsed: {value1}");
}
// Modern style: declare inline
if (int.TryParse("456", out int value2))
{
Console.WriteLine($"Parsed: {value2}");
}
// Works with complex types too
if (DateTime.TryParse("2025-11-04", out DateTime date))
{
Console.WriteLine($"Date: {date:yyyy-MM-dd}");
}
// Discard pattern when you only care about success
if (int.TryParse("789", out _))
{
Console.WriteLine("Input is a valid integer");
}
Output:
Parsed: 123
Parsed: 456
Date: 2025-11-04
Input is a valid integer
The inline declaration keeps the variable scoped to the if statement block. If you try to access value2 outside the if, the compiler reports an error. The discard pattern out _ tells the compiler you don't need the parsed value, just the success indicator.
Key Differences Between ref and out
While both ref and out pass by reference, they differ in initialization requirements and intended use. Here's a practical example that shows when each is appropriate:
// ref: reads AND modifies existing value
void ApplyDiscount(ref decimal price, decimal discountPercent)
{
// We need the current price to calculate the discount
decimal discount = price * (discountPercent / 100);
price -= discount;
}
// out: produces a new value, ignores initial state
bool CalculateTax(decimal amount, out decimal tax)
{
if (amount < 0)
{
tax = 0;
return false;
}
tax = amount * 0.08m; // 8% tax rate
return true;
}
// Usage demonstrates the difference
decimal productPrice = 100m;
ApplyDiscount(ref productPrice, 10); // Must initialize first
Console.WriteLine($"Discounted price: {productPrice:C}");
// tax variable doesn't need initialization
if (CalculateTax(productPrice, out decimal salesTax))
{
Console.WriteLine($"Sales tax: {salesTax:C}");
Console.WriteLine($"Total: {(productPrice + salesTax):C}");
}
Output:
Discounted price: $90.00
Sales tax: $7.20
Total: $97.20
ApplyDiscount uses ref because it needs to read the current price to calculate the discount. CalculateTax uses out because it produces a completely new tax value based on the input amount. The previous value of salesTax (if any) is irrelevant.
Returning Multiple Values
The out parameter pattern is common for methods that need to return both a status and a result. However, modern C# also supports tuples for this scenario. Let's compare both approaches:
// Traditional approach with out parameter
bool TryGetUserByEmail(string email, out User user)
{
if (string.IsNullOrEmpty(email))
{
user = null;
return false;
}
// Simulate database lookup
user = new User { Email = email, Name = "John Doe" };
return true;
}
// Modern approach with tuple return
(bool Success, User User) GetUserByEmail(string email)
{
if (string.IsNullOrEmpty(email))
{
return (false, null);
}
var user = new User { Email = email, Name = "Jane Smith" };
return (true, user);
}
// Usage comparison
if (TryGetUserByEmail("test@example.com", out User foundUser))
{
Console.WriteLine($"Found (out): {foundUser.Name}");
}
var (success, user) = GetUserByEmail("demo@example.com");
if (success)
{
Console.WriteLine($"Found (tuple): {user.Name}");
}
class User
{
public string Email { get; set; }
public string Name { get; set; }
}
Output:
Found (out): John Doe
Found (tuple): Jane Smith
Both patterns work, but tuples are often more readable for new code. Use out parameters when you're matching existing API conventions (like TryParse) or when microseconds matter, since tuples can allocate heap memory for reference types.
Common Pitfalls
Forgetting the keyword at the call site: Unlike some languages, C# requires you to specify ref or out when calling the method. This makes it obvious at the call site that the argument might change. If you forget the keyword, the compiler reports an error.
Assuming out clears the variable: When you pass a variable as out, its initial value is technically visible to the method, though the compiler prevents you from reading it before assignment. Don't assume out automatically zeros or nulls the variable.
Trying to overload on ref vs out: You can't create two methods that differ only by ref versus out. The compiler treats them as the same signature for overload resolution. This is because both compile to the same IL instruction at the low level.
Not assigning all out parameters: Every code path must assign every out parameter before the method returns. If you have multiple return statements, each one must come after all out assignments. The compiler checks this flow analysis and reports errors if any path might skip the assignment.
Try It Yourself
Here's a complete example that demonstrates both ref and out in a practical scenario. This console application simulates a shopping cart where you can update quantities and calculate totals.
Steps:
- Create a new console project:
dotnet new console -n RefOutDemo
- Navigate to the project folder:
cd RefOutDemo
- Replace Program.cs with the code below
- Execute the application:
dotnet run
// Using ref to modify existing cart total
void AddToCart(ref decimal cartTotal, decimal itemPrice, int quantity)
{
cartTotal += itemPrice * quantity;
Console.WriteLine($"Added ${itemPrice * quantity:F2}. New total: ${cartTotal:F2}");
}
// Using out to calculate tax separately
bool CalculateSalesTax(decimal subtotal, string state, out decimal tax)
{
tax = state.ToUpper() switch
{
"CA" => subtotal * 0.0725m,
"TX" => subtotal * 0.0625m,
"NY" => subtotal * 0.04m,
_ => 0
};
return tax > 0;
}
// Start with empty cart
decimal cartTotal = 0m;
// Add items using ref to accumulate
AddToCart(ref cartTotal, 29.99m, 2);
AddToCart(ref cartTotal, 15.50m, 1);
AddToCart(ref cartTotal, 49.99m, 1);
Console.WriteLine($"\nSubtotal: ${cartTotal:F2}");
// Calculate tax using out
if (CalculateSalesTax(cartTotal, "CA", out decimal tax))
{
Console.WriteLine($"Sales Tax (CA): ${tax:F2}");
Console.WriteLine($"Grand Total: ${cartTotal + tax:F2}");
}
else
{
Console.WriteLine("No sales tax for this state");
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Expected Output:
Added $59.98. New total: $59.98
Added $15.50. New total: $75.48
Added $49.99. New total: $125.47
Subtotal: $125.47
Sales Tax (CA): $9.10
Grand Total: $134.57
The ref parameter in AddToCart accumulates the running total across multiple calls. The out parameter in CalculateSalesTax produces a new tax value based on the state. This separation of concerns makes each method's purpose clear.
When Not to Use ref or out
While ref and out solve specific problems, they're not always the best choice. Here's when to look for alternatives:
When returning a single value: If your method only needs to return one thing, just return it normally. Don't use out to return a value and a status code when you could return the value and throw an exception on errors. Exceptions are clearer for truly exceptional cases.
When working with async methods: You can't use ref or out with async methods or lambda expressions that cross async boundaries. If you need multiple return values from an async method, use tuples instead: async Task<(bool, int)> TryGetValueAsync().
When the method conceptually returns something: Modern C# has better ways to express multiple return values. Named tuples make intent clearer: (bool IsValid, string ErrorMessage) Validate() reads better than a boolean return plus an out string error parameter.
When you're optimizing prematurely: Some developers use ref to avoid copying structs, but small structs (under 16 bytes) copy efficiently. Profile before optimizing. For large structs, consider readonly ref or in parameters instead, which prevent accidental modification.