C# Events

Events in C# are a fundamental feature that facilitate communication between objects in an event-driven programming model. They enable a class to notify other classes or objects when something of interest occurs, promoting a decoupled and maintainable architecture. This comprehensive guide delves deep into C# events, covering their declaration, usage, advanced concepts, best practices, and real-world applications with detailed explanations and code examples.

Table of Contents

  1. Introduction to Events
  2. - What are Events?
    - Delegates and Events Relationship
    - Event-Driven Programming
  3. Declaring Events
  4. - Event Keyword
    - Delegate Declaration for Events
    - Event Declaration Syntax
  5. Subscribing to Events
  6. - Using the += Operator
    - Anonymous Methods and Lambda Expressions
    - Using Event Handlers
  7. Raising Events
  8. - Protected Virtual Methods
    - Thread-Safe Event Invocation
  9. Multicast Events
  10. - Combining Multiple Event Handlers
    - Removing Event Handlers
  11. Built-in Event Patterns
  12. - EventHandler
    - EventHandler<T>
  13. Custom Event Arguments
  14. - Creating Custom EventArgs
    - Using Custom EventArgs
  15. Covariance and Contravariance in Events
  16. - Understanding Covariance
    - Understanding Contravariance
  17. Thread Safety in Events
  18. - Ensuring Thread-Safe Event Invocation
  19. Best Practices
  20. - Use the Event Keyword
    - Follow the .NET Event Pattern
    - Avoid Exposing Delegate Types
    - Unsubscribe from Events
    - Use Protected Virtual Methods
  21. Common Mistakes with Events
  22. - Not Checking for Null Before Raising Events
    - Exposing Events as Public Delegates
    - Forgetting to Unsubscribe from Events
  23. Real-World Example
  24. Summary

1. Introduction to Events

What are Events?

Events in C# are a way for a class to provide notifications to clients of that class when something of interest occurs. They are built on top of delegates and follow a specific pattern that ensures a clean and maintainable codebase.

Delegates and Events Relationship

Delegates are type-safe function pointers that reference methods. Events leverage delegates to allow methods to be called in response to certain actions or changes in state.

- Delegate: Defines the signature of the methods that can be called.
- Event: Encapsulates the delegate, providing a layer of abstraction and restricting direct invocation by external classes.

Event-Driven Programming

Event-driven programming is a paradigm where the flow of the program is determined by events such as user actions, sensor outputs, or messages from other programs. C# events are integral to this model, especially in GUI applications and asynchronous programming.

2. Declaring Events

Event Keyword

The `event` keyword in C# is used to declare an event within a class. It restricts the ways in which external classes can interact with the event, typically allowing only subscription and unsubscription.

Delegate Declaration for Events

Before declaring an event, you must define a delegate that specifies the signature of the event handlers.

Example:
public delegate void NotifyEventHandler(string message);

Event Declaration Syntax

Once the delegate is defined, declare an event using the `event` keyword.

Example:
public class Publisher
{
    // Declare the delegate
    public delegate void NotifyEventHandler(string message);
    
    // Declare the event using the delegate
    public event NotifyEventHandler OnNotify;
}
Explanation:
- NotifyEventHandler: Delegate defining the method signature for event handlers.
- OnNotify: Event that can be subscribed to by methods matching the delegate's signature.

3. Subscribing to Events

Using the += Operator

To subscribe a method to an event, use the `+=` operator. The method must match the delegate's signature.

Example:
public class Subscriber
{
    public void Subscribe(Publisher pub)
    {
        pub.OnNotify += HandleNotification;
    }

    public void HandleNotification(string message)
    {
        Console.WriteLine($"Received notification: {message}");
    }
}
Explanation:
- HandleNotification: Method that handles the event, matching the delegate's signature.
- Subscribe: Method that attaches `HandleNotification` to the `OnNotify` event.

Anonymous Methods and Lambda Expressions

C# allows the use of anonymous methods and lambda expressions to subscribe to events without explicitly defining separate methods.

Example:
public class Program
{
    public static void Main()
    {
        Publisher pub = new Publisher();
        
        // Using anonymous method
        pub.OnNotify += delegate(string msg)
        {
            Console.WriteLine($"Anonymous method received: {msg}");
        };
        
        // Using lambda expression
        pub.OnNotify += (msg) => Console.WriteLine($"Lambda received: {msg}");
        
        pub.TriggerEvent("Event triggered!");
    }
}
Explanation:
- Anonymous Method: Inline method defined using the `delegate` keyword.
- Lambda Expression: Concise syntax for defining inline methods using `=>`.

