Site

Delegates — C# Action, Func, and Lambda

Tutorial S7  •  C# / Learn

S7.0 What This Teaches

This tutorial covers delegates and functional programming in C#:

S7.1 Delegate Basics

A delegate is a type that holds a reference to a method with a matching signature. You can invoke a delegate like calling the method directly:
// Declare a delegate type
delegate int MathOp(int a, int b);

// Assign a method
MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;

Console.WriteLine(add(3, 4));   // 7
Console.WriteLine(mul(3, 4));   // 12

// Store and pass as argument
static int Apply(int x, int y, MathOp op) => op(x, y);
Console.WriteLine(Apply(5, 6, add));   // 11

S7.2 Action and Func

The BCL provides generic delegate types so you rarely need to declare your own:
// Action<T> - takes arguments, returns void
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice");

// Func<T, TResult> - takes arguments, returns TResult
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 4));   // 7

// Predicate<T> - returns bool (shorthand for Func<T, bool>)
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4));   // True

// Higher-order function using Func
static List<T> Filter<T>(List<T> items, Func<T, bool> predicate) =>
    items.Where(predicate).ToList();

S7.3 Lambda Expressions

// Expression lambda - single expression, implicit return
Func<int, int> square = x => x * x;

// Statement lambda - multiple statements in braces
Func<int, int> safeSqrt = x =>
{
    if (x < 0) return 0;
    return (int)Math.Sqrt(x);
};

// No parameters
Action printTime = () => Console.WriteLine(DateTime.Now);

// Multiple parameters
Func<int, int, int> clamp = (value, max) => Math.Min(value, max);

S7.4 Closures

A lambda can capture variables from its enclosing scope. The captured variable is shared - if it changes, the lambda sees the new value:
int multiplier = 3;
Func<int, int> triple = x => x * multiplier;
Console.WriteLine(triple(5));   // 15

multiplier = 10;
Console.WriteLine(triple(5));   // 50 - sees the changed value!

// Factory function returning a closure
static Func<int, int> MakeAdder(int delta) => x => x + delta;

var addFive = MakeAdder(5);
var addTen  = MakeAdder(10);
Console.WriteLine(addFive(3));  // 8
Console.WriteLine(addTen(3));   // 13

S7.5 Multicast Delegates

A delegate variable can hold references to multiple methods. Using += adds a method to the invocation list:
Action<string> log = msg => Console.WriteLine($"[Log] {msg}");
Action<string> alert = msg => Console.WriteLine($"[ALERT] {msg}");

Action<string> notify = log;
notify += alert;   // both will be called

notify("Server started");
// [Log] Server started
// [ALERT] Server started

notify -= log;   // remove from invocation list
Events in C# are built on multicast delegates. The publisher calls the delegate and all subscribers receive the notification.

S7.6 Example - All Together

// Delegates - pipeline of transformation steps with Func composition.

var pipeline = Compose<string, string>(
    s => s.Trim(),
    s => s.ToLower(),
    s => s.Replace(" ", "_")
);

string[] inputs = { "  Hello World  ", " Foo Bar ", "  C Sharp  " };
foreach (string s in inputs)
    Console.WriteLine(pipeline(s));

static Func<T, T> Compose<T, T2>(params Func<T, T>[] steps)
    where T2 : T
{
    return input => steps.Aggregate(input, (acc, f) => f(acc));
}
Expected output:
hello_world
foo_bar
c_sharp

S7.7 Exercise

Exercise
  • Write a Memoize<T, TResult>(Func<T, TResult> f) method that caches previously computed results in a dictionary and returns a new Func<T, TResult>. Test it with a slow function.
  • Use an Action<string> multicast delegate to create a simple logging system with Console and file handlers.
  • Write a higher-order function Retry(Action action, int times) that calls action up to times times, catching exceptions on each attempt.

S7.8 Common Mistakes

Captured loop variable

var actions = new List<Action>();
for (int i = 0; i < 3; i++)
    actions.Add(() => Console.WriteLine(i));  // captures i, not a copy

actions.ForEach(a => a());   // prints 3 3 3 - all see i=3!

// Fix: capture a local copy
for (int i = 0; i < 3; i++)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));  // prints 0 1 2
}

Null delegate invocation

Action<string> handler = null;
handler("test");   // NullReferenceException
handler?.Invoke("test");   // safe - no-ops if null

S7.9 Key Terms

TermMeaning
delegateType representing a method reference with a specific signature
Action<T>Built-in delegate that takes parameters and returns void
Func<T, TResult>Built-in delegate that takes parameters and returns TResult
Predicate<T>Shorthand for Func<T, bool>
lambda expression(params) => body - anonymous method assigned to a delegate
closureLambda that captures variables from its enclosing scope
multicast delegateDelegate holding multiple method references; all called on invoke
higher-order functionFunction that takes or returns another function