Methods and delegates
Methods are the principal callable construct in C#: free functions are not admitted (every method belongs to a type), though static methods on static classes serve essentially the same role. The language provides delegates (named function-pointer types), lambda expressions (anonymous function literals), and events (a multicast subscription mechanism); the combination is the C# substitute for first-class functions and is the substrate on which LINQ, async, and event-driven programming are built. Each subsequent revision of the language has refined the function surface — async methods, expression-bodied members, local functions, generic delegates — and the conventions for using them are part of the language’s idiom.
Method declaration
A method declaration combines an access modifier, optional static, optional other modifiers, a return type, a name, a parameter list, and a body:
public class Counter {
private int count = 0;
public void Increment() {
count++;
}
public int Value() {
return count;
}
public static int Compare(Counter a, Counter b) {
return a.count - b.count;
}
}
Members and access modifiers are treated in Classes, structs, and records. The principal forms of method declaration are familiar from C++ and Java; the C#-specific additions are documented below.
Expression-bodied members
C# 6 introduced expression-bodied members: a single-expression alternative to the block body:
public int Value() => count;
public int Square(int n) => n * n;
public string Name => $"counter-{count}"; // expression-bodied property
The form is conventional for short methods whose body is a single expression. C# 7 extended it to constructors, finalizers, and accessors:
public class Holder {
private int value;
public Holder(int initial) => value = initial;
public int Value { get => value; set => this.value = value; }
}
Parameter modifiers
C# admits four parameter modifiers beyond the default by-value:
| Modifier | Direction | Notes |
|---|---|---|
ref | bidirectional | Caller’s variable must be initialised; method may read and write |
out | output-only | Caller’s variable need not be initialised; method must assign |
in | read-only by reference | Caller’s variable read-only by the method; admits efficient struct passing |
params | variadic | Last parameter; accepts any number of arguments |
void Modify(ref int n) { n = n + 1; }
void Produce(out int result) { result = 42; }
double Dot(in Vector3 a, in Vector3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;
double Sum(params int[] values) => values.Sum();
At the call site, ref and out arguments must be marked:
int n = 5;
Modify(ref n); // n becomes 6
Produce(out int r); // r is bound here (declaration-and-assignment)
double total = Sum(1, 2, 3, 4, 5); // params: passed as int[] { 1, 2, 3, 4, 5 }
The in modifier is invisible at the call site (the call looks like a normal by-value call), reflecting its semantic role as an optimisation rather than a contract.
C# 7 admitted declaration in argument: Produce(out int r) simultaneously declares r and passes it as an out argument. The form is the conventional contemporary syntax.
Default arguments
C# admits default values for parameters:
public void Connect(string host, int port = 80, int timeoutMs = 5000) {
/* ... */
}
Connect("example.com"); // port=80, timeoutMs=5000
Connect("example.com", 443); // port=443, timeoutMs=5000
Connect("example.com", 443, 10000); // all explicit
Defaults are at trailing parameters; you cannot have a default in the middle of the list except by using named arguments:
Connect("example.com", timeoutMs: 10000); // port defaults; timeoutMs by name
Named arguments are conventional when the call site has many parameters or when the meaning of a positional argument is non-obvious:
SetWindow(width: 800, height: 600, fullscreen: false, frameless: true);
Method overloading
Two or more methods in the same scope may share a name as long as they differ in the signature — the parameter types or the number of parameters:
void Print(int n);
void Print(double d);
void Print(string s);
void Print(int n, int width);
Print(42); // int overload
Print(3.14); // double overload
Print("hello"); // string overload
Print(42, 5); // (int, int) overload
Overload resolution selects the best match. The standard defines an elaborate ranking — exact match, derived-to-base, implicit conversion, generic specialisation — that selects the candidate requiring the fewest conversions. Ambiguity is a compile-time error.
Methods cannot differ only in:
- Their return type.
- The presence of
params. - The presence of
refversusout(these do differ for overload resolution, butindoes not always).
Local functions
C# 7 introduced local functions: methods declared inside another method:
public IEnumerable<int> Squares(int n) {
return Range().Select(Square);
IEnumerable<int> Range() {
for (int i = 0; i < n; i++) yield return i;
}
static int Square(int x) => x * x; // C# 8: static local function
}
Local functions:
- Have access to the enclosing method’s locals (and parameters), like a closure but without allocating a delegate.
- Are scoped to the enclosing method.
- May be recursive.
- May be declared
static(C# 8) to prevent capture of locals; a static local function is required to be self-contained.
The conventional uses are: helper functions used only within one method; iterators that need an outer entry-point method to validate parameters before the deferred execution begins; recursive helpers.
Lambda expressions
A lambda is an anonymous function:
Func<int, int> square = x => x * x;
Func<int, int, int> add = (a, b) => a + b;
Action<string> print = s => Console.WriteLine(s);
Action hello = () => Console.WriteLine("hello");
var values = new[] { 1, 2, 3, 4, 5 };
var doubled = values.Select(x => x * 2);
The lambda syntax has two forms:
- Expression body:
params => expression. Returns the value of the expression. - Statement body:
params => { statements }. May contain multiple statements; usesreturnto produce a value.
The parameter list:
- Single untyped parameter:
x => …(parentheses omittable). - Multiple or typed parameters:
(int x, int y) => …or(x, y) => …. - No parameters:
() => ….
Since C# 9, lambdas may be static (preventing capture of enclosing locals) and may declare attributes (C# 10):
Func<int, int> identity = static x => x; // no capture
C# 10 admitted explicit return-type declarations on lambdas:
var f = int (int x) => x * 2; // explicit return type
Captures and closures
A lambda may capture variables from the enclosing scope:
public Func<int, int> Adder(int delta) {
return x => x + delta; // captures `delta`
}
var add5 = Adder(5);
var n = add5(10); // 15
Captures are by reference in C#: the lambda holds a reference to the enclosing variable, not a copy. Modifying the captured variable in the enclosing scope is observable from the lambda:
int total = 0;
Action add = () => total++; // captures `total` by reference
add(); add(); add();
Console.WriteLine(total); // 3
The mechanism interacts with loop variables in subtle ways. The conventional advice:
- Be cautious capturing loop variables; the last-iteration’s value is what each lambda sees, unless a fresh local is introduced inside the loop.
- Use
staticlambdas to prevent unintended capture and to make the absence-of-capture explicit.
Anonymous methods
C# 2 introduced anonymous methods with the delegate keyword:
Func<int, int> square = delegate (int x) { return x * x; };
Lambdas (C# 3) supersede anonymous methods for nearly every use; anonymous methods remain in the language for backward compatibility but are rare in modern code.
Delegates
A delegate is a type that represents a method signature. Delegates are reference types; instances hold a reference to a method (free or instance) and may be invoked, copied, and combined.
Custom delegate types
public delegate int Comparer(string a, string b);
public delegate void EventHandler(object sender, EventArgs e);
public delegate bool Predicate<T>(T item);
The declaration introduces a new type. Instances may be created by:
- Method group conversion:
Comparer cmp = MyCompare;(whereMyCompareis a method). - Lambda:
Comparer cmp = (a, b) => string.Compare(a, b);. - Anonymous method:
Comparer cmp = delegate (string a, string b) { return ... };.
The standard generic delegates
The .NET BCL provides generic delegate types that obviate most custom delegate declarations:
| Delegate | Signature |
|---|---|
Action | void() |
Action<T> | void(T) |
Action<T1, T2> | void(T1, T2) |
Action<T1, ..., T16> | up to 16 parameters |
Func<TResult> | TResult() |
Func<T, TResult> | TResult(T) |
Func<T1, T2, TResult> | TResult(T1, T2) |
Func<T1, ..., T16, TResult> | up to 16 parameters, returns TResult |
Predicate<T> | bool(T) |
Comparison<T> | int(T, T) |
The conventional contemporary choice is to use these generic delegates rather than to declare custom ones; custom delegates are appropriate when the signature is recurring and the type carries documentation value.
Func<int, int> square = x => x * x;
Action<string> log = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;
Comparison<string> cmp = (a, b) => a.Length - b.Length;
Multicast delegates
A delegate instance may hold several method references; invoking the delegate invokes each:
Action notifier = () => Console.WriteLine("first");
notifier += () => Console.WriteLine("second");
notifier(); // prints both
The += operator combines two delegates; -= removes one. The mechanism is the foundation of events.
Events
An event is a member that admits subscription and invocation:
public class Button {
public event EventHandler? Click;
public void RaiseClick() {
Click?.Invoke(this, EventArgs.Empty);
}
}
var button = new Button();
button.Click += (sender, e) => Console.WriteLine("clicked");
button.RaiseClick();
The event keyword declares a member that:
- Outside the declaring class, may only have
+=and-=applied to it (subscribers cannot invoke or replace). - Inside the declaring class, behaves like an ordinary delegate field (may be invoked).
The ?.Invoke pattern is conventional: if no subscribers are registered, the underlying delegate is null and the call short-circuits.
C# also admits custom event accessors that override the default +=/-= behaviour, but the simple form above is the standard.
Extension methods
An extension method is a static method that may be invoked as if it were an instance method on the type of its first parameter:
public static class StringExtensions {
public static bool IsNullOrWhitespace(this string? s) {
return string.IsNullOrWhiteSpace(s);
}
}
bool b = "hello".IsNullOrWhitespace(); // syntactic sugar for StringExtensions.IsNullOrWhitespace("hello")
Extension methods:
- Are declared as
staticmethods in astaticclass. - The first parameter is preceded by
this. - Can be invoked as instance methods at the call site, provided the static class is in scope (
usingdirective in the calling file). - Do not actually modify the type they extend; they are syntactic sugar over a static call.
The principal use is LINQ: every standard query operator (Where, Select, ToList, etc.) is an extension method on IEnumerable<T>. The mechanism is also conventional for utility methods that conceptually operate on a type but cannot be added to the type directly.
async methods
A method declared async may use await expressions to await the completion of asynchronous operations:
public async Task<string> FetchAsync(string url) {
using var client = new HttpClient();
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
string result = await FetchAsync("https://example.com");
The full treatment is in Async and concurrency. The principal points:
asyncdoes not start the work on a different thread; it admits suspension atawaitpoints.- The method’s return type is
Task,Task<T>,ValueTask,ValueTask<T>, orvoid(for event handlers; rare). - The compiler synthesises a state machine; each
awaitproduces a continuation.
Specifiers
Several specifiers refine method declarations:
| Specifier | Effect |
|---|---|
static | Member belongs to the type, not to instances. |
virtual | Admits override in derived classes. |
override | Overrides a base virtual or abstract member. |
sealed | (Method) may not be further overridden. |
abstract | (Method) has no body; the type must be abstract. |
new | (Method) hides an inherited member rather than overriding. |
extern | Body provided by an external library (typically P/Invoke). |
partial | (Method) may have a separate definition. |
async | Admits await expressions. |
unsafe | Admits pointer manipulation. |
[DllImport("kernel32.dll")]
public static extern uint GetCurrentProcessId();
public partial class Generator {
public partial void OnGenerated();
}
public partial class Generator {
public partial void OnGenerated() {
// implementation
}
}
A note on what C# does not have
The features that some other languages provide and their status in C#:
| Feature | Available? |
|---|---|
| First-class free functions | No. Static methods on static classes are the substitute. |
| Nested functions | Yes (local functions). |
| Function composition | Manual. Define Func<A, C> Compose<A, B, C>(Func<A, B> f, Func<B, C> g). |
| Currying | Manual. Common in functional libraries. |
| Multiple return values | Through tuples (preferred) or out parameters. |
| Named and default arguments | Yes. |
| Variadic | Yes (params). |
| Generic methods | Yes. Treated in Generics. |
| Operator overloading | Yes. |
| Default interface methods | Yes (since C# 8). |
The combination — instance and static methods, lambdas, delegates, events, extension methods, async, generics — covers most of the practical functional and method-based programming patterns. Custom delegates are rare in modern code; the standard generic delegates suffice for nearly every case.