Using Event Handlers

Event handlers can be standard methods, anonymous methods, or lambda expressions that process the event when it is raised.

Example:
public class Program
{
    public static void Main()
    {
        Publisher pub = new Publisher();
        Subscriber sub = new Subscriber();
        
        sub.Subscribe(pub);
        pub.TriggerEvent("Hello, Subscribers!");
    }
}
Explanation:
- Subscriber.Subscribe: Attaches the `HandleNotification` method to the `OnNotify` event.
- TriggerEvent: Raises the event, invoking all subscribed handlers.

4. Raising Events

Protected Virtual Methods

To allow derived classes to override the event invocation behavior, it's common to define a protected virtual method that raises the event. Example:
public class Publisher
{
    public delegate void NotifyEventHandler(string message);
    public event NotifyEventHandler OnNotify;

    protected virtual void RaiseNotify(string message)
    {
        OnNotify?.Invoke(message);
    }

    public void TriggerEvent(string message)
    {
        RaiseNotify(message);
    }
}
Explanation:
- RaiseNotify: Protected virtual method that raises the event if there are any subscribers.
- TriggerEvent: Public method that can be called to trigger the event.

Thread-Safe Event Invocation

To ensure thread safety when raising events, make a copy of the event delegate before invoking it.

Example:
public class Publisher
{
    public delegate void NotifyEventHandler(string message);
    public event NotifyEventHandler OnNotify;

    protected virtual void RaiseNotify(string message)
    {
        NotifyEventHandler handler = OnNotify;
        if (handler != null)
        {
            handler(message);
        }
    }

    public void TriggerEvent(string message)
    {
        RaiseNotify(message);
    }
}
Explanation:
- Copy Delegate: Assign the event to a local variable to prevent race conditions where subscribers are added or removed during invocation.

5. Multicast Events

Delegates in C# are multicast, meaning they can reference multiple methods. When an event is raised, all subscribed methods are invoked in the order they were added.

Combining Multiple Event Handlers

Use the `+` operator or `Delegate.Combine` to attach multiple handlers to an event.

Example:
public class Program
{
    public delegate void NotifyEventHandler(string message);
    
    public static void Main()
    {
        Publisher pub = new Publisher();
        Subscriber sub1 = new Subscriber("Subscriber1");
        Subscriber sub2 = new Subscriber("Subscriber2");
        
        pub.OnNotify += sub1.HandleNotification;
        pub.OnNotify += sub2.HandleNotification;
        
        pub.TriggerEvent("Multicast event triggered!");
    }
}

public class Subscriber
{
    private string name;
    
    public Subscriber(string name)
    {
        this.name = name;
    }
    
    public void HandleNotification(string message)
    {
        Console.WriteLine($"{name} received: {message}");
    }
}
Explanation:
- Multiple Subscribers: `sub1` and `sub2` subscribe to the `OnNotify` event.
- Event Invocation: Both `HandleNotification` methods are called when the event is triggered.

Removing Event Handlers

Use the `-` operator or `Delegate.Remove` to detach a handler from an event.

Example:
public class Program
{
    public delegate void NotifyEventHandler(string message);
    
    public static void Main()
    {
        Publisher pub = new Publisher();
        Subscriber sub1 = new Subscriber("Subscriber1");
        Subscriber sub2 = new Subscriber("Subscriber2");
        
        pub.OnNotify += sub1.HandleNotification;
        pub.OnNotify += sub2.HandleNotification;
        
        pub.TriggerEvent("First event.");
        
        // Unsubscribe sub1
        pub.OnNotify -= sub1.HandleNotification;
        
        pub.TriggerEvent("Second event.");
    }
}

public class Subscriber
{
    private string name;
    
    public Subscriber(string name)
    {
        this.name = name;
    }
    
    public void HandleNotification(string message)
    {
        Console.WriteLine($"{name} received: {message}");
    }
}
Explanation:
- First Event: Both subscribers receive the notification.
- Unsubscribe `sub1`: Only `sub2` receives the second notification.

Sample Output:
Subscriber1 received: First event.
Subscriber2 received: First event.
Subscriber2 received: Second event.

