Delegates and events are what make C# capable of flexible, decoupled designs, from simple callbacks to full publisher-subscriber systems. This collection of 21 C# exercises covers declaring and invoking custom delegates, chaining multicast delegates, and building event-driven code using C#’s event keyword.
Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you understand how delegates and events fit together in real applications.
Also, See: C# Exercises with 22 topic-wise sets and 620+ practice questions.
What You’ll Practice
- Delegates: Declaring, assigning, and invoking custom delegate types.
- Multicast Delegates: Chaining multiple methods to a single delegate.
- Events: Publisher-subscriber patterns using the
eventkeyword. - Built-in Delegates:
Func<T>,Action<T>, andEventHandler.
+ Table Of Contents (21 Exercises)
Table of contents
- Exercise 1: The Math Operator
- Exercise 2: String Transformer
- Exercise 3: Filter List of Integers
- Exercise 4: Multicast Logger
- Exercise 5: Anonymous Greeting
- Exercise 6: Lambda Conversion
- Exercise 7: Action Reporter
- Exercise 8: Func Calculator
- Exercise 9: Predicate Matcher
- Exercise 10: The Button Click
- Exercise 11: Stock Price Tracker
- Exercise 12: Thermostat Alert
- Exercise 13: Unsubscribing Memory Leak
- Exercise 14: Standard .NET Event Pattern
- Exercise 15: Safe Event Invocation
- Exercise 16: Text File Downloader
- Exercise 17: Generic Data Pipeline
- Exercise 18: BankAccount Overdraft Protection
- Exercise 19: The Chat Room Broadcast
- Exercise 20: Traffic Light System
- Exercise 21: Custom Event Accessors (add / remove)
Exercise 1: The Math Operator
Practice Problem: Create a delegate called MathOperation that accepts two integers and returns an integer. Implement Add and Multiply methods, then assign them to the delegate to perform calculations.
Purpose: This exercise helps you practice declaring a delegate type and understand how delegates let you hold a reference to a method and invoke it indirectly, the foundation of callback patterns and event handling in C#.
Given Input: a = 10, b = 5, invoked once through Add and once through Multiply.
Expected Output:
Add Result = 15 Multiply Result = 50
▼ Hint
- Declare a delegate signature that matches
AddandMultiply: twointparameters returning anint. - Write the
AddandMultiplymethods so their signatures match the delegate exactly. - Assign the delegate variable to
Addfirst, invoke it, then reassign it toMultiplyand invoke again. - Remember that a delegate variable can point to any method as long as the signature matches.
▼ Solution & Explanation
Explanation:
delegate int MathOperation(int a, int b): Declares a delegate type that can reference any method taking two integers and returning an integer.MathOperation operation = Add: Assigns theAddmethod to the delegate variable without calling it yet.operation(10, 5): Invokes whichever method the delegate currently points to, using the same call syntax as a normal method call.operation = Multiply: Reassigns the delegate to a different method, showing that the same variable can represent different behavior at runtime.
Exercise 2: String Transformer
Practice Problem: Write a delegate StringTransformer that takes a string and returns a string. Pass it to a method that processes a list of names, letting the caller choose whether to convert them to uppercase, lowercase, or reverse them.
Purpose: This exercise helps you practice passing delegates as method parameters, which decouples the transformation logic from the method that applies it, a pattern used heavily in LINQ and functional-style C# code.
Given Input: names = ["Alice", "Bob", "Charlie"], processed with an uppercase transformer.
Expected Output:
ALICE BOB CHARLIE
▼ Hint
- Declare the delegate as
string StringTransformer(string input). - Write separate methods for uppercase, lowercase, and reverse, each matching that signature.
- Create a
ProcessNamesmethod that accepts a list of names and aStringTransformerdelegate, then loops through the list applying the delegate to each item. - Call
ProcessNameswith different methods to see the behavior change without modifying the loop itself.
▼ Solution & Explanation
Explanation:
delegate string StringTransformer(string input): Defines a delegate type for any method that takes a string and returns a transformed string.ProcessNames(List<string> names, StringTransformer transformer): Accepts the transformation logic as a parameter instead of hardcoding it inside the method.transformer(name): Invokes whichever method was passed in, applying it to each name during the loop.- Flexibility: Passing
ToLowerCaseor a reverse method instead ofToUpperCasechanges the output without touchingProcessNamesat all.
Exercise 3: Filter List of Integers
Practice Problem: Create a custom delegate IntFilter that takes an integer and returns a boolean. Write a method FilterList(List<int> numbers, IntFilter filter) that returns a new list containing only the elements that satisfy the condition.
Purpose: This exercise helps you practice using a delegate as a condition that gets evaluated at runtime, which is essentially how methods like List<T>.Where and List<T>.FindAll work internally.
Given Input: numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], filtered using an IsEven condition.
Expected Output: Filtered = 2, 4, 6, 8, 10
▼ Hint
- Declare the delegate as
bool IntFilter(int number). - Write an
IsEvenmethod that returns true when a number is divisible by 2. - Inside
FilterList, loop through the input list and call the delegate on each element to decide whether to keep it. - Add matching elements to a new list and return that list at the end.
▼ Solution & Explanation
Explanation:
delegate bool IntFilter(int number): Defines a delegate representing any condition that takes an integer and returns true or false.filter(number): Calls whichever condition method was passed in, so the filtering rule is decided by the caller, not byFilterListitself.result.Add(number): Builds a new list containing only the numbers that satisfied the condition, leaving the original list unchanged.- Reusability: Passing a different method, such as one checking for positive numbers, reuses
FilterListwithout any changes to its code.
Exercise 4: Multicast Logger
Practice Problem: Create a LogMessage delegate. Write three methods that log a message to the console, a mock text file, and a mock debug window. Combine all three into a single multicast delegate and invoke it once.
Purpose: This exercise helps you practice combining multiple methods into one multicast delegate using the += operator, which is the exact mechanism that powers events in C#.
Given Input: message = "System started successfully"
Expected Output:
Console: System started successfully File: System started successfully Debug: System started successfully
▼ Hint
- Declare the delegate as
void LogMessage(string message). - Write three methods with that same signature, one per logging target.
- Combine the methods into one delegate variable using the
+=operator. - Invoke the combined delegate a single time and observe all three methods running in the order they were added.
▼ Solution & Explanation
Explanation:
LogMessage logger = LogToConsole: Starts the multicast delegate with a single method.logger += LogToFile: Adds another method to the invocation list without removing the first one.logger += LogToDebug: Adds a third method, so the delegate now holds three methods in its invocation list.logger("System started successfully"): Runs every method in the invocation list, in order, with a single call.
Exercise 5: Anonymous Greeting
Practice Problem: Rewrite a simple greeting delegate using an anonymous method with the delegate keyword instead of a separate named method.
Purpose: This exercise helps you practice writing anonymous methods, a step toward lambda expressions, and understand how they simplify short pieces of delegate logic that don’t need to be reused elsewhere.
Given Input: name = "Priya"
Expected Output: Hello, Priya! Welcome.
▼ Hint
Use the delegate keyword directly when assigning the delegate variable, and write the greeting logic inline instead of creating a separate named method.
▼ Solution & Explanation
Explanation:
delegate (string name) { ... }: Defines the method body inline, right where the delegate is assigned, instead of pointing to a separately declared method.- No method name: The block has no name of its own, which is why it is called an anonymous method.
greet("Priya"): Invokes the inline logic exactly the same way you would invoke a delegate pointing to a named method.
Exercise 6: Lambda Conversion
Practice Problem: Take the anonymous method from Exercise 5 and rewrite it using a modern lambda expression (=>).
Purpose: This exercise helps you practice lambda syntax as a shorter alternative to anonymous methods, a style you will see constantly in LINQ queries and event handler assignments.
Given Input: name = "Priya"
Expected Output: Hello, Priya! Welcome.
▼ Hint
Replace the delegate (parameters) { } syntax with (parameters) => expression, keeping the same logic inside.
▼ Solution & Explanation
Explanation:
name => Console.WriteLine(...): The part before=>lists the parameter, and the part after it is the logic that runs, with nodelegatekeyword or curly braces needed for a single statement.- Type inference: The compiler infers that
nameis astringbecause theGreetdelegate signature requires it. - Same behavior: The lambda version behaves identically to the anonymous method version, just with less code.
Exercise 7: Action Reporter
Practice Problem: Use the built-in Action<string, ConsoleColor> delegate to build a console reporting system that prints messages in different colors, without declaring a custom delegate type.
Purpose: This exercise helps you practice using the built-in Action<> family of delegates, which removes the need to declare a custom void-returning delegate for common cases.
Given Input: Three messages with associated colors: ("Info: Load complete", Green), ("Warning: Low disk space", Yellow), ("Error: Connection failed", Red).
Expected Output:
Info: Load complete Warning: Low disk space Error: Connection failed
▼ Hint
- Declare a variable of type
Action<string, ConsoleColor>pointing to a method that matches those two parameters. - Inside that method, set
Console.ForegroundColorbefore printing the message. - Reset the color back to default after printing, so it doesn’t affect later output.
- Call the action multiple times with different message and color combinations.
▼ Solution & Explanation
Explanation:
Action<string, ConsoleColor>: A built-in delegate type that represents any method taking a string and aConsoleColorand returning nothing, so no custom delegate declaration is needed.Console.ForegroundColor = color: Changes the text color for whatever is printed next.Console.ResetColor(): Restores the default console color so later output isn’t affected by a previous call.report(message, color): Invokes the same logic three times with different arguments, printing each message in its own color.
Exercise 8: Func Calculator
Practice Problem: Use the built-in Func<double, double, double> delegate to implement a basic calculator capable of division, checking for division by zero before the calculation runs.
Purpose: This exercise helps you practice using the built-in Func<> family of delegates for methods that return a value, a pattern used throughout LINQ and functional-style C# code.
Given Input: a = 10, b = 2 and then a = 10, b = 0 to test the zero check.
Expected Output:
Result = 5 Cannot divide by zero
▼ Hint
- Declare a variable of type
Func<double, double, double>pointing to aDividemethod. - Before invoking the function, check whether the divisor is zero.
- If it is zero, print an error message instead of calling the function.
- Otherwise, invoke the function and print the returned result.
▼ Solution & Explanation
Explanation:
Func<double, double, double>: A built-in delegate type representing a method with twodoubleparameters that returns adouble, soDividefits it directly.if (b == 0): Guards against invoking the delegate with an invalid divisor, avoiding a runtime exception.operation(a, b): Invokes whichever function was passed intoCalculate, keeping the division logic separate from the safety check.- Reusability: Passing a different
Func<double, double, double>, such as one for multiplication, would work withCalculatewithout any changes.
Exercise 9: Predicate Matcher
Practice Problem: Use Predicate<T> to find the first book in a List<Book> that matches a specific criteria, such as being published before a certain year.
Purpose: This exercise helps you practice using Predicate<T> together with List<T>.Find, a built-in search mechanism that avoids writing a manual loop.
Given Input: A list of books, searching for the first one published before 2000.
Expected Output: Found: The Great Gatsby (1925)
▼ Hint
- Define a simple
Bookclass withTitleandYearproperties. - Create a
List<Book>containing a few sample books with different years. - Use
List<Book>.Findwith aPredicate<Book>that checks whetherYearis less than 2000. - Print the matched book’s title and year, or a not-found message if
Findreturns null.
▼ Solution & Explanation
Explanation:
Predicate<Book>: A built-in delegate type representing a method that takes aBookand returns abool, used to describe a matching condition.book => book.Year < 2000: A lambda expression supplying the actual condition without writing a separate named method.books.Find(publishedBefore2000): Scans the list internally and returns the first book that satisfies the predicate, or null if none match.- Null check: Checking
match != nullavoids a runtime error when no book satisfies the condition.
Exercise 10: The Button Click
Practice Problem: Create a Button class. Simulate a user clicking the button by defining a custom delegate and an OnClick event, then raise the event when a Click() method is called.
Purpose: This exercise helps you practice the relationship between delegates and events. An event is a controlled way of exposing a delegate so outside code can subscribe to it, without being able to invoke it directly or overwrite existing subscribers.
Given Input: A handler method is subscribed to OnClick before button.Click() is called.
Expected Output: Button was clicked!
▼ Hint
- Declare a delegate such as
void ClickHandler(). - Inside the
Buttonclass, declare an event of that delegate type, for exampleevent ClickHandler OnClick. - Write a
Click()method that checks whetherOnClickhas any subscribers before invoking it. - Outside the class, subscribe a handler method to
OnClickusing+=before callingClick().
▼ Solution & Explanation
Explanation:
event ClickHandler OnClick: Exposes the delegate as an event, so outside code can only subscribe or unsubscribe with+=and-=, not invoke it or replace it directly.OnClick?.Invoke(): Safely raises the event only if at least one method has subscribed, avoiding a null reference error when there are no subscribers.button.OnClick += ShowClickMessage: Subscribes an external method to the event from outside theButtonclass.button.Click(): Triggers the internal logic that raises the event, running every subscribed method in response.
Exercise 11: Stock Price Tracker
Practice Problem: Create a Stock class with a Price property. Raise a PriceChanged event whenever the price changes, notifying a StockTrader class that prints the old and new prices.
Purpose: This exercise helps you practice raising an event from inside a property setter whenever state changes, and letting an external class react to that change, the core mechanism behind property change notifications.
Given Input: Price starts at 100, then changes to 150, then to 165.
Expected Output:
Price changed from 100 to 150 Price changed from 150 to 165
▼ Hint
- Store the price in a private backing field and only raise the event from inside the
Priceproperty’s setter. - Compare the incoming value against the current price before assigning it, so the event only fires on an actual change.
- Declare a delegate that carries both the old price and the new price as parameters.
- Subscribe the
StockTradermethod to the event before changing the price so it receives the notification.
▼ Solution & Explanation
Explanation:
private decimal price: A backing field that stores the actual value separately from the public property, so the old value is still available when the event fires.if (value != price): Ensures the event only fires when the new price is genuinely different, avoiding redundant notifications.PriceChanged?.Invoke(oldPrice, price): Raises the event safely and passes both the old and new values to every subscriber.stock.PriceChanged += trader.OnPriceChanged: Subscribes an instance method from a completely separate class, showing that events can connect unrelated classes together.
Exercise 12: Thermostat Alert
Practice Problem: Design a Thermostat class that fires a TemperatureCritical event if the temperature rises above 100°C.
Purpose: This exercise helps you practice conditionally raising an event only when a threshold is crossed, a pattern common in monitoring, alerting, and safety-critical systems.
Given Input: Temperature is set to 85, then to 105.
Expected Output: Critical temperature reached: 105°C
▼ Hint
- Store the temperature using a property with a backing field, similar to the Stock Price Tracker exercise.
- Inside the setter, check whether the new value is greater than 100 after assigning it.
- Only invoke
TemperatureCriticalwhen that condition is true, passing the current temperature. - Subscribe a handler before setting the temperature so you can observe when the alert does and doesn’t fire.
▼ Solution & Explanation
Explanation:
if (temperature > 100): Guards the event so it only fires once the reading crosses the critical threshold.TemperatureCritical?.Invoke(temperature): Passes the current reading to every subscriber at the moment the alert condition is met.- Lambda subscriber: Subscribes an inline lambda instead of a separate named method, printing the alert message directly.
- Setting
Temperature = 85first: Shows that the event does not fire for a normal reading, only for one that trips the threshold.
Exercise 13: Unsubscribing Memory Leak
Practice Problem: Write a program where an object subscribes to an event, fires it, unsubscribes, and fires it again. Verify that the second fire does not trigger the unsubscribed object.
Purpose: This exercise helps you practice using -= to detach a handler from an event, which matters for preventing memory leaks caused by subscribers that outlive the object they are attached to.
Given Input: Subscribe Handler to MyEvent, raise the event, unsubscribe Handler, then raise the event again.
Expected Output: Handler triggered (printed only once, after the first raise)
▼ Hint
- Declare a simple event with a delegate that takes no parameters.
- Subscribe a method using
+=and raise the event to confirm it runs. - Unsubscribe using
-=with the exact same method reference used to subscribe. - Raise the event a second time and confirm nothing prints, since the invocation list is now empty.
▼ Solution & Explanation
Explanation:
publisher.MyEvent += Handler: AddsHandlerto the invocation list so it runs the next time the event is raised.- First
RaiseEvent(): Prints “Handler triggered” becauseHandleris still subscribed at that point. publisher.MyEvent -= Handler: RemovesHandlerfrom the invocation list using the same method reference that was used to subscribe.- Second
RaiseEvent(): Produces no output, confirming the unsubscribe worked and the object is no longer referenced by the event’s invocation list.
Exercise 14: Standard .NET Event Pattern
Practice Problem: Redo the Stock Price Tracker (Exercise 11) using the standard .NET guidelines: use EventHandler<T> and a custom EventArgs subclass named StockChangedEventArgs.
Purpose: This exercise helps you practice the standard .NET event pattern that every framework and library event follows: a sender object, a piece of data derived from EventArgs, and the generic EventHandler<T> delegate.
Given Input: Price starts at 100, then changes to 150, then to 165.
Expected Output:
Price changed from 100 to 150 Price changed from 150 to 165
▼ Hint
- Create a
StockChangedEventArgsclass that inherits fromEventArgsand exposesOldPriceandNewPriceproperties. - Declare the event using
EventHandler<StockChangedEventArgs>instead of a custom delegate type. - Make sure the handler method signature matches
(object sender, StockChangedEventArgs e). - Raise the event by passing
thisas the sender and a newStockChangedEventArgsinstance as the data.
▼ Solution & Explanation
Explanation:
StockChangedEventArgs : EventArgs: A custom data container following the .NET convention of deriving fromEventArgsto bundle event data together.EventHandler<StockChangedEventArgs> PriceChanged: Uses the standard generic delegate instead of a custom one, matching the pattern used throughout the .NET framework.PriceChanged?.Invoke(this, new StockChangedEventArgs(...)): Passes the publishing object as the sender along with the event data as a single package.OnPriceChanged(object sender, StockChangedEventArgs e): Matches the required handler signature forEventHandler<T>, giving access to both the sender and the event data.
Exercise 15: Safe Event Invocation
Practice Problem: Implement a class that raises an event safely using the null-conditional operator (MyEvent?.Invoke(this, EventArgs.Empty)) to avoid a null reference exception when there are no subscribers.
Purpose: This exercise helps you practice the standard safe-invocation idiom, and understand why checking for null before invoking an event matters, especially when subscribers could be added or removed on another thread.
Given Input: RaiseEvent() is called once before any subscriber is attached, then a handler subscribes, then RaiseEvent() is called again.
Expected Output: Event raised safely (printed once, only after a subscriber is attached; the first call produces no output and no exception).
▼ Hint
- Declare the event using the built-in
EventHandlertype. - Inside the method that raises the event, use the null-conditional operator instead of calling
Invokedirectly. - Call the raising method once before subscribing anything, to confirm no exception occurs.
- Subscribe a handler afterward and call the raising method again to see it print.
▼ Solution & Explanation
Explanation:
MyEvent?.Invoke(this, EventArgs.Empty): Checks whetherMyEventis null before invoking it, avoiding aNullReferenceExceptionwhen nobody has subscribed.- First
RaiseEvent()call: Produces no output and no crash, because the null-conditional operator skips the invocation entirely. EventArgs.Empty: A reusable, pre-built empty instance ofEventArgs, convenient when an event doesn’t carry any extra data.- Second
RaiseEvent()call: Prints “Event raised safely” becauseHandleris now subscribed.
Exercise 16: Text File Downloader
Practice Problem: Create a FileDownloader class. Implement a ProgressChanged event using EventHandler<DownloadProgressEventArgs> that passes the percentage completion back to the console UI.
Purpose: This exercise helps you practice modeling a progress-reporting workflow with the standard event pattern, similar to how real download or upload APIs report status back to a UI layer.
Given Input: A simulated download that reports progress in steps of 25, up to 100.
Expected Output:
Download progress: 25% Download progress: 50% Download progress: 75% Download progress: 100%
▼ Hint
- Create a
DownloadProgressEventArgsclass that inherits fromEventArgsand holds aPercentCompleteproperty. - Declare
event EventHandler<DownloadProgressEventArgs> ProgressChangedinsideFileDownloader. - Write a
StartDownloadmethod that loops through progress increments, raising the event at each step. - Subscribe a console-printing handler before calling
StartDownload.
▼ Solution & Explanation
Explanation:
DownloadProgressEventArgs : EventArgs: Bundles the percentage value into a dedicated data class, following the standard event pattern.for (int percent = 25; percent <= 100; percent += 25): Simulates the download advancing in fixed increments, raising the event at each step.ProgressChanged?.Invoke(this, new DownloadProgressEventArgs(percent)): Reports progress to any subscriber withoutFileDownloaderneeding to know how it will be displayed.OnProgressChanged: Readse.PercentCompletefrom the event data and prints it, keeping console-specific formatting outside ofFileDownloader.
Exercise 17: Generic Data Pipeline
Practice Problem: Write a generic class Pipeline<T> that accepts data and passes it through a chain of generic Func<T, T> delegates to transform the data step by step.
Purpose: This exercise helps you practice combining generics with delegates to build a flexible, reusable processing pipeline, a pattern common in data transformation and middleware-style code.
Given Input: Starting value 3, passed through three steps: double it, add 5, then square it.
Expected Output: Result = 121
▼ Hint
- Give
Pipeline<T>an internal list ofFunc<T, T>steps. - Write an
AddStepmethod that appends a transformation function to that list. - Write a
Process(T input)method that loops through the steps, feeding the output of one step as the input to the next. - Add the steps in the exact order they should run, then call
Processwith the starting value.
▼ Solution & Explanation
Explanation:
Pipeline<T>: A generic class that can transform any data type, not just integers, as long as matchingFunc<T, T>steps are supplied.AddStep(Func<T, T> step): Appends a transformation to an internal list without running it immediately.foreach (Func<T, T> step in steps): Applies each stored transformation in sequence, feeding the result of one step into the next.pipeline.Process(3): Runs the value3through all three steps in order:3 → 6 → 11 → 121.
Exercise 18: BankAccount Overdraft Protection
Practice Problem: Create a BankAccount class with a Withdraw method. If a withdrawal exceeds the balance, fire an OverdraftAttempted event, and allow the subscriber to cancel the transaction using a property inside the event data.
Purpose: This exercise helps you practice using event data to let a subscriber influence the outcome of an operation, a pattern used for cancellable events such as closing a window or validating a form before it submits.
Given Input: Balance starts at 100. A withdrawal of 150 is attempted, and the subscriber sets Cancel = true.
Expected Output:
Overdraft attempted for 150 Transaction cancelled by subscriber Balance remains: 100
▼ Hint
- Create an
OverdraftEventArgsclass inheriting fromEventArgswith a settableCancelproperty, defaulting to false. - Inside
Withdraw, check if the requested amount exceeds the balance before raisingOverdraftAttempted. - After raising the event, check the
Cancelproperty on that same event data instance to decide what happens next. - If
Cancelis true, print a cancellation message and leave the balance unchanged; otherwise complete the withdrawal as usual.
▼ Solution & Explanation
Explanation:
OverdraftEventArgswith aCancelproperty: Carries both the attempted amount and a writable flag that a subscriber can set to influence what happens next.OverdraftAttempted?.Invoke(this, args): Raises the event and passes the sameargsinstance to every subscriber, so any changes they make toCancelstay visible afterward.if (args.Cancel): Checked immediately after raising the event, letting the subscriber’s decision control whether the withdrawal proceeds.e.Cancel = trueinside the subscriber: Demonstrates how external code can cancel an operation started inside another class, withoutBankAccountneeding to know why.
Exercise 19: The Chat Room Broadcast
Practice Problem: Build a ChatRoom class where multiple User objects can join. When one user sends a message, the ChatRoom broadcasts it to all other connected users through events.
Purpose: This exercise helps you practice using events for one-to-many communication between independent objects, similar to how publish-subscribe messaging systems work.
Given Input: Three users, Alice, Bob, and Charlie, join the room. Alice sends "Hello everyone!".
Expected Output:
Bob received: Hello everyone! Charlie received: Hello everyone!
▼ Hint
- Give
Userits ownMessageSentevent that fires whenever that user sends a message. - When a user joins the room, have
ChatRoomsubscribe to that user’sMessageSentevent. - When
ChatRoomreceives a message from a sender, loop through every joined user and deliver the message to each one. - Skip the original sender in that loop, so a user never receives their own broadcasted message.
▼ Solution & Explanation
Explanation:
event Action<User, string> MessageSentonUser: LetsChatRoomknow whenever a user sends a message, withoutUserneeding a direct reference toChatRoom.room.Join(user): Registers the user in the room’s list and subscribes to that user’sMessageSentevent in a single step.Broadcast(User sender, string message): Loops through every joined user and delivers the message, skipping the original sender.alice.Send(...): Triggers the whole chain, from Alice raising her own event toChatRoomdelivering the message to Bob and Charlie.
Exercise 20: Traffic Light System
Practice Problem: Create a TrafficLightController that manages Red, Yellow, and Green state transitions. Use events to notify attached Car and Pedestrian objects so each can change its own behavior.
Purpose: This exercise helps you practice broadcasting a single state change to multiple different subscriber types at once, showing that an event isn’t limited to notifying just one kind of listener.
Given Input: The controller changes from its starting state to Green, then Yellow, then Red.
Expected Output:
Car: Driving through green light Pedestrian: Do not cross Car: Slowing down for yellow light Pedestrian: Do not cross Car: Stopped at red light Pedestrian: Waiting to cross
▼ Hint
- Define an
enumwith values forRed,Yellow, andGreen. - Declare a single event on
TrafficLightControllerthat carries the new state as its parameter. - Write a method that updates the state and raises the event, and call it once per transition.
- Have both
CarandPedestriansubscribe their own methods to the same event, each reacting differently to the same state.
▼ Solution & Explanation
Explanation:
event Action<LightState> LightChanged: A single event that any number of different object types can subscribe to, regardless of what each one does in response.controller.LightChanged += car.OnLightChangedand+= pedestrian.OnLightChanged: Attaches two unrelated classes to the same event without either one knowing about the other.ChangeLight(newState): The only place that raises the event, keeping the decision of “what happens next” entirely outsideTrafficLightController.switchstatement insideCar.OnLightChanged: Shows each subscriber deciding its own reaction to the exact same broadcasted state.
Exercise 21: Custom Event Accessors (add / remove)
Practice Problem: Implement an event explicitly using add and remove accessors to log to the console every time a subscriber attaches or detaches from the event.
Purpose: This exercise helps you practice writing custom add and remove accessors, revealing what the compiler normally generates automatically behind a plain event declaration.
Given Input: A handler subscribes to MyEvent, then unsubscribes from it.
Expected Output:
Subscriber added Subscriber removed
▼ Hint
- Declare a private backing field of the delegate type to store the actual invocation list yourself.
- Write the event using explicit
addandremoveblocks instead of the shorthandeventdeclaration. - Inside
add, append the incoming handler to the backing field and print a log message. - Inside
remove, detach the incoming handler from the backing field and print a log message.
▼ Solution & Explanation
Explanation:
private Notify handlers: A manually declared backing field that stores subscribers, since explicit accessors bypass the compiler-generated one.add { handlers += value; ... }: Runs every time someone subscribes with+=, letting you insert custom logic such as logging alongside the actual subscription.remove { handlers -= value; ... }: Runs every time someone unsubscribes with-=, mirroring theaddaccessor.value: A keyword representing the method being subscribed or unsubscribed, available automatically inside both accessors.

Leave a Reply