Polyglot
Languages C# functions
C# § functions

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:

ModifierDirectionNotes
refbidirectionalCaller’s variable must be initialised; method may read and write
outoutput-onlyCaller’s variable need not be initialised; method must assign
inread-only by referenceCaller’s variable read-only by the method; admits efficient struct passing
paramsvariadicLast 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 ref versus out (these do differ for overload resolution, but in does 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:

  1. Expression body: params => expression. Returns the value of the expression.
  2. Statement body: params => { statements }. May contain multiple statements; uses return to 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 static lambdas 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; (where MyCompare is 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:

DelegateSignature
Actionvoid()
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 static methods in a static class.
  • The first parameter is preceded by this.
  • Can be invoked as instance methods at the call site, provided the static class is in scope (using directive 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:

  • async does not start the work on a different thread; it admits suspension at await points.
  • The method’s return type is Task, Task<T>, ValueTask, ValueTask<T>, or void (for event handlers; rare).
  • The compiler synthesises a state machine; each await produces a continuation.

Specifiers

Several specifiers refine method declarations:

SpecifierEffect
staticMember belongs to the type, not to instances.
virtualAdmits override in derived classes.
overrideOverrides 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.
externBody provided by an external library (typically P/Invoke).
partial(Method) may have a separate definition.
asyncAdmits await expressions.
unsafeAdmits 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#:

FeatureAvailable?
First-class free functionsNo. Static methods on static classes are the substitute.
Nested functionsYes (local functions).
Function compositionManual. Define Func<A, C> Compose<A, B, C>(Func<A, B> f, Func<B, C> g).
CurryingManual. Common in functional libraries.
Multiple return valuesThrough tuples (preferred) or out parameters.
Named and default argumentsYes.
VariadicYes (params).
Generic methodsYes. Treated in Generics.
Operator overloadingYes.
Default interface methodsYes (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.