6. Built-in Event Patterns

C# provides built-in generic delegates that simplify event declaration and handling without the need to define custom delegate types.

EventHandler

Definition: Represents a method that will handle an event that does not have event data.

Syntax:
public delegate void EventHandler(object sender, EventArgs e);

Example:
using System;

public class Publisher
{
    public event EventHandler OnSimpleEvent;

    protected virtual void RaiseEvent()
    {
        OnSimpleEvent?.Invoke(this, EventArgs.Empty);
    }

    public void TriggerEvent()
    {
        RaiseEvent();
    }
}

public class Program
{
    public static void Main()
    {
        Publisher pub = new Publisher();
        pub.OnSimpleEvent += HandleSimpleEvent;
        pub.TriggerEvent();
    }

    public static void HandleSimpleEvent(object sender, EventArgs e)
    {
        Console.WriteLine("Simple event handled.");
    }
}

Explanation:
- EventHandler: Standard delegate for events without additional data.
- OnSimpleEvent: Event using `EventHandler`.
- HandleSimpleEvent: Method matching `EventHandler` signature.

Sample Output:
Simple event handled.

EventHandler<TEventArgs>

Definition: Represents a method that will handle an event with data.

Syntax:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;

Example:
using System;

public class Publisher
{
    public event EventHandler<CustomEventArgs> OnCustomEvent;

    protected virtual void RaiseEvent(string message)
    {
        OnCustomEvent?.Invoke(this, new CustomEventArgs(message));
    }

    public void TriggerEvent(string message)
    {
        RaiseEvent(message);
    }
}

public class CustomEventArgs : EventArgs
{
    public string Message { get; }
    
    public CustomEventArgs(string message)
    {
        Message = message;
    }
}

public class Program
{
    public static void Main()
    {
        Publisher pub = new Publisher();
        pub.OnCustomEvent += HandleCustomEvent;
        pub.TriggerEvent("Hello with custom event args!");
    }

    public static void HandleCustomEvent(object sender, CustomEventArgs e)
    {
        Console.WriteLine($"Custom event received: {e.Message}");
    }
}
Explanation:
- EventHandler<TEventArgs>: Standard delegate for events with additional data.
- CustomEventArgs: Inherits from `EventArgs` and contains custom data.
- HandleCustomEvent: Method matching `EventHandler<CustomEventArgs>` signature.

Sample Output:
Custom event received: Hello with custom event args!

7. Anonymous Methods and Lambda Expressions

C# allows the use of anonymous methods and lambda expressions to subscribe to events without explicitly defining separate methods.

Anonymous Methods

Example:
using System;

public class Publisher
{
    public delegate void NotifyEventHandler(string message);
    public event NotifyEventHandler OnNotify;

    public void TriggerEvent(string message)
    {
        OnNotify?.Invoke(message);
    }
}

public class Program
{
    public static void Main()
    {
        Publisher pub = new Publisher();
        
        // Subscribe using an anonymous method
        pub.OnNotify += delegate(string msg)
        {
            Console.WriteLine($"Anonymous method received: {msg}");
        };
        
        pub.TriggerEvent("Event with anonymous method.");
    }
}
Explanation:
- Anonymous Method: Defined inline using the `delegate` keyword.
- Subscription: Attached directly to the event without a separate method.

Sample Output:
Anonymous method received: Event with anonymous method.

Lambda Expressions

Example:
using System;

public class Publisher
{
    public delegate void NotifyEventHandler(string message);
    public event NotifyEventHandler OnNotify;

    public void TriggerEvent(string message)
    {
        OnNotify?.Invoke(message);
    }
}

public class Program
{
    public static void Main()
    {
        Publisher pub = new Publisher();
        
        // Subscribe using a lambda expression
        pub.OnNotify += (msg) => Console.WriteLine($"Lambda received: {msg}");
        
        pub.TriggerEvent("Event with lambda expression.");
    }
}
Explanation:
- Lambda Expression: Concise syntax using `=>` to define inline methods.
- Subscription: Attached directly to the event with minimal syntax.

Sample Output:
Lambda received: Event with lambda expression.

8. Covariance and Contravariance in Events

Understanding Covariance

Covariance allows a delegate to reference a method that returns a type derived from the delegate's return type.

Example:
using System;

public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal speaks.");
    }
}

