Methods and Parameters

Master C# methods including parameter passing, ref/out/in keywords, optional parameters, and expression-bodied members

C#
Methods
Parameters
Fundamentals
20 min read

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 ref requires initialization before passing, out must 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 in for large structs to avoid copying while ensuring immutability.

Common Interview Questions

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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).
  7. 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) and void M(out int x) conflict. However, void M(int x) and void M(ref int x) are valid overloads.