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;
}

When to Use vs. When to Avoid

| Scenario | Use It? | Why | |----------|---------|-----| | Need to modify caller's variable | ✅ ref | Pass by reference allows modification | | Returning multiple values | ✅ out | Cleaner than tuple for Try* patterns | | Large struct parameter | ✅ in | Avoids copying while ensuring read-only | | Variable number of arguments | ✅ params | Convenient API, but has allocation overhead | | Boolean parameters | ⚠️ Named args | DoSomething(includeDetails: true) is more readable | | Many optional parameters | ❌ Consider builder | Too many optionals makes API confusing |

Common Patterns

TryParse Pattern

Use out parameter to return success/failure with the parsed value.

public static bool TryParseEmail(string input, out string email)
{
    email = string.Empty;
    if (string.IsNullOrWhiteSpace(input) || !input.Contains('@'))
        return false;

    email = input.Trim().ToLowerInvariant();
    return true;
}

// Usage
if (TryParseEmail(userInput, out var email))
    Console.WriteLine($"Valid: {email}");

Fluent Method Chaining

Return this for chainable configuration methods.

public class RequestBuilder
{
    private string _url;
    private int _timeout = 30;

    public RequestBuilder WithUrl(string url) { _url = url; return this; }
    public RequestBuilder WithTimeout(int seconds) { _timeout = seconds; return this; }
    public Request Build() => new Request(_url, _timeout);
}

// Usage
var request = new RequestBuilder()
    .WithUrl("https://api.example.com")
    .WithTimeout(60)
    .Build();

Common Mistakes

Mistake: Forgetting ref at Call Site

// ❌ Bad - missing ref keyword
int value = 5;
Increment(value);  // Compiler error or wrong overload called

// ✅ Good - ref at both declaration and call site
void Increment(ref int x) => x++;
int value = 5;
Increment(ref value);  // value is now 6

Why: C# requires ref/out at both the method signature and call site to make pass-by-reference explicit and intentional.

Mistake: Ignoring out Parameter

// ❌ Bad - ignoring returned value
int.TryParse(input, out int result);
// result might be 0 due to failed parse

// ✅ Good - check the boolean return
if (int.TryParse(input, out int result))
    Console.WriteLine(result);
else
    Console.WriteLine("Invalid input");

Why: The out parameter is set even on failure (usually to default). Always check the return value.

Practical Example

Scenario: Creating a method that validates and normalizes user input.

public static class InputValidator
{
    public static bool TryNormalizeName(
        string? input,
        out string normalizedName,
        int maxLength = 50)
    {
        normalizedName = string.Empty;

        if (string.IsNullOrWhiteSpace(input))
            return false;

        var trimmed = input.Trim();
        if (trimmed.Length > maxLength)
            return false;

        // Capitalize first letter of each word
        normalizedName = CultureInfo.CurrentCulture.TextInfo
            .ToTitleCase(trimmed.ToLowerInvariant());
        return true;
    }
}

// Usage
if (InputValidator.TryNormalizeName(userInput, out var name))
    user.Name = name;

Related Topics

  • Delegates and Events: Methods can be passed as delegates; understanding signatures is key
  • Extension Methods: Special static methods that appear as instance methods
  • Async/Await: Async methods have special parameter and return type rules

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.