public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks.");
    }
}

public delegate Animal AnimalDelegate();

public class Program
{
    public static Animal GetAnimal()
    {
        return new Animal();
    }

    public static Dog GetDog()
    {
        return new Dog();
    }

    public static void Main()
    {
        AnimalDelegate animalDel = GetAnimal;
        animalDel();
        
        // Covariance: Assigning method returning Dog to delegate returning Animal
        animalDel = GetDog;
        animalDel();
    }
}
Explanation:
- AnimalDelegate: Delegate returning `Animal`.
- GetDog: Returns `Dog`, which is derived from `Animal`.
- Assignment: `GetDog` can be assigned to `AnimalDelegate` due to covariance.

Sample Output:
Animal speaks.
Dog barks.

Understanding Contravariance

Contravariance allows a delegate to reference a method that takes parameters of a type that is a base type of the delegate's parameter type.

Example:
using System;

public class Animal
{
    public virtual void Feed()
    {
        Console.WriteLine("Feeding animal.");
    }
}

public class Dog : Animal
{
    public override void Feed()
    {
        Console.WriteLine("Feeding dog.");
    }
}

public delegate void FeedDelegate(Dog dog);

public class Program
{
    public static void FeedAnimal(Animal animal)
    {
        animal.Feed();
    }

    public static void Main()
    {
        FeedDelegate feedDel = FeedAnimal; // Contravariance: Method takes Animal, delegate expects Dog
        Dog myDog = new Dog();
        feedDel(myDog);
    }
}
Explanation:
- FeedDelegate: Delegate expecting a method that takes a `Dog`.
- FeedAnimal: Method takes an `Animal`, which is a base class of `Dog`.
- Assignment: Allowed due to contravariance.

Sample Output:
Feeding dog.

9. Custom Event Arguments

Creating Custom EventArgs

To pass additional data with an event, create a class that inherits from `EventArgs`.

Example:
using System;

public class CustomEventArgs : EventArgs
{
    public string Message { get; }
    public int Number { get; }

    public CustomEventArgs(string message, int number)
    {
        Message = message;
        Number = number;
    }
}

Using Custom EventArgs

Example:
using System;

public class Publisher
{
    public event EventHandler<CustomEventArgs> OnCustomEvent;

    protected virtual void RaiseCustomEvent(string message, int number)
    {
        OnCustomEvent?.Invoke(this, new CustomEventArgs(message, number));
    }

    public void TriggerEvent(string message, int number)
    {
        RaiseCustomEvent(message, number);
    }
}

public class Subscriber
{
    public void Subscribe(Publisher pub)
    {
        pub.OnCustomEvent += HandleCustomEvent;
    }

    public void HandleCustomEvent(object sender, CustomEventArgs e)
    {
        Console.WriteLine($"Received message: {e.Message}, Number: {e.Number}");
    }
}

public class Program
{
    public static void Main()
    {
        Publisher pub = new Publisher();
        Subscriber sub = new Subscriber();
        
        sub.Subscribe(pub);
        pub.TriggerEvent("Custom event occurred!", 42);
    }
}
Explanation:
- CustomEventArgs: Contains additional data (`Message` and `Number`) for the event.
- OnCustomEvent: Event using `EventHandler<CustomEventArgs>`.
- HandleCustomEvent: Method processes the custom event data.

Sample Output:
Received message: Custom event occurred!, Number: 42

10. Thread Safety in Events

When dealing with events in a multi-threaded environment, ensuring thread safety is crucial to prevent race conditions and ensure that events are raised and handled correctly.

Ensuring Thread-Safe Event Invocation

Use a copy of the event delegate to prevent race conditions where subscribers might be added or removed during invocation.

Example:
using System;
using System.Threading;

public class Publisher
{
    public event EventHandler OnDataReceived;

    public void ReceiveData()
    {
        // Simulate data reception
        Thread.Sleep(1000);
        RaiseDataReceived();
    }

    protected virtual void RaiseDataReceived()
    {
        EventHandler handler = OnDataReceived;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }
}

public class Subscriber
{
    public void Subscribe(Publisher pub)
    {
        pub.OnDataReceived += HandleDataReceived;
    }

    public void HandleDataReceived(object sender, EventArgs e)
    {
        Console.WriteLine("Data received and handled.");
    }
}

