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.
- The caller must initialize the variable.
- The method can read and write the variable.
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.
- The caller must initialize the argument.
- The method can read, but cannot assign to the parameter.
- Often used to avoid copying large structs while making intent explicit.
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).
- Must be the last parameter in the method signature.
- The caller can pass individual arguments, an array, or nothing at all.
- Expanded-form calls (for example,
Sum(1, 2, 3)) construct theparamscollection at the call site; passing an existing collection can avoid extra construction.
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.
- Enables runtime polymorphism.
- Calls are resolved by the runtime type, not just the variable type.
class Animal
{
public virtual string Speak() => "...";
}
override
override replaces a virtual/abstract member implementation in a derived class.
- Signature must match the base member.
- You can still call the base implementation with
base.MethodName().
class Dog : Animal
{
public override string Speak() => "Woof";
}
new
new hides a member from the base class (it does not override it).
- Behavior depends on the compile-time type of the variable.
- Use intentionally when you want member hiding, not polymorphism.
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
- Optional parameter defaults are substituted at the call site during compilation, so changing a default value in a shared library does not update already compiled consumers; this can create silent behavior drift across services. Prefer explicit overloads for public APIs and treat default-value changes as breaking behavior.
inparameters are readonly by reference, but the compiler can introduce temporaries for some argument forms or conversions, which means expected copy-avoidance may not happen. Benchmark hot paths, and if you must minimize temporaries, design APIs around stricter by-ref calling patterns (for example,ref readonlyparameters) and avoid argument expressions that require conversions.- Member hiding with
newis resolved by compile-time type, so callers can observe different results for the same runtime object depending on reference type. Prefervirtual/overridewhen polymorphism is intended, and avoidnewon public APIs unless the behavior split is deliberate and documented.
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
ref for reference types if reference types are already passed by reference?
Reference type values (the reference) are passed by value. ref is needed when you want the callee to replace the caller's reference (rebind it to a different object).
in parameter used for?To pass an argument by readonly reference: avoid copies for large structs and communicate that the method should not modify the argument.
Parameters with default values that callers can omit; the default is substituted at compile time at the call site.
Animal a = new Dog();, how can you make a.Category() return the derived value, and what does that imply for API design?
Mark the base member as virtual and the derived member as override. That enables polymorphic dispatch by runtime type. It also means the base API explicitly supports extensibility and behavioral substitution.
new over override?
Prefer new only when polymorphism is not desired and you intentionally want different behavior based on the compile-time reference type (for compatibility or specialized APIs). In most extensible designs, override is the safer default.
virtual, but you need derived-specific behavior. What are your options?
Option 1: Change the base API to virtual (best if you own the base type and want polymorphism). Option 2: Hide with new (works, but no polymorphism and can confuse callers). Option 3: Redesign with composition/strategy if inheritance is not a good fit.
Links
- Method parameters and modifiers (ref/in/out) — official reference for all parameter modifiers with semantics and examples.
- C# 13: params collections — explains the C# 13 extension of
paramsto any collection type, not just arrays. - C# 13: calling methods is easier and faster — .NET team blog post on the performance and ergonomics improvements to method calls in C# 13.