In the previous lesson you formalized "what an object can do" with interfaces. This lesson
solves a different problem: how to make one part of the program notify another that
something has happened — for example, that a loan has been registered — without whoever
triggers the notification needing to know in advance who will react, or how many will.
C#'s solution rests on two related pieces: delegates, variables that store a reference to
a method, and events, built on top of delegates, which formalize the
publisher/subscriber pattern. In this lesson you'll get to know both, and take the first step
toward the Library class, which in the coming lessons will organize BiblioTech's entire
catalog.
Contents
- The problem: notifying without coupling who notifies to who listens
- Delegates: variables that point to methods
- Predefined delegates:
Action,Func, andPredicate - Multicast: a delegate with several subscribed methods
- Events:
eventand the publisher/subscriber pattern eventversus a plain public delegate- Applying events in BiblioTech:
LoanRegistered
- The problem: notifying without coupling who notifies to who listens
Imagine that, when a loan is registered in BiblioTech, you want several things to happen at once: show a confirmation message, perhaps in the future send an email notification, perhaps log the loan to an audit file. The direct approach — calling each of those actions explicitly inside the method that registers the loan — works, but it strongly couples the logic of "registering a loan" with "everything that must happen afterward," and every time a new notification is added, that method has to be touched again. Delegates and events let you invert that relationship: the code that registers the loan simply "announces that something happened," without knowing or caring who (none, one, or several) is listening for that announcement.
- Delegates: variables that point to methods
A delegate is a type that represents a method's signature (its parameters and return type) and whose variables can store a reference to any method compatible with that signature:
This line declares a delegate type called ItemFilter: any method that takes a LibraryItem
and returns a bool is compatible with it. A variable of this type can point to any of those
methods:
bool IsMagazine(LibraryItem item)
{
return item is Magazine;
}
ItemFilter filter = IsMagazine; // the "filter" variable now points to the IsMagazine method
Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(filter(book1)); // False: invokes IsMagazine(book1) through the delegatefilter(book1) doesn't call any method literally named filter: it invokes, through the
delegate, whichever method filter points to at that moment (IsMagazine). This is what
makes a delegate useful: it can be passed as a parameter, stored in a variable or a field, and
changed at run time to point to a different concrete method.
- Predefined delegates:
Action, Func, and Predicate
Action, Func, and PredicateDeclaring your own delegate for every distinct signature would be repetitive. .NET offers
three families of predefined generic delegates (the generics, with <T>, are studied in
depth in the next lesson) that cover almost every common case:
| Delegate | Parameters | Return value | Example signature |
|---|---|---|---|
Action |
Zero or more (up to 16) | None (void) |
Action<Loan> → void M(Loan l) |
Func<..., TResult> |
Zero or more | The last generic type parameter | Func<LibraryItem, bool> → bool M(LibraryItem i) |
Predicate<T> |
Exactly one | bool |
Predicate<LibraryItem> → bool M(LibraryItem i) |
In practice, Predicate<T> and Func<T, bool> are equivalent (same parameters, same return
type); Predicate<T> is older and appears mostly in legacy APIs, while Func/Action are
the ones you'll see more often in modern code and in LINQ (a later lesson in this module).
With these predefined delegates, there's no longer any need to declare ItemFilter:
Func<LibraryItem, bool> isMagazine = IsMagazine; // same method, predefined delegate
Action<string> showMessage = Console.WriteLine; // an Action wrapping a method from the BCL itself
showMessage("Notice from an Action");
- Multicast: a delegate with several subscribed methods
A key feature of delegates in C# is that they are not limited to pointing to a single
method: with += you can "subscribe" more than one, and all of them run, in order, when the
delegate is invoked (this is called multicast):
Action<string> notifications = null;
notifications += message => Console.WriteLine($"[Log] {message}");
notifications += message => Console.WriteLine($"[Console] {message}");
notifications("Loan registered");
// [Log] Loan registered
// [Console] Loan registered-= removes a previously subscribed method. If an Action-typed delegate (with no return
value) is null because nobody has subscribed yet, invoking it directly would throw an
exception; that's why it's common to check first with ?.Invoke(...), as you'll see in
section 7.
- Events:
event and the publisher/subscriber pattern
event and the publisher/subscriber patternA plain public delegate has one drawback: any external code could not only subscribe, but
also replace the entire list of subscribers (with = instead of +=), or even invoke it
directly from outside, breaking the intent that only the object itself should decide when to
"notify." The event keyword solves this: it declares a delegate with stricter rules, designed
exactly for the publisher/subscriber pattern.
- The
Libraryclass is the publisher: it's the only one that can invoke the event (LoanRegistered?.Invoke(...)), always from within the class itself. - Any other code can be a subscriber: it can add (
+=) or remove (-=) a method that reacts to the event, but it cannot invoke it directly or replace the whole list of subscribers with=.
void NotifyLoan(Loan loan)
{
Console.WriteLine($"'{loan.Book.Title}' has been lent to {loan.Member.Name}");
}
Library library = new Library();
library.LoanRegistered += NotifyLoan; // valid subscription from outside the class
// library.LoanRegistered(loan1); // Compilation error: an event can't be invoked from outside
// library.LoanRegistered = NotifyLoan; // Compilation error: can't be replaced with "="
event versus a plain public delegate
event versus a plain public delegatepublic Action<Loan> Field; |
public event Action<Loan> Event; |
|
|---|---|---|
Subscribe with += from outside |
Yes | Yes |
Replace the whole list with = from outside |
Yes (risk of accidentally wiping out other subscribers) | No, compilation error |
| Invoke directly from outside the class | Yes | No, compilation error |
| Intent communicated when reading the code | Ambiguous: looks like an ordinary field | Clear: "this is a notification you can subscribe to" |
In practice, whenever the purpose is "announce that something happened" (a loan registered, a
return, an item added to the catalog), event is the right choice; a public delegate without
event is rarely justified outside very specific cases.
- Applying events in BiblioTech:
LoanRegistered
LoanRegisteredWith what you've learned, here's the first version of the Library class (a minimal version,
focused only on the event; in the next lesson, on Collections, it will be expanded with the
full catalog of items and members):
class Library
{
public event Action<Loan> LoanRegistered;
public void RegisterLoan(Loan loan)
{
Console.WriteLine($"Registering loan of '{loan.Book.Title}'...");
LoanRegistered?.Invoke(loan); // the "?." prevents an exception if nobody has subscribed
}
}A typical subscriber, thought of as a small "notification log":
void NotificationLog(Loan loan)
{
Console.WriteLine($"'{loan.Book.Title}' has been lent to {loan.Member.Name}");
}And the program that connects both pieces:
Library library = new Library();
library.LoanRegistered += NotificationLog;
Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Member member1 = new Member(1, "Ana Martinez");
book1.Lend();
Loan loan1 = new Loan(book1, member1);
library.RegisterLoan(loan1);Output:
RegisterLoan doesn't know NotificationLog, and knows nothing about it; it simply invokes
its event, and whoever has subscribed (one, several, or none) reacts. A second subscriber
could be added tomorrow — for example, one that in Module 5 writes the loan to a file —
without touching a single line of Library or RegisterLoan.
Common Mistakes and Tips
- Invoking an event without a
nullcheck: if nobody has subscribed yet,LoanRegistered.Invoke(...)(without?.) throws aNullReferenceException; always useLoanRegistered?.Invoke(...)inside the publishing class. - Trying to invoke an event from outside the class:
library.LoanRegistered(l)doesn't compile; only theLibraryclass itself can invoke its event. If you need something external to trigger the logic, expose a public method (likeRegisterLoan) that internally invokes the event. - Forgetting to unsubscribe (
-=) when it's no longer needed: if an object subscribes to an event of another, longer-lived object and never unsubscribes, the publisher keeps a reference to the subscriber indefinitely, which can prevent memory from being released (an introduction to this topic; it's revisited in more detail in Module 6, Memory Management and the GC). - Confusing the execution order of several subscribers with a strong guarantee: although in
practice they run in the order they were subscribed with
+=, you shouldn't design logic that critically depends on that order between independent subscribers. - Tip: if a method only needs to report a change, with no useful return value expected,
event Action<T>(orEventHandler<T>in more traditional .NET APIs) is almost always the right choice.
Exercises
-
Declare a
Func<LibraryItem, bool>delegate calledfilterthat points to a methodIsBookAvailable(LibraryItem item)(returnstrueif the item is aBookand isAvailable). Invoke it on two different items and show the result. -
Create a
Libraryclass with anevent Action<Loan> LoanRegisteredand a methodRegisterLoan(Loan loan)that invokes it. Subscribe two different methods to the event (one that shows a confirmation message, another that shows the member's name), and check that both run when a loan is registered. -
Add a third subscriber to the previous exercise's solution, then remove it with
-=before registering a second loan. Check that, on the second loan, that third subscriber no longer runs.
Solutions
bool IsBookAvailable(LibraryItem item)
{
return item is Book && item.Available;
}
Func<LibraryItem, bool> filter = IsBookAvailable;
Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Magazine magazine1 = new Magazine("National Geographic", "Various authors", 302);
Console.WriteLine(filter(book1)); // True
Console.WriteLine(filter(magazine1)); // False: not a Book
class Library
{
public event Action<Loan> LoanRegistered;
public void RegisterLoan(Loan loan)
{
LoanRegistered?.Invoke(loan);
}
}
void Confirmation(Loan l) => Console.WriteLine($"Loan of '{l.Book.Title}' confirmed.");
void ShowMember(Loan l) => Console.WriteLine($"Member: {l.Member.Name}");
Library library = new Library();
library.LoanRegistered += Confirmation;
library.LoanRegistered += ShowMember;
Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Member member1 = new Member(1, "Ana Martinez");
book1.Lend();
library.RegisterLoan(new Loan(book1, member1));
// Loan of 'Hopscotch' confirmed.
// Member: Ana Martinez
void TemporaryNotice(Loan l) => Console.WriteLine("Temporary test notice");
library.LoanRegistered += TemporaryNotice;
// ... all three subscribers would run on the next loan ...
library.LoanRegistered -= TemporaryNotice; // removed before the second loan
Book book2 = new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
Member member2 = new Member(2, "Luis Gomez");
book2.Lend();
library.RegisterLoan(new Loan(book2, member2));
// Only Confirmation and ShowMember run; TemporaryNotice no longer runs.
Conclusion
In this lesson you've learned what a delegate is, how to use the predefined Action, Func,
and Predicate, how the same delegate can have several methods subscribed at once
(multicast), and how event restricts that capability to the publisher/subscriber pattern,
preventing improper invocations or replacements from outside the class. The Library class
has been born, with its LoanRegistered event, which in the coming lessons of this module
will grow into the central point of BiblioTech's entire catalog.
Delegates and events solve "how to react to something that already happened." The next lesson
switches to another fundamental part of modern C#: pattern matching, which will let you
express, much more compactly and readably, the type checks (is, as) you already used in
the Polymorphism lesson, along with switch expressions and nullable reference types.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
