Delegates, Events & Lambda Expressions

Course 2 · Ch 3
Delegates, Events & Lambda Expressions
Near-identical lambda syntax to java2-3 — hiding a genuinely different mechanism underneath

csharp2-2's LINQ lambdas worked without ever asking what a lambda actually compiles to in C#. This chapter answers that — and the answer is a real, structural departure from java2-3's own model.

Delegates — A Type-Safe Function Pointer

delegate int Calculator(int a, int b); Calculator add = (a, b) => a + b; Console.WriteLine(add(3, 4)); // 7

A delegate declares a type describing a method signature — genuinely similar in spirit to C's own function pointers, but type-checked at compile time the way a raw C function pointer never is. A Calculator variable can hold any method (or lambda) matching that exact signature.

Built-in Delegate Types — Func<> and Action<>

Func<int, int, int> add = (a, b) => a + b; // last type parameter is the return type Action<string> log = msg => Console.WriteLine(msg); // void-returning Predicate<int> isEven = n => n % 2 == 0; // bool-returning, semantic name

Almost nobody declares a custom delegate type like Calculator in real code — Func<T1, ..., TResult> and Action<T1, ...> cover nearly every shape needed, genuinely comparable to java2-3's own built-in java.util.function interfaces.

Lambdas Assigned to Delegates — The Real Mechanism

Here's the reveal, and it's a real divergence from java2-3: in Java, a lambda compiles to an anonymous implementation of a functional interface's single method — genuinely an OOP mechanism, an object with one method. In C#, a lambda assigned to a delegate compiles to something different — a real compiler-generated method (often a private static method), and the delegate variable is a type-safe reference to that method, much closer in spirit to a function pointer than to an interface implementation. The syntax at the call site looks nearly identical in both languages; the mechanism underneath genuinely isn't.

Multicast Delegates

Action<string> notify = msg => Console.WriteLine("Log: " + msg); notify += msg => Console.WriteLine("Email: " + msg); // += adds a SECOND method to the same delegate notify("Server started"); // invokes BOTH — "Log: ..." then "Email: ..."

A delegate variable can reference multiple methods at once via +=, invoking all of them in order when called — genuinely unique to C#, with no direct Java equivalent at all. This multicast capability is the exact mechanism the next feature is built on.

The event Keyword

public class Button { public event Action Clicked; // a restricted multicast delegate public void Press() { Clicked?.Invoke(); // only Button itself can invoke it directly } } Button b = new Button(); b.Clicked += () => Console.WriteLine("Clicked!"); // outside code can only += / -= b.Press();

event restricts a multicast delegate field: outside code can only +=/-= subscribers, never invoke it directly or reassign it with = — only the declaring class can. It's a genuine publish/subscribe pattern baked directly into the language, contrasted against Java's typical approach of hand-rolling a List<Listener> and manually iterating it to fire an event.

MechanismC (c2-7)Java (java2-3)C#
What it really isa raw, untyped pointer to a functionan anonymous interface implementationa type-safe reference to a method
Compile-time type checkingnone — mismatched signatures compileyes — via the functional interfaceyes — via the delegate type
Referencing multiple functions at oncenot built innot built inmulticast delegates, built in (+=)
Built-in publish/subscribenonenone — hand-rolled listener listsevent keyword
Reach for Func<>/Action<> before declaring a custom delegate type
A custom delegate declaration is worth it mainly when a genuinely descriptive name adds real clarity over Func<int, int, int> — otherwise the built-in types cover the shape just as well with less ceremony.
Invoking an event with no subscribers throws — guard it
A multicast delegate or event with nobody subscribed is null, not an empty, safely-invokable list — calling it directly throws NullReferenceException. The Clicked?.Invoke() pattern, using csharp1-3's own null-conditional operator, exists specifically to guard against this.

Coding Challenges

Challenge 1

Declare a Func<int, int, int> that multiplies its two arguments and an Action<string> that prints a message, then call both and show the results.

📄 View solution
Challenge 2

Create an Action<string> delegate variable, add two separate lambdas to it using +=, then invoke it once and show both lambdas ran in order.

📄 View solution
Challenge 3

Write a class Alarm with a public event Action Triggered and a method Sound() that raises it safely using the null-conditional operator. Demonstrate calling Sound() both with and without a subscriber attached, showing neither call throws.

📄 View solution

Chapter 3 Quick Reference

  • A delegate is a type-safe method reference — compile-time checked, unlike c2-7's raw C function pointers
  • Func<>/Action<>/Predicate<> cover nearly every shape, avoiding custom delegate declarations
  • A C# lambda compiles to a real referenced method — a different mechanism from java2-3's anonymous interface implementation, despite near-identical call-site syntax
  • Multicast delegates (+=) reference multiple methods at once, invoked in order — no Java equivalent
  • event restricts a multicast delegate to +=/-= from outside code — a built-in publish/subscribe pattern Java hand-rolls instead
  • Guard event invocation with ?.Invoke() — an unsubscribed event is null, not an empty list
  • Next chapter: async/await — first-class language syntax since C# 5, contrasted against java2-5's own concurrency tools