public class Program
{
    public static void Main()
    {
        Publisher pub = new Publisher();
        Subscriber sub = new Subscriber();
        
        sub.Subscribe(pub);
        
        Thread thread = new Thread(pub.ReceiveData);
        thread.Start();
        
        thread.Join();
    }
}
Explanation:
- Copy Delegate: Assign `OnDataReceived` to a local variable before invoking.
- Multi-threading: Event is raised from a separate thread safely.

Sample Output:
Data received and handled.

11. Best Practices

Use the Event Keyword

Always use the `event` keyword when declaring events to restrict external classes from invoking the event directly, ensuring encapsulation.

Follow the .NET Event Pattern

Adhere to the standard .NET event pattern, which uses `EventHandler` and `EventHandler<TEventArgs>` delegates, providing consistency and compatibility with .NET libraries.

Example:
public class Publisher
{
    public event EventHandler<CustomEventArgs> OnCustomEvent;

    protected virtual void RaiseCustomEvent(string message)
    {
        OnCustomEvent?.Invoke(this, new CustomEventArgs(message));
    }
}

Avoid Exposing Delegate Types

Do not expose delegate types directly; use the `event` keyword to encapsulate them, preventing unintended external invocation or manipulation.

Unsubscribe from Events

Ensure that subscribers unsubscribe from events when they are no longer needed to prevent memory leaks and unintended behavior.

Example:
public class Subscriber
{
    public void Subscribe(Publisher pub)
    {
        pub.OnNotify += HandleNotification;
    }

    public void Unsubscribe(Publisher pub)
    {
        pub.OnNotify -= HandleNotification;
    }

    public void HandleNotification(object sender, EventArgs e)
    {
        Console.WriteLine("Notification received.");
    }
}

Use Protected Virtual Methods

Provide protected virtual methods to raise events, allowing derived classes to override and extend event invocation behavior.

Example:
protected virtual void RaiseNotify(string message)
{
    OnNotify?.Invoke(this, new NotifyEventArgs(message));
}

12. Common Mistakes with Events

Not Checking for Null Before Raising Events

Failing to check if there are any subscribers before invoking an event can lead to `NullReferenceException`.

Mistake:

public void TriggerEvent(string message)
{
    OnNotify(message); // Throws exception if no subscribers
}
Solution:
Use the null-conditional operator or check if the delegate is not null.
public void TriggerEvent(string message)
{
    OnNotify?.Invoke(this, new NotifyEventArgs(message));
}

Exposing Events as Public Delegates

Declaring events as public delegates without the `event` keyword allows external classes to invoke or reset the event, breaking encapsulation.

Mistake:
public class Publisher
{
    public NotifyEventHandler OnNotify; // Should use 'event' keyword
}
Solution:
Use the `event` keyword to encapsulate the delegate.
public class Publisher
{
    public event NotifyEventHandler OnNotify;
}

Forgetting to Unsubscribe from Events

Not unsubscribing from events, especially in long-running applications, can lead to memory leaks and prevent garbage collection of subscribers.

Mistake:
public class Subscriber
{
    public Subscriber(Publisher pub)
    {
        pub.OnNotify += HandleNotification;
    }

    // No unsubscription
}

Solution:
Unsubscribe when the subscriber is no longer needed.
public class Subscriber
{
    public void Subscribe(Publisher pub)
    {
        pub.OnNotify += HandleNotification;
    }

    public void Unsubscribe(Publisher pub)
    {
        pub.OnNotify -= HandleNotification;
    }

    public void HandleNotification(object sender, EventArgs e)
    {
        Console.WriteLine("Notification received.");
    }
}

Using Delegates Instead of Interfaces

Using delegates for scenarios better suited to interfaces can lead to less maintainable and scalable code.

Mistake:
public delegate void ProcessDelegate();

public class Processor
{
    public ProcessDelegate OnProcess;
}

Solution:
Use interfaces to define contracts for classes.
public interface IProcessor
{
    void Process();
}

public class Processor
{
    public event Action OnProcess;
}

13. Real-World Example

Example: Implementing a Temperature Monitoring System Using Events

This example demonstrates a temperature monitoring system where a sensor raises events when the temperature exceeds certain thresholds. Subscribers can handle these events to perform actions like logging, alerting, or adjusting settings.

Code Example:
using System;

public class TemperatureEventArgs : EventArgs
{
    public double Temperature { get; }
    public DateTime TimeStamp { get; }

