What are delegates, events, and how do they differ?

10 minintermediate.NETdelegatesevents

Quick Answer

Delegates are type-safe function pointers that can be called directly and support multicast. Events are special delegates with restrictions - they can only be invoked from within the declaring class and only support += and -= operations. Events provide better encapsulation and implement the publisher-subscriber pattern safely.

Detailed Answer

Delegates:

  • Type-safe function pointers
  • Reference type that holds references to methods
  • Can point to any method with matching signature
  • Supports multicast (calling multiple methods)
  • Can be called directly by any code that has access

Events:

  • Built on top of delegates
  • Special kind of delegate with restrictions
  • Provides publisher-subscriber pattern
  • Can only be invoked from within the declaring class
  • Add/remove methods only (cannot be assigned from outside)

Delegate Example:

// Define delegate type
public delegate void NotificationHandler(string message);

public class Example
{
    // Delegate field - can be called and assigned from anywhere
    public NotificationHandler OnNotification;
    
    public void SendNotification(string msg)
    {
        // Anyone can call this delegate
        OnNotification?.Invoke(msg);
    }
}

// Usage
Example example = new Example();

// Problem: Can be assigned from outside, replacing all subscribers!
example.OnNotification = (msg) => Console.WriteLine($"Handler 1: {msg}");

// Problem: Can be called from outside the class!
example.OnNotification?.Invoke("Direct call");

Event Example:

// Define delegate type
public delegate void NotificationHandler(string message);

public class Example
{
    // Event - restricted delegate
    public event NotificationHandler OnNotification;
    
    public void SendNotification(string msg)
    {
        // Only the class itself can invoke the event
        OnNotification?.Invoke(msg);
    }
}

// Usage
Example example = new Example();

// Can only add/remove handlers (+=, -=)
example.OnNotification += (msg) => Console.WriteLine($"Handler 1: {msg}");
example.OnNotification += (msg) => Console.WriteLine($"Handler 2: {msg}");

// ERROR: Cannot assign directly
// example.OnNotification = (msg) => Console.WriteLine("Test");

// ERROR: Cannot call from outside
// example.OnNotification?.Invoke("Test");

Built-in EventHandler:

public class Button
{
    // Using built-in EventHandler delegate
    public event EventHandler Click;
    
    // Using generic EventHandler with custom args
    public event EventHandler<MouseEventArgs> MouseMove;
    
    protected virtual void OnClick()
    {
        Click?.Invoke(this, EventArgs.Empty);
    }
    
    protected virtual void OnMouseMove(MouseEventArgs e)
    {
        MouseMove?.Invoke(this, e);
    }
}

// Custom EventArgs
public class MouseEventArgs : EventArgs
{
    public int X { get; set; }
    public int Y { get; set; }
}

// Usage
Button button = new Button();
button.Click += (sender, e) => Console.WriteLine("Button clicked!");
button.MouseMove += (sender, e) => Console.WriteLine($"Mouse at {e.X}, {e.Y}");

Multicast Delegates:

public delegate void LogHandler(string message);

public class Logger
{
    private LogHandler logHandler;
    
    public void AddLogMethod(LogHandler handler)
    {
        logHandler += handler;  // Add to invocation list
    }
    
    public void RemoveLogMethod(LogHandler handler)
    {
        logHandler -= handler;  // Remove from invocation list
    }
    
    public void Log(string message)
    {
        // Calls all methods in invocation list
        logHandler?.Invoke(message);
    }
}

// Usage
Logger logger = new Logger();
logger.AddLogMethod(msg => Console.WriteLine($"Console: {msg}"));
logger.AddLogMethod(msg => File.AppendAllText("log.txt", msg));
logger.AddLogMethod(msg => Debug.WriteLine(msg));

logger.Log("Error occurred");  // Calls all three methods

Key Differences:

FeatureDelegateEvent
AssignmentCan be directly assigned (=)Cannot be assigned, only += or -=
InvocationCan be called from anywhereCan only be invoked within declaring class
PurposeGeneral callback mechanismPublisher-subscriber pattern
EncapsulationWeakStrong
ProtectionNo protection from outsideProtected from outside manipulation

Real-world Example - Stock Market:

public class Stock
{
    private decimal price;
    
    // Event with custom EventArgs
    public event EventHandler<PriceChangedEventArgs> PriceChanged;
    
    public decimal Price
    {
        get => price;
        set
        {
            if (price != value)
            {
                decimal oldPrice = price;
                price = value;
                OnPriceChanged(new PriceChangedEventArgs(oldPrice, value));
            }
        }
    }
    
    protected virtual void OnPriceChanged(PriceChangedEventArgs e)
    {
        PriceChanged?.Invoke(this, e);
    }
}

public class PriceChangedEventArgs : EventArgs
{
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }
    public decimal Change => NewPrice - OldPrice;
    
    public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
    {
        OldPrice = oldPrice;
        NewPrice = newPrice;
    }
}

// Subscribers
public class StockAlert
{
    public StockAlert(Stock stock)
    {
        stock.PriceChanged += OnPriceChanged;
    }
    
    private void OnPriceChanged(object sender, PriceChangedEventArgs e)
    {
        if (e.Change > 10)
            Console.WriteLine($"Alert: Price jumped by {e.Change:C}");
    }
}

public class StockLogger
{
    public StockLogger(Stock stock)
    {
        stock.PriceChanged += OnPriceChanged;
    }
    
    private void OnPriceChanged(object sender, PriceChangedEventArgs e)
    {
        Console.WriteLine($"Price changed from {e.OldPrice:C} to {e.NewPrice:C}");
    }
}

// Usage
Stock appleStock = new Stock { Price = 150 };
StockAlert alert = new StockAlert(appleStock);
StockLogger logger = new StockLogger(appleStock);

appleStock.Price = 165;  // Both alert and logger are notified

Action and Func Delegates:

// Action - no return value
Action<string> logAction = msg => Console.WriteLine(msg);
Action<int, int> addAction = (x, y) => Console.WriteLine(x + y);

// Func - with return value (last type parameter is return type)
Func<int, int, int> add = (x, y) => x + y;
Func<string, bool> isValid = str => !string.IsNullOrEmpty(str);

// Usage
int result = add(5, 3);  // 8
bool valid = isValid("test");  // true

When to use what:

  • Use delegates when you need direct method reference or callbacks
  • Use events when implementing observer/publisher-subscriber pattern
  • Use Action/Func for simple inline delegates (LINQ, callbacks)
  • Use custom delegates when you need specific naming or complex signatures

Related Resources