The content and code of this article is referenced from book Pro C#5.0 and the .NET 4.5 Framework by Apress. The intention of the writing is to review the konwledge and gain better understanding of the .net framework.
Up to this point, most of the applications you developed sent requests to a given object. However, many applications require that an object be able to communicate back to the entity that created it using callback mechanism.
Under the .net platform, the delegate type is the preferred means of defining and responding to callbacks. Essentially, the .net delegate type is a type-safe object that "points to " a methods or list of methods can be invoked at a later time.
1. Understanding the .net delegate type
Traditionaly, we use c-style function to achieve callback in button-cliking, mouse-moving, menu-selecting. However, the problem with the approach is that it represents little more than a raw address in memory, and therefore, there might be frequent source of bugs, hard crashes and runtime disasters.
In the .net framework, callback function is accomplished in a much safer and more object-oriented manner using delegates. In essence, a delegate is a type-safe object that points to another method in the application, which can be invoked at a later time. Delegate contains three important pieces of information - (1) the address of the method on which it makes calls (2) the parameters of the methods (3) the return type of this method
1.1 Defining a delegate type
In .net, you use keyword delegate to define a delegate type, and the name of the delegate can be whatever you desire. However, you must define the delegate to match the signature of the method it will point to .
public delegate int BinaryOp(int x, int y);
As example, the delegate BinaryOp can points to a method that returns an integer and takes two integers as input parameters. When c# compiler processes delegate types, it automatically generates a sealed class deriving from System.MulticastDelegate, and defines three public methods - BeginInvoke, EndInvode, and Invoke. BeginInvoke and EndInvoke provide the ability to call method asynchronously on a separate thread of execution.
1 sealed class BinaryOp : System.MulticastDelegate //generated at the background 2 { 3 public int Invoke (int x, int y); 4 public IAsyncResult BeginInvoke (int x, int y, AsyncCallback cb, object state); 5 public int EndInvoke (IAsyncResult result); 6 }
Delegates can also point to methods that contain any number of out or ref parameters.
public delegate int BinaryOp(out int x, ref bool z, int y);
To summarize, a C# delegate type definition results in a sealed class with three compiler-generated methods whose prameter and return types are based on the delegate‘s declaration.
1.2 System.MulticaseDelegate and System.Delegate base classes
When you build a type using delegate, you are indirectly declaring a class type that derives from System.MulticastDelegate. And it provides descendants with access to a list that contains the addresses of the methods maintained by the delegate objects.
Member | Meaning in life |
Method | This property returns a System.Reflection.MethodInfo object that represents details of a static method maintained by the delegate |
Target | If the method to be called is defined at the object level, Target returns an object that represents the method maintained by the delegate. |
Combine() | Adds a method to the list maintained by the delegate. You can also use += operator as a shorthand notation |
GetInvocationList() | This method returns an array of System.Delegate objects, each representing a paticular method that may be invoked. |
Remove(), RemoveAll() | remove a method from the delegate‘s invocation list. You can also use -= operator as a shorthand notation. |
1.3 One simple delegate example
1 public delegate int BinaryOp(int x, int y); 2 public class SimpleMath 3 { 4 public static int Add (int x, int y) 5 { 6 return x + y; 7 } 8 9 public static int Substract(int x, int y) 10 { 11 return x - y; 12 } 13 } 14 15 class MainClass 16 { 17 public static void Main (string[] args) 18 { 19 //delegate object that points to Add method 20 BinaryOp op = new BinaryOp (SimpleMath.Add); 21 Console.WriteLine (op (10, 20)); //invoke the add method 22 } 23 }
Under the hood, the runtime actually calls the compiler-generated Invoke() method on you MulticastDelegate derived class.
1.4 Sending object state notification using delegate
Now, let‘s look at a more realistic delegate example.
1 public class Car 2 { 3 //property 4 public int CurrentSpeed { get; set; } 5 public int MaxSpeed { get; set; } 6 public string PetName { get; set; } 7 8 private bool carIsDead; 9 10 //constructor 11 public Car(){MaxSpeed = 100;} 12 public Car(string name, int maxSp, int currSp) 13 { 14 CurrentSpeed = currSp; 15 PetName = name; 16 MaxSpeed = maxSp; 17 } 18 19 //delegate 20 public delegate void CarEngineHandler(string msgForCaller); //delegate take one string as parameter, and returns void 21 22 private CarEngineHandler listOfHanlder; 23 24 //assign methods to delegate 25 public void RegisterWithCarEngine(CarEngineHandler methodToCall) 26 { 27 listOfHanlder = methodToCall; 28 } 29 30 public void Accelerate(int delta) 31 { 32 if (carIsDead) { 33 if (listOfHanlder != null) 34 listOfHanlder ("Sorry , this car is dead.."); //invoke method in delegate 35 } else { 36 CurrentSpeed += delta; 37 if (listOfHanlder != null && CurrentSpeed > 80) { 38 listOfHanlder ("Careful buddy"); 39 } 40 } 41 } 42 }
In accelerate, we need to check the value against null becuase it is the caller‘s job to allocate these objects by calling register method.
1.5 Enable multicasting
In c#, a delegate object can maintain a list of methods to call, rather than just a single method.
//assign methods to delegate public void RegisterWithCarEngine(CarEngineHandler methodToCall) { listOfHanlder += methodToCall; }
And we use += operator to achieve multicasting. When we use +=, acutally the runtime is calling Delegate.Combine().
1 public void RegisterWithCarEngine(CarEngineHandler methodToCall) 2 { 3 4 if (listOfHanlder == null) 5 listOfHanlder += methodToCall; 6 else 7 Delegate.Combine (listOfHanlder, methodToCall); 8 }
1.6 removing targets from delegate invocation list
The delegate also defines a static Remove() method that allows a caller to dynamically remove a method from a delegate‘s invocation list. As alternative, you can also use -= operator.
1 public void UnRegisterWithCarEngine(CarEngineHandler methodToCall) 2 { 3 Delegate.Remove(listOfHanlder, methodToCall); 4 }
1.7 Method Group conversion Syntax
public static void Main (string[] args) { Car c1 = new Car ("car1", 120, 50); Car.CarEngineHandler handler = new Car.CarEngineHandler (PrintMsg); //create delegate object c1.RegisterWithCarEngine (handler); } public static void PrintMsg(string msg) { Console.WriteLine (msg); }
In the example, we create a delegate variable, then pass the variable to car method. As a simplification, C# provides a shortcut termed method group conversion, allows you to supply a direct method name, rather than a delegate object, when calling method take delegate as arguments.
public static void Main (string[] args) { Car c1 = new Car ("car1", 120, 50); c1.RegisterWithCarEngine (PrintMsg); //pass method directly }
1.8 Understanding Generic Delegates
C# allows you to create generic delegates.
public static void Main (string[] args) { Car c1 = new Car ("car1", 120, 50); c1.RegisterWithCarEngine (PrintMsg); //pass method directly }
1.9 The Generic Action<> and Func<> Delegates
In many cases, we can use framework‘s built-in Action<> and Func<> delegates, instead of creating many custom delegates. The Action<> delegate is defined in System namespace. You can use this generic delegate to point to a method that takes up 16 arguments and returns void.
public static void Main (string[] args) { Action<string, int> action = new Action<string, int> (DisplayMsg); action ("test", 100); } static void DisplayMsg(string arg1, int arg2) { Console.WriteLine (arg1); Console.WriteLine (arg2); }
If you want to point a method has return value, you can use Func<>.
public static void Main (string[] args) { Func<int, int, int> func = new Func<int, int, int> (Add); //the last parameter is returning type Console.WriteLine(func (14, 24)); } static int Add(int arg1, int arg2) { return arg1 + arg2; }
2. C# Events
As a shortcut, C# provides the event keyword, and you don‘t have to build custom methods to add or remove methods to delegate‘s invocation list. When the compiler processes the event keyword, you are provided with registration and unregistration methods.
//delegate public delegate void CarEngineHandler(string msgForCaller); //delegate take one string as parameter, and returns void //create event, to handle registration and unregistration public event CarEngineHandler Exploded; public void Accelerate(int delta) { if (carIsDead) { if (Exploded != null) Exploded ("Sorry , this car is dead.."); //invoke method in delegate } else { CurrentSpeed += delta; if (Exploded != null && CurrentSpeed > 80) { Exploded ("Careful buddy"); } } }
At the background, two methods are generated for event keyword, one method start with add_CarEngineHanlder and the other starts with Remove_CarEngineHandler. The caller simply make use of += and -= operators directly to register and unregister methods with delegate.
public static void Main (string[] args) { Car car = new Car ("car1", 150, 90); Car.CarEngineHandler d = new Car.CarEngineHandler (PrintMsg); car.Exploded += d; }
You can even simplify the code by using method group conversion.
public static void Main (string[] args) { Car car = new Car ("car1", 150, 90); car.Exploded += PrintMsg; }
2.1 Custom Event arguments
There is one final enhancement we could make to the Car class that mirros Microsoft‘s recommended event pattern. In general, the first parameter of the underlying delegate is System.object represents a reference to the calling object, and the second parameter is a descendant of System.EventArgs represents information regarding the event.
public class CarEventArgs : EventArgs { public readonly string msg; public CarEventArgs(string message) { msg = message; } }
//delegate public delegate void CarEngineHandler(object sender, CarEventArgs e); //create event, to handle registration and unregistration public event CarEngineHandler Exploded; public void Accelerate(int delta) { if (carIsDead) { if (Exploded != null) Exploded (this, new CarEventArgs("Sorry , this car is dead..")); //invoke method in delegate } }
public static void Main (string[] args) { Car car = new Car ("car1", 150, 90); car.Exploded += CarBlow; car.Accelerate (10); } public static void CarBlow(object sender, CarEventArgs e) { Console.WriteLine(sender); Console.WriteLine(e.msg); }
2.2 Generic EventHandler<T> delegate
Given that so many custom delegates take an object and an EventArgs as parameters, you could further streamline the example by using EventHanlder<T>, where T is your own EventArgs type.
public event EventHandler<CarEventArgs> Exploded; //no need to delcare delegate
public static void Main (string[] args) { Car car = new Car ("car1", 150, 90); EventHandler<CarEventArgs> d = new EventHandler<CarEventArgs> (CarBlow); car.Exploded += d; car.Accelerate (10); }
3. C# anonymous methods
When a caller wants to listen to incoming events, it must define a method that matches the signature of associated delegate. However, the custom methods are seldom used by any other parts of the program. In C#, it is possible to associate an event directly to a block of code statement at the time of event registration. Such code is termed as anonymous methods.
public static void Main (string[] args) { Car c1 = new Car ("car", 100, 80); c1.Exploded += delegate { Console.WriteLine("test anonymous method"); }; c1.Exploded += delegate(object sender, CarEventArgs e) { Console.WriteLine("test anonymouse method two"); }; }
3.1 Accessing local variable
Anonymous methods are able to access the local variables of the method that defines them.
public static void Main (string[] args) { int count = 0; Car c1 = new Car ("car", 100, 80); c1.Exploded += delegate { Console.WriteLine("test anonymous method"); count++; }; c1.Exploded += delegate(object sender, CarEventArgs e) { count++; Console.WriteLine("test anonymouse method two"); }; }
4. Understanding Lambda expression
Lambda expressions are nothing more than a very concise way to author anonymous methods and simplify how we work with .net delegate type. Before looking at the example, let‘s investigate the special delegate type in c# Predicate<T> which points to any method returning a bool and takes a single type parameter as the only input parameters. It is used in List<T> FindAll (Predicate<T> match) method to narrow down the list.
public static void TraditionalDelegateSyntax() { List<int> list = new List<int> (); list.AddRange(new int[]{20, 1, 4, 8, 3, 44}); Predicate<int> callback = new Predicate<int> (IsEvenNumber); List<int> eNumber = list.FindAll (callback); foreach (int i in eNumber) { Console.WriteLine (i); } } static bool IsEvenNumber(int i) { return (i % 2) == 0; }
Traditionally, we have a method IsEvenNumber to return bool value based on the input parameter. With lambda, we can improve the code with less keystrokes.
public static void AnonymouseMethodSyntax() { List<int> list = new List<int> (); list.AddRange(new int[]{20, 1, 4, 8, 3, 44}); List<int> eNumber = list.FindAll (delegate(int i) { return (i % 2) == 0; }); foreach (int i in eNumber) { Console.WriteLine (i); } }
public static void AnonymouseMethodSyntax() { List<int> list = new List<int> (); list.AddRange(new int[]{20, 1, 4, 8, 3, 44}); List<int> eNumber = list.FindAll (delegate(int i) { return (i % 2) == 0; }); List<int> eNumber1 = list.FindAll ( i => (i % 2) == 0); //lambda expression foreach (int i in eNumber) { Console.WriteLine (i); } }
In this case, rather that directly creating a Predicate<T> delegate type and then authoring a standalone method, we are able to inline a method anonymously.
4.1 Dissecting lambda expression
A lambda expression is written by first defining a parameter list, followed by the => token, followed by a set of statements that will process these arguments. It can be understood as ArgumentsToProcess => StatementsToProcessThem
List<int> eNumber1 = list.FindAll ( i => (i % 2) == 0); //lambda expression with implicit parameter type List<int> eNumber1 = list.FindAll ( (int i) => (i % 2) == 0); //explicitly state the parameter type
4.2 Processing arguments with multiple statements
C# allows you to build lambda expressions using multiple statement blocks.
List<int> eNumber1 = list.FindAll ( (int i) => { Console.WriteLine("test"); bool isEven = (i % 2) == 0; return isEven; });