Methods

Intro

Methods are the core unit of behavior in C#: they define contracts, shape API boundaries, and express how data flows through a system. Parameter modifiers like ref, in, and params are not just syntax details — they directly affect mutability, copying/allocation behavior, and performance characteristics at call sites. A misplaced in on a 4-byte int adds overhead instead of saving it (the runtime passes a pointer plus a defensive copy), while a missing ref on a 128-byte Matrix4x4 silently copies 128 bytes per call in a hot rendering loop. Dispatch keywords like virtual, override, and new determine whether behavior is polymorphic at runtime or resolved by compile-time type — getting this wrong creates bugs where the "right" method runs for the wrong variable type, invisible until you upcast.

Input Parameters

ref

ref passes a variable by reference.

Even for reference types, ref is useful when you want to change the reference itself (not just mutate the object it points to):

class MyClass {}

static void ModifyReference(ref MyClass obj)
{
    obj = new MyClass();
}

var myObj = new MyClass();
ModifyReference(ref myObj);

ref is also useful for value types when you need the callee to update the caller's variable:

static void InitializeAndModify(ref int value)
{
    value = 10;
}

int num = 0;
InitializeAndModify(ref num);

in

in is a readonly by-ref parameter.

static void ProcessData(in int value)
{
    // value = 10; // Compile-time error
    Console.WriteLine(value);
}

params

params lets a method accept a variable number of arguments as an array (or, since C# 13, recognized collection types).

static int Sum(params int[] numbers)
{
    int total = 0;
    foreach (var n in numbers)
        total += n;
    return total;
}

Sum(1, 2, 3);   // 6
Sum();           // 0
Sum(new[] { 4, 5 }); // 9 — explicit array also works

C# 13 extends params beyond arrays to recognized collection types such as Span<T>, ReadOnlySpan<T>, and IEnumerable<T>. This can reduce allocations in some call patterns (especially span-based calls), but allocation behavior still depends on the target type and call form:

static int Sum(params ReadOnlySpan<int> numbers)
{
    int total = 0;
    foreach (var n in numbers)
        total += n;
    return total;
}

Inheritance Method Keywords

virtual

virtual marks a base-class method as overridable.

class Animal
{
    public virtual string Speak() => "...";
}

override

override replaces a virtual/abstract member implementation in a derived class.

class Dog : Animal
{
    public override string Speak() => "Woof";
}

new

new hides a member from the base class (it does not override it).

class Animal
{
    public string Category() => "Animal";
}

class Dog : Animal
{
    public new string Category() => "Dog";
}

virtual vs override vs new in one example

class Animal
{
    public virtual string Speak() => "...";
    public string Category() => "Animal";
}

class Dog : Animal
{
    public override string Speak() => "Woof";
    public new string Category() => "Dog";
}

Animal asAnimal = new Dog();
Dog asDog = new Dog();

Console.WriteLine(asAnimal.Speak());    // Woof (runtime dispatch)
Console.WriteLine(asDog.Speak());       // Woof
Console.WriteLine(asAnimal.Category()); // Animal (member hiding)
Console.WriteLine(asDog.Category());    // Dog

override participates in polymorphism; new does not.

Pitfalls

Tradeoffs

Decision Option A Option B When A When B
By-value vs in By-value (copies the argument) in (readonly reference) Small types (≤16 bytes: int, Guid, small structs) — copy is cheaper than indirection Large structs (>16 bytes: Matrix4x4, decimal-heavy DTOs) — avoids 128+ byte copies on hot paths
in vs ref in (readonly reference) ref (mutable reference) Callee only reads — communicates intent, compiler enforces immutability Callee must mutate or rebind — e.g., TryParse patterns, swap utilities
override vs new override (runtime polymorphism) new (compile-time hiding) Almost always — predictable dispatch, works through base-type references Rare: deliberate compile-time-only behavior split, documented API hiding for compatibility
params T[] vs params ReadOnlySpan<T> params T[] (heap array per call) params ReadOnlySpan<T> (stack or inline buffer) Pre-C# 13 code, or when caller needs to store the array C# 13+, hot paths where allocation pressure matters — span-based avoids the heap allocation

Decision rule: default to by-value for types ≤16 bytes and override for all polymorphic methods. Introduce in only when profiling shows copy cost matters (typically structs >16 bytes called >10K/sec). Use new only when you own both types and the behavior split is documented in XML comments.

Questions


Whats next