You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
10 KiB
10 KiB
marp | paginate | math | theme | title |
---|---|---|---|---|
true | true | mathjax | buutti | N. Delegates and events |
Delegates and events
Overview
- Delegates
- Multicast Delegates
- Anonymous Methods
- Events
Delegates
- Delegates are reference type variables that hold a reference to a method or multiple methods
- Class objects hold a reference to a class instance, delegate objects hold a reference to a method / methods
- Similar to function pointers in C and C++, or how any function in JavaScript works
- Allows for methods to be passed as variables, useful for creating, for example, events
Creating a delegate
-
Declare a delegate using the following syntax:
delegate returnType DelegateName(parameters);
-
Example:
delegate void PrintDelegate(string output);
-
This creates a new delegate of type
void
, namedPrintDelegate
and one parameter of typestring
-
The referenced method return and parameter types have to match the delegate!
Referencing a delegate
- After creating the delegate, it can be instantiated and the method assigned to it with the method name:
delegate void PrintDelegate(string output); static void Main(string[] args) { void PrintInLower(string text) { Console.WriteLine(text.ToLower()); } PrintDelegate print = PrintInLower; print("AaaBbbCcc"); // Outputs "aaabbbccc" }
Using multicast delegates
- Delegates can be composed of multiple methods using the
+
operator - Using the same
PrintDelegate
delegate as before, we could do this:
delegate void PrintDelegate(string output);
static void Main(string[] args)
{
void PrintInLower(string text)
{
Console.WriteLine(text.ToLower());
}
void PrintInUpper(string text)
{
Console.WriteLine(text.ToUpper());
}
PrintDelegate print = PrintInLower;
print += PrintInUpper;
print("AaaBbbCcc");
}
Removing methods
- Methods can be removed from the delegate with the
-
operator:delegate void PrintDelegate(string output); static void Main(string[] args) { ... print -= PrintInLower; print("AaaBbbCcc"); // Outputs "AAABBBCCC" ... }
Delegates: An example
- Let's extend our previous example (without the
-=
part) by creating a new class calledDelegateTest
, and giving it a constructor that takes aPrintDelegate
object as a parameter:public class DelegateTest { public DelegateTest(PrintDelegate printDelegate) { printDelegate("This Method Was Called From Another Class!"); } }
void PrintInLower(string text)
{
Console.WriteLine(text.ToLower());
}
void PrintInUpper(string text)
{
Console.WriteLine(text.ToUpper());
}
// Initialize new delegate which is composed of PrintInLower method
PrintDelegate print = PrintInLower;
// Add PrintInUpper method to the delegate
print += PrintInUpper;
// Send the delegate to the class constructor
DelegateTest delegateTest = new DelegateTest(print);
Anonymous methods
- Delegates can be initialized anonymously (without a specified name)
- Anonymous method in variable declaration:
delegate void PrintDelegate(string output); bool printUpper = true; PrintDelegate printCheckUpper = delegate (string text) { if (printUpper) Console.WriteLine(text.ToUpper()); else Console.WriteLine(text); }; printCheckUpper("I'm not angry!"); // Outputs I'M NOT ANGRY!
- Notice that the actual method that prints the text is not declared anywhere!
- You can use an empty anonymous method to initialize a delegate that does nothing:
delegate void SomeDelegate(); class Program { static void Main(string[] args) { // Initialize an empty delegate, add method later... SomeDelegate myDelegate = new SomeDelegate(delegate { }); } }
Events
The problem
class Game
{
Sound gameOverSound;
Window gameOverScreen;
void OnGameOver()
{
gameOverSound.Play(); // plays some sound
gameOverScreen.Show(); // shows a screen
}
}
...
class Program
{
static void Main()
{
var game = new Game();
// somewhere in the game logic...
// game.OnGameOver();
}
}
- Consider a game engine with three classes,
Sound
,Window
andGame
. - If implemented like this, the
Game
class has to know about theSound
andWindow
classes \Rightarrow
Game
is tightly coupled, and thus dependent on theSound
andWindow
classes- Changes in either of the classes could break the code!
The solution: Events
- A solution to this problem is the Publisher-subscriber pattern
- In C#, this pattern is implemented with the
event
keyword
- In C#, this pattern is implemented with the
- Events are signals that are raised by a Publisher and received by a Subscriber
- The publisher does not know or care who, if anyone, receives the signal
- Changes in the subscriber classes do not affect the publisher
- In C#, events are multicast delegates
- When an object triggers an event, the event invokes event handlers
- Event handlers are delegate instances added to the event
Raising events
- Events consist of two elements:
- A delegate that identifies the method that provides the response to the event
- In the simplest case, we can use the built-in
EventHandler
delegate
- In the simplest case, we can use the built-in
- An optional class to hold event data if the event provides data.
public class Publisher { public event EventHandler SampleEvent; // Wrap the event in a protected virtual method // to enable derived classes to raise the event. protected virtual void OnSampleEvent() { // Raise the event in a thread-safe manner using the ?. operator. SampleEvent?.Invoke(this); } }
- A delegate that identifies the method that provides the response to the event
Raising the event
Events can only be invoked (i.e., raised) from within the class (or derived classes) or struct where they're declared (the publisher class)
public class Publisher
{
public event EventHandler SampleEvent;
protected virtual void OnSampleEvent()
{
SampleEvent?.Invoke(this);
}
}
class Program
{
static void Main()
{
var pub = new Publisher();
pub.OnSampleEvent(); // ✅ Works fine
// pub.SampleEvent(); // ❌ Not allowed!
}
}
Adding subscribers to the event
- Now that we know how to raise the event, we can add subscribers to it
- i.e., functions that get called when the event is raised
public class Publisher
{
public event EventHandler SampleEvent;
protected virtual void OnSampleEvent()
{
SampleEvent?.Invoke(this);
}
}
class Program
{
static void Main()
{
var pub = new Publisher();
pub.SampleEvent +=
() => Console.WriteLine("Sample event!");
pub.OnSampleEvent();
}
}
Custom event handler
- We can define the delegate ourselves, for example if we want to send some data to the event.
public class Publisher { public delegate void SampleEventHandler(object sender); // Declare the event. public event SampleEventHandler SampleEvent; protected virtual void OnSampleEvent() { SampleEvent?.Invoke(this); } }
Event arguments
- Finally, we can send arguments to the event like this:
public class SampleEventArgs { public SampleEventArgs(string text) { Text = text; } public string Text { get; } // readonly } public class Publisher { public delegate void SampleEventHandler(object sender, SampleEventArgs e); public event SampleEventHandler SampleEvent; protected virtual void RaiseSampleEvent() { SampleEvent?.Invoke(this, new SampleEventArgs("Hello")); } }
"Fixing" the Game Over example
public delegate void GameOverHandler();
class Game
{
public event GameOverHandler GameOver;
protected virtual void OnGameOver()
{
GameOver?.Invoke(this);
}
}
...
class Program
{
static void Main()
{
var game = new Game();
Sound gameOverSound;
Window gameOverScreen;
game.GameOver += gameOverSound.Play;
game.GameOver += gameOverScreen.Show;
}
}
- Declare an event handler delegate (
GameOverHandler
) - Declare an instance of the handler with the
event
keyword (GameOVer
) - Declare a virtual method that invokes the event
- Add methods that subscribe to the event
Exercise 1: A rudimentary event system
Create a console application for controlling a plant treatment system with three methods that print the following outputs:
Method | Output |
---|---|
void ReleaseWater() |
Releasing water... |
void ReleaseFertilizer() |
Releasing fertilizer... |
void IncreaseTemperature() |
Increasing temperature... |
- All methods are off by default. Create a main loop where the user can...
- ...type the name of the method to switch each method on (add it to the delegate)
- ...type
run
to execute all the methods that are on