    public TemperatureEventArgs(double temperature)
    {
        Temperature = temperature;
        TimeStamp = DateTime.Now;
    }
}

public class TemperatureSensor
{
    public event EventHandler<TemperatureEventArgs> OnTemperatureExceeded;

    private double threshold;

    public TemperatureSensor(double threshold)
    {
        this.threshold = threshold;
    }

    public void MeasureTemperature(double currentTemperature)
    {
        Console.WriteLine($"Measured Temperature: {currentTemperature}°C");
        if (currentTemperature > threshold)
        {
            RaiseTemperatureExceeded(currentTemperature);
        }
    }

    protected virtual void RaiseTemperatureExceeded(double temperature)
    {
        OnTemperatureExceeded?.Invoke(this, new TemperatureEventArgs(temperature));
    }
}

public class Logger
{
    public void LogTemperatureExceeded(object sender, TemperatureEventArgs e)
    {
        Console.WriteLine($"[Logger] Temperature exceeded! {e.Temperature}°C at {e.TimeStamp}");
    }
}

public class Alarm
{
    public void TriggerAlarm(object sender, TemperatureEventArgs e)
    {
        Console.WriteLine($"[Alarm] Alarm triggered! Temperature: {e.Temperature}°C");
    }
}

public class Program
{
    public static void Main()
    {
        TemperatureSensor sensor = new TemperatureSensor(30.0);
        Logger logger = new Logger();
        Alarm alarm = new Alarm();

        // Subscribe to the event
        sensor.OnTemperatureExceeded += logger.LogTemperatureExceeded;
        sensor.OnTemperatureExceeded += alarm.TriggerAlarm;

        // Simulate temperature measurements
        sensor.MeasureTemperature(25.0);
        sensor.MeasureTemperature(32.5);
        sensor.MeasureTemperature(28.0);
        sensor.MeasureTemperature(35.2);
    }
}
Explanation:
- TemperatureEventArgs: Custom event arguments containing temperature data and timestamp.
- TemperatureSensor: Monitors temperature and raises an event when the threshold is exceeded.
- Logger & Alarm: Subscribers that handle the `OnTemperatureExceeded` event.
- Program.Main: Sets up the sensor, subscribers, and simulates temperature measurements.

Sample Output:
Measured Temperature: 25°C
Measured Temperature: 32.5°C
[Logger] Temperature exceeded! 32.5°C at 4/27/2024 2:30:45 PM
[Alarm] Alarm triggered! Temperature: 32.5°C
Measured Temperature: 28°C
Measured Temperature: 35.2°C
[Logger] Temperature exceeded! 35.2°C at 4/27/2024 2:30:45 PM
[Alarm] Alarm triggered! Temperature: 35.2°C

14. Summary

C# events are a robust and flexible mechanism for implementing the observer pattern, enabling objects to communicate changes and actions seamlessly. By leveraging delegates, events promote a decoupled architecture, enhancing code maintainability and scalability.

Key Takeaways:
- Events and Delegates: Events are based on delegates and allow methods to be called in response to specific actions.

- Event Declaration: Use the `event` keyword with delegates to define events within classes.

- Subscribing and Unsubscribing: Utilize the `+=` and `-=` operators to manage event subscriptions.

- Raising Events: Implement protected virtual methods to raise events, ensuring encapsulation and flexibility.

- Multicast Delegates: Allow multiple methods to handle a single event, enabling complex event handling scenarios.

- Built-in Event Patterns: Use `EventHandler` and `EventHandler<TEventArgs>` for standardized event handling.

- Custom EventArgs: Extend `EventArgs` to pass additional data with events.

- Covariance and Contravariance: Enhance flexibility in delegate assignments by allowing variance in return and parameter types.

- Thread Safety: Ensure thread-safe event invocation to prevent race conditions in multi-threaded applications.

- Best Practices: Follow standard event patterns, manage subscriptions diligently, and encapsulate event delegates appropriately.

- Common Mistakes: Avoid null references, improper event exposure, and neglecting to unsubscribe from events.

- Real-World Applications: Implement systems like logging, monitoring, UI event handling, and more using events.

By mastering C# events, you can build responsive, maintainable, and scalable applications that effectively handle complex interactions and state changes.

Previous: C# Delegates | Next: C# Attributes

<
>