Overview
Understanding parameter passing mechanisms is crucial for writing efficient, bug-free code. The choice between value, reference, and output parameters affects both behavior and performance.
Core Concepts
Method Signatures and Overloading
public class Calculator
{
// Method overloading - same name, different parameters
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
public int Add(int a, int b, int c) => a + b + c;
// Return type alone doesn't distinguish overloads
// public double Add(int a, int b) => a + b; // Error: already defined
// Different parameter types = different method
public string Describe(int number) => $"Integer: {number}";
public string Describe(string text) => $"String: {text}";
}
// Usage
var calc = new Calculator();
int result1 = calc.Add(1, 2); // Calls int version
double result2 = calc.Add(1.5, 2.5); // Calls double version
string desc = calc.Describe(42); // Calls int version
Pass by Value vs Reference
// Value types - copy is passed
void ModifyValue(int x)
{
x = 100; // Only modifies local copy
}
int number = 5;
ModifyValue(number);
Console.WriteLine(number); // Still 5
// Reference types - reference copy is passed
void ModifyList(List<int> list)
{
list.Add(100); // Modifies original
list = new List<int>(); // Only changes local reference
}
var myList = new List<int> { 1, 2, 3 };
ModifyList(myList);
Console.WriteLine(myList.Count); // 4 (100 was added)
// ref keyword - actual variable is passed
void ModifyWithRef(ref int x)
{
x = 100; // Modifies original
}
int num = 5;
ModifyWithRef(ref num);
Console.WriteLine(num); // 100
ref, out, and in Parameters
// ref - bidirectional (read and write)
void Swap(ref int a, ref int b)
{
int temp = a; // Can read
a = b; // Can write
b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y); // x=2, y=1
// out - output only (must assign before return)
bool TryDivide(int dividend, int divisor, out int quotient, out int remainder)
{
if (divisor == 0)
{
quotient = 0;
remainder = 0;
return false;
}
quotient = dividend / divisor;
remainder = dividend % divisor;
return true;
}
// Inline out declaration (C# 7+)
if (TryDivide(10, 3, out int q, out int r))
{
Console.WriteLine($"{q} remainder {r}"); // 3 remainder 1
}
// Discard out parameters you don't need
if (TryDivide(10, 3, out _, out int remainder))
{
Console.WriteLine($"Remainder: {remainder}");
}
// in - readonly reference (optimization for large structs)
readonly struct LargeStruct
{
public readonly double X, Y, Z, W;
// Many more fields...
}
double CalculateLength(in LargeStruct point)
{
// point.X = 5; // Error: cannot modify
return Math.Sqrt(point.X * point.X + point.Y * point.Y);
}
params Keyword
// Variable number of arguments
public int Sum(params int[] numbers)
{
int total = 0;
foreach (var n in numbers)
{
total += n;
}
return total;
}
// Usage
int result1 = Sum(1, 2, 3); // 6
int result2 = Sum(1, 2, 3, 4, 5); // 15
int result3 = Sum(); // 0
int result4 = Sum(new int[] { 1, 2 }); // Can also pass array
// params must be last parameter
public void Log(string message, params object[] args)
{
Console.WriteLine(string.Format(message, args));
}
Log("User {0} logged in at {1}", "Alice", DateTime.Now);
Optional and Named Parameters
// Optional parameters with defaults
public void CreateUser(
string name,
string email,
int age = 0,
bool isActive = true,
string? department = null)
{
// Implementation
}
// Calling with positional arguments
CreateUser("Alice", "alice@example.com", 25, true, "Engineering");
// Calling with named arguments (any order)
CreateUser(
email: "bob@example.com",
name: "Bob",
department: "Sales");
// Mix positional and named (positional must come first)
CreateUser("Charlie", "charlie@example.com", department: "HR");
// Named arguments improve readability for boolean parameters
CreateUser("Dave", "dave@example.com", isActive: false);
Expression-Bodied Members
public class Person
{
private string _firstName;
private string _lastName;
// Expression-bodied constructor
public Person(string firstName, string lastName) =>
(_firstName, _lastName) = (firstName, lastName);
// Expression-bodied method
public string GetFullName() => $"{_firstName} {_lastName}";
// Expression-bodied property (read-only)
public string Initials => $"{_firstName[0]}{_lastName[0]}";
// Expression-bodied property with getter and setter
public string FirstName
{
get => _firstName;
set => _firstName = value ?? throw new ArgumentNullException(nameof(value));
}
// Void expression-bodied method
public void PrintName() => Console.WriteLine(GetFullName());
}
Local Functions
public int Factorial(int n)
{
// Validate input
if (n < 0)
throw new ArgumentException("Must be non-negative", nameof(n));
// Local function - only accessible within this method
int Calculate(int x)
{
if (x <= 1) return 1;
return x * Calculate(x - 1);
}
return Calculate(n);
}
// Static local function (C# 8+) - cannot capture variables
public int Process(int[] numbers)
{
int total = 0;
// Static - more efficient, cannot access 'total' or 'numbers' directly
static int Square(int x) => x * x;
foreach (var n in numbers)
{
total += Square(n);
}
return total;
}
Interview Tips
Tip 1: Know that
refrequires initialization before passing,outmust be assigned in the method. This is a very common interview question.
Tip 2: Understand that reference types passed by value can still be modified - you're passing a copy of the reference, not a copy of the object.
Tip 3: Use
infor large structs to avoid copying while ensuring immutability.
Common Interview Questions
-
What's the difference between ref and out?
ref: Variable must be initialized before passing; method can read and write.out: Variable doesn't need initialization; method must assign before returning. Both pass by reference, but out is for output-only scenarios.
-
What happens when you pass a reference type to a method?
- A copy of the reference is passed. Modifying the object's properties affects the original. Reassigning the parameter to a new object only affects the local copy, not the original reference.
-
When would you use the params keyword?
- When a method should accept variable number of arguments. Common for formatting methods, logging, mathematical operations. Must be the last parameter. Consider performance for high-frequency calls.
-
What is an expression-bodied member?
- A concise syntax using
=>for single-expression methods, properties, constructors. Example:int Square(int x) => x * x;. Improves readability for simple operations.
- A concise syntax using
-
What are local functions and why use them?
- Functions declared inside methods. Benefits: encapsulation (only visible in containing method), can capture local variables, better performance than lambdas (no allocation). Use for helper logic that's only relevant locally.
-
What is the in parameter modifier?
- Passes argument by reference but read-only. Used for large structs to avoid copy while preventing modification. Compiler may pass by value if struct is small (optimization).
-
Can you overload methods that differ only by ref/out?
- No, ref and out cannot be used to distinguish overloads.
void M(ref int x)andvoid M(out int x)conflict. However,void M(int x)andvoid M(ref int x)are valid overloads.
- No, ref and out cannot be used to distinguish overloads.