C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
45: Delegates Explained
If you've looked at the C# documentation for delegates, you've probably seen them described as "type-safe function pointers." That's technically true, but it's a textbook definition that doesn't actually tell you why you'd use one. In plain English: a delegate is a way to treat a method like a variable. You can pass a method into another method, store it in a list, or swap it out at runtime.
To make this click, let's build a simple notification system. Imagine we're writing a `UserAccount` service. When a password is changed, we need to notify the user. But here's the catch: some users want an email, some want an SMS, and some might want a push notification. I don't want my `AccountService` to have a giant if/else block checking user preferences every time a password changes.
The problem with hardcoded logic
Initially, I might have written something like this. It's simple, but it's rigid:
public class AccountService
{
public void ChangePassword(string newPassword)
{
// ... logic to update password in DB ...
Console.WriteLine("Password updated in database.");
// Hardcoded notification
SendEmailNotification("Your password was changed!");
}
private void SendEmailNotification(string message)
{
Console.WriteLine($"Email sent: {message}");
}
}
The problem is that ChangePassword is now married to SendEmailNotification. If I want to add SMS support, I have to modify the AccountService class. That violates the Open/Closed Principle—we should be able to add new notification methods without touching the core business logic.
Defining our function contract
This is where the delegate comes in. Think of a delegate as a "contract" for a method. I'm telling C#, "I don't care which method is called, as long as it returns void and takes a single string as an argument."
// This defines the "shape" of the method we want to pass around
public delegate void NotificationHandler(string message);
public class AccountService
{
public void ChangePassword(string newPassword, NotificationHandler notifyMethod)
{
// ... logic to update password ...
Console.WriteLine("Password updated in database.");
// We call the delegate here, not a specific method
notifyMethod("Your password was changed!");
}
}
Now, the AccountService doesn't know how the notification happens; it just knows it has a NotificationHandler it can trigger.
The signature mismatch headache
Here is where I usually trip up when I'm first prototyping these. I tried to add a logging method to my system to track when notifications were sent, but I forgot that delegates are incredibly strict about signatures.
public class NotificationUtils
{
public static void SendSms(string msg) => Console.WriteLine($"SMS: {msg}");
public static void LogToDisk(string msg, DateTime timestamp) => Console.WriteLine($"Logged {msg} at {timestamp}");
}
// ... inside Main ...
AccountService service = new AccountService();
// This works fine
service.ChangePassword("Secret123", NotificationUtils.SendSms);
// I tried this, and the compiler screamed at me:
service.ChangePassword("Secret123", NotificationUtils.LogToDisk);
The error is essentially saying: "Cannot convert method 'LogToDisk' to delegate 'NotificationHandler'." Why? Because LogToDisk requires two arguments (string and DateTime), but our NotificationHandler delegate only allows one. A delegate isn't just a pointer; it's a type. If the signatures don't match exactly, it won't compile. To fix this, I had to wrap the logging call in a method that matched the delegate's signature or use a lambda expression.
Stacking notifications with multicast
One of the coolest things about delegates is that they can be "multicast." This means a single delegate variable can actually hold a list of multiple methods and call them all in sequence.
NotificationHandler notifications = NotificationUtils.SendSms;
notifications += NotificationUtils.SendEmail; // Imagine this method exists
// Now, when I pass 'notifications' to the service,
// BOTH the SMS and Email methods will be executed.
service.ChangePassword("NewPass456", notifications);
By using the += operator, I've created a chain of events. The AccountService still thinks it's just calling one method, but C# is actually iterating through the internal list of methods attached to that delegate. It's a lightweight way to implement the Observer pattern without building a complex event system from scratch.
📋 Practical Task
Exercise: Building a Custom Data Filter Pipeline
You are building a data processing tool that takes a list of strings (raw logs) and applies various filters to them before printing them. Instead of hardcoding the filters, you will use delegates to make the pipeline customizable.
Your Requirements:
- Define a delegate named
LogFilterthat takes astringand returns abool(true if the log should be kept, false if it should be discarded). - Create a class
LogProcessorwith a methodProcessLogs(List<string> logs, LogFilter filter). This method should iterate through the list and only print the strings that thefilterdelegate returnstruefor. - Write two separate static methods:
IsError(returns true if the string contains the word "ERROR") andIsWarning(returns true if the string contains "WARN"). - In your
Mainmethod, callProcessLogstwice: once passing theIsErrormethod and once passing theIsWarningmethod. - Bonus: Try using a lambda expression as the filter to only keep logs that are longer than 10 characters.
There are no comments for now.