Parameters
By the end of this lesson
Pass data into methods, including by value, by reference and with out.
Parameters are how a method gets what it needs. The behaviour that catches almost everyone is what happens when the thing you pass is an object.
The rule in C# is one sentence: arguments are copied. Everything else follows from what exactly gets copied.
int stock = 10;
Reduce(stock, 3);
Console.WriteLine(stock); // still 10
static void Reduce(int quantity, int amount)
{
quantity -= amount; // changes the copy and nothing else
}- quantity is a copy of stock. Subtracting from it cannot reach back to the caller's variable.
- This surprises people because the method looks like it is doing something. It is — to a value that disappears when the method ends.
- To give the caller the new quantity, return it: stock = Reduce(stock, 3);
Objects: the reference is copied, the object is not
var order = new Order { Reference = "ORD-1041", Status = "Draft" };
Mutate(order);
Console.WriteLine(order.Status); // Submitted — the caller sees this
Replace(order);
Console.WriteLine(order.Reference); // ORD-1041 — the caller does not see this
static void Mutate(Order order)
{
order.Status = "Submitted";
}
static void Replace(Order order)
{
order = new Order { Reference = "ORD-9999", Status = "Draft" };
}
public class Order
{
public string Reference { get; set; } = "";
public string Status { get; set; } = "";
}- Both methods take the same kind of parameter. Mutate changes the object the reference points at, and the caller sees it, because there is only one object.
- Replace assigns a new object to its parameter. That parameter is a copy of the reference, so the caller's variable still points where it always did, and the new order is discarded when the method ends.
- This is what "reference types are passed by value" means: the reference is copied, the object is shared.
- When a method should give the caller a different object, return it. That is clearer than any keyword, and the call site shows it.
The modifiers, and what each is for:
- No modifier
- The method gets a copy of the value, or a copy of the reference. This covers the large majority of parameters you will write.
- ref
- The method works on the caller's variable itself, so assigning to it is visible outside. The caller must give it a value first, and ref appears at the call site as well.
- out
- The method must assign it before returning. The caller does not set it first. This is what TryParse uses to hand back the parsed value.
- in
- A read-only reference. It exists to avoid copying a large struct, and the method cannot assign to it.
- params
- Accepts any number of arguments as an array, so the caller writes a comma-separated list. It has to be the last parameter.
decimal balance = 250m;
ApplyFee(ref balance, 4.99m);
Console.WriteLine(balance); // 245.01
if (TryGetDiscountRate("SPRING24", out decimal rate))
{
Console.WriteLine($"Rate: {rate:P0}");
}
Console.WriteLine(Total(19.99m, 4.50m, 120m)); // 144.49
static void ApplyFee(ref decimal balance, decimal fee)
{
balance -= fee;
}
static bool TryGetDiscountRate(string code, out decimal rate)
{
rate = 0m; // assigned on every path, including failure
if (code != "SPRING24")
{
return false;
}
rate = 0.10m;
return true;
}
static decimal Total(params decimal[] amounts)
{
decimal total = 0m;
foreach (decimal amount in amounts)
{
total += amount;
}
return total;
}- ref has to be written at the call site too. C# insists on that so a reader can see which arguments a call might change.
- An out parameter must be assigned on every path out of the method, failure included, or the build fails. Setting it first is the simplest way to satisfy that.
- The Try pattern pairs a bool with an out value: one answer for whether it worked, one for the result.
- params lets the caller pass three numbers rather than construct an array. Inside the method it is an ordinary array.
Summary
- Arguments are copied: for a value type the value, for a reference type the reference
- Changing an object inside a method is visible to the caller; replacing it is not
- ref lets a method assign to the caller's variable, and must be written at the call site
- out must be assigned on every path and is the basis of the Try pattern
- For two results that belong together, prefer a tuple or a small class over ref and out
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Write one method that adds a SKU to a List<string> it is given, and another that assigns a brand new list to its parameter.
Print the caller's list count after each call. Predict both numbers before you run it.
Show solution
The first method changes the list the caller is holding, because both the parameter and the caller's variable refer to the same list object. The count goes up.
The second method points its own copy of the reference at a new list. The caller's variable is unaffected, so the count is unchanged and the new list is thrown away.
Neither method is wrong. The lesson is that the signature looks identical in both cases, so the only way to know which one a method does is to read it — which is a good argument for methods that return their result instead.
List<string> skus = new List<string> { "LAMP-01" };
AddSku(skus, "STAND-04");
Console.WriteLine(skus.Count); // 2 — the same list was changed
ReplaceList(skus);
Console.WriteLine(skus.Count); // still 2 — the caller kept its own list
static void AddSku(List<string> skus, string sku)
{
skus.Add(sku);
}
static void ReplaceList(List<string> skus)
{
skus = new List<string> { "TIDY-09" };
}Challenge
Challenge
A method is declared as void ApplyMonthlyFee(ref decimal balance, out decimal feeCharged).
Rewrite it so both results come back through the return value, then decide which version you would rather call and why.
Show solution
A tuple return carries both values with names, so the call site reads as two results rather than two side effects. The caller's balance variable is only changed where the caller chooses to change it.
The version with ref and out is not broken, and in a hot loop it avoids allocating anything. That rarely decides the matter in business code.
What usually decides it: the returning version can be called inside a larger expression, can be used in an async method, and can be read without checking the method body to learn which arguments it modifies.
decimal balance = 250m;
var result = ApplyMonthlyFee(balance, 4.99m);
balance = result.NewBalance;
Console.WriteLine($"Charged {result.FeeCharged:N2}, balance now {balance:N2}");
static (decimal NewBalance, decimal FeeCharged) ApplyMonthlyFee(decimal balance, decimal fee)
{
decimal charged = balance >= fee ? fee : 0m;
return (balance - charged, charged);
}Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.