Parameter Passing Techniques in C#
When you call a method, you usually send it some data to work with. The way that data is sent to the method is called parameter passing. C# gives us several techniques, and each one decides an important question: can the method change the caller's variable or not?
In this lesson we learn all of them with small, easy examples:
- Pass by value (the default)
- Pass by reference with
ref - Output parameters with
out - Read-only reference with
in - Variable number of arguments with
params - Optional parameters and named arguments
Parameter vs argument
These two words are often mixed up, so let's make them clear first.
static int Add(int a, int b) // a and b are PARAMETERS (in the method definition)
{
return a + b;
}
int result = Add(10, 20); // 10 and 20 are ARGUMENTS (the actual values sent)
- Parameter: the variable written in the method definition. It is like an empty box waiting for a value.
- Argument: the real value you pass when you call the method.
Quick recap: value types and reference types
To understand parameter passing, you must remember what a variable holds:
| Value types | Reference types | |
|---|---|---|
| Examples | int, double, bool, char, decimal, DateTime, struct, enum | class objects, string, arrays, List<T> |
| The variable holds | The actual value (e.g. 10) | An address (reference) pointing to the object in memory |
| Copying the variable copies | The value | The address, so both copies point to the same object |
Think of a value type as a photocopy of a document: if someone writes on the photocopy, your original is safe. A reference type is like sharing the address of your house: whoever has the address can go and repaint your house.
1. Pass by value (the default)
When you don't write any keyword, C# passes a copy of the argument to the method. The method works on its own copy.
Pass by value with a value type
using System;
class Program
{
static void Increase(int number)
{
number = number + 10;
Console.WriteLine($"Inside method: {number}");
}
static void Main()
{
int marks = 50;
Increase(marks);
Console.WriteLine($"After method : {marks}");
}
}
The method changed only its own copy. The caller's marks is still 50.
Pass by value with a reference type
This is where many learners get confused. A copy is still passed, but the copy is a copy of the address. So both variables point to the same object.
class Student
{
public string Name { get; set; } = "";
public int Marks { get; set; }
}
class Program
{
static void GiveBonus(Student s)
{
s.Marks = s.Marks + 10; // changes the SAME object the caller has
}
static void ReplaceStudent(Student s)
{
s = new Student { Name = "New Student", Marks = 99 }; // only the local copy now points to a new object
}
static void Main()
{
Student rahul = new Student { Name = "Rahul", Marks = 70 };
GiveBonus(rahul);
Console.WriteLine($"After GiveBonus : {rahul.Name}, {rahul.Marks}");
ReplaceStudent(rahul);
Console.WriteLine($"After ReplaceStudent: {rahul.Name}, {rahul.Marks}");
}
}
- Changing the object's data (
s.Marks = ...) is visible to the caller, because both point to the same object. - Assigning a new object (
s = new Student()) is not visible to the caller, because only the method's copy of the address changed.
string is a reference type, but it is immutable (it can never be changed). Any "change" like name = name + "!" creates a new string, so a method can never change the caller's string when it is passed by value.
2. Pass by reference with ref
The ref keyword passes the variable itself, not a copy. Any change inside the method changes the caller's variable.
Rules of ref:
- Write
refin both places: the method definition and the method call. - The variable must be initialised before passing it.
- The method may read it and may change it (changing is not required).
static void Increase(ref int number)
{
number = number + 10;
}
static void Main()
{
int marks = 50;
Increase(ref marks);
Console.WriteLine($"After method: {marks}");
}
Classic example: swap two numbers
static void Swap(ref int x, ref int y)
{
int temp = x;
x = y;
y = temp;
}
static void Main()
{
int a = 5, b = 9;
Swap(ref a, ref b);
Console.WriteLine($"a = {a}, b = {b}");
}
Without ref, the method would swap only its own copies and a and b would stay 5 and 9.
ref with a reference type
Remember ReplaceStudent from above? With ref, assigning a new object does reach the caller:
static void ReplaceStudent(ref Student s)
{
s = new Student { Name = "New Student", Marks = 99 };
}
Student rahul = new Student { Name = "Rahul", Marks = 70 };
ReplaceStudent(ref rahul);
Console.WriteLine($"{rahul.Name}, {rahul.Marks}"); // New Student, 99
3. Output parameters with out
The out keyword is used when a method needs to send a value back through a parameter. It is very useful when a method must return more than one value.
Rules of out:
- Write
outin both the definition and the call. - The variable does not need to be initialised before the call.
- The method must assign a value to it before it returns, otherwise you get a compile error.
static void Calculate(int a, int b, out int sum, out int product)
{
sum = a + b;
product = a * b;
}
static void Main()
{
int total, multiply; // not initialised, and that's fine
Calculate(4, 5, out total, out multiply);
Console.WriteLine($"Sum = {total}, Product = {multiply}");
}
Declaring the out variable in the call (C# 7+)
Calculate(4, 5, out int total, out int multiply); // declare inline
Calculate(4, 5, out int onlySum, out _); // _ = "I don't need this value" (discard)
Real-world example: int.TryParse
The most famous use of out in .NET is the TryParse pattern. It returns true/false and gives the converted value through out:
string input = "125";
if (int.TryParse(input, out int age))
{
Console.WriteLine($"Valid age: {age}");
}
else
{
Console.WriteLine("Please enter a number");
}
If the method forgets to assign an out parameter, the compiler stops you:
static void GetValue(out int x)
{
// forgot to assign x
}
4. Read-only reference with in
The in keyword (C# 7.2+) passes the variable by reference, like ref, but the method is not allowed to change it. It is mainly used to pass large structs without copying them, while still keeping them safe.
struct Point3D
{
public double X, Y, Z;
}
static double Distance(in Point3D p)
{
// p.X = 10; // compile error: p is read-only
return Math.Sqrt(p.X * p.X + p.Y * p.Y + p.Z * p.Z);
}
static void Main()
{
Point3D point = new Point3D { X = 3, Y = 4, Z = 12 };
Console.WriteLine(Distance(in point)); // "in" is optional at the call site
}
For small types like int there is no benefit in using in. Use it for big structs in performance-sensitive code.
ref vs out vs in at a glance
| Point | ref | out | in |
|---|---|---|---|
| Passed by | Reference | Reference | Reference |
| Must be initialised before the call? | Yes | No | Yes |
| Method must assign a value? | No | Yes | Not allowed |
| Method can change it? | Yes | Yes (must) | No (read-only) |
| Data direction | In and out (two-way) | Out only | In only |
| Keyword at the call site | Required | Required | Optional |
| Typical use | Swap, update the caller's variable | Return multiple values, TryParse | Pass large structs without copying |
5. Variable number of arguments with params
Sometimes you don't know how many values the caller will send. The params keyword lets a method accept any number of arguments (including zero) as an array.
static int Sum(params int[] numbers)
{
int total = 0;
foreach (int n in numbers)
{
total += n;
}
return total;
}
static void Main()
{
Console.WriteLine(Sum()); // 0 arguments
Console.WriteLine(Sum(5)); // 1 argument
Console.WriteLine(Sum(1, 2, 3, 4)); // 4 arguments
Console.WriteLine(Sum(new[] { 10, 20 })); // an array also works
}
You already use params every day: Console.WriteLine("{0} {1}", a, b) and string.Join(", ", ...) use it.
Rules of params:
- A method can have only one
paramsparameter. - It must be the last parameter:
void Log(string message, params object[] values) - It must be a single-dimensional array (from C# 13 other collection types such as
List<T>andSpan<T>are also allowed). - It can't be combined with
ref,outorin.
6. Optional parameters
An optional parameter has a default value. If the caller doesn't pass it, the default is used.
static void PrintBill(string item, int quantity = 1, double discount = 0)
{
Console.WriteLine($"{item} x {quantity}, discount {discount}%");
}
static void Main()
{
PrintBill("Pen"); // uses both defaults
PrintBill("Notebook", 3); // uses default discount
PrintBill("Bag", 2, 10); // no defaults used
}
Rules of optional parameters:
- Optional parameters must come after all required parameters.
- The default value must be a compile-time constant (a number, string,
null,default), not something likeDateTime.Now.
7. Named arguments
Named arguments let you pass a value by writing the parameter name. This makes calls easier to read and lets you skip some optional parameters.
// Skip quantity, set only discount
PrintBill("Shoes", discount: 15);
// Any order is allowed when all arguments are named
PrintBill(discount: 5, item: "Watch", quantity: 2);
// Much more readable than CreateUser("Amit", true, false)
CreateUser(name: "Amit", isAdmin: true, sendWelcomeEmail: false);
All techniques together
Here is one small program that uses every technique, so you can see them side by side:
using System;
class Program
{
static void Main()
{
int stock = 10;
AddStock(stock); // by value: no change
Console.WriteLine($"By value : {stock}"); // 10
AddStock(ref stock); // ref: changed
Console.WriteLine($"By ref : {stock}"); // 15
GetPriceRange(out int min, out int max); // out: returns two values
Console.WriteLine($"Range : {min} - {max}");
Console.WriteLine($"Total : {Total(100, 250, 75)}"); // params
Greet("Priya"); // optional
Greet("Priya", greeting: "Welcome"); // named
}
static void AddStock(int qty) => qty += 5;
static void AddStock(ref int qty) => qty += 5;
static void GetPriceRange(out int min, out int max)
{
min = 100;
max = 500;
}
static int Total(params int[] prices)
{
int sum = 0;
foreach (int p in prices) sum += p;
return sum;
}
static void Greet(string name, string greeting = "Hello")
{
Console.WriteLine($"{greeting}, {name}!");
}
}
Notice that AddStock(int) and AddStock(ref int) are two different overloads. You can overload a method using ref versus no keyword, but you cannot have two overloads that differ only by ref and out.
Which technique should I use?
| You want to... | Use |
|---|---|
| Just give data to a method (most cases) | Pass by value (no keyword) |
| Let the method change the caller's variable | ref |
| Get extra values back, or use a Try... pattern | out |
| Pass a large struct quickly and safely | in |
| Accept any number of values | params |
| Give parameters sensible defaults | Optional parameters |
| Make calls with many arguments readable | Named arguments |
Tip: don't overuse ref and out. If a method needs to return several values, a tuple such as (int Sum, int Product) Calculate(int a, int b) or a small class is often cleaner.
Preparing for interviews? Read Parameter Passing Interview Questions and Answers.
Summary
- By default C# passes arguments by value: the method gets a copy.
- For reference types, the copy is a copy of the address, so the method can change the object's data but cannot replace the caller's object.
ref: two-way, must be initialised before the call.out: one-way out, need not be initialised, must be assigned inside the method.in: by reference but read-only, useful for large structs.params: any number of arguments, only one and only as the last parameter.- Optional parameters have default values; named arguments pass values by parameter name.