C# Note2:委托(delegate) & Lambda表达式 & 事件(event)
前言
本文主要讲述委托和Lambda表达式的基础知识,以及如何通过Lambda表达式实现委托调用,并阐述.NET如何将委托用作实现事件的方式。
参考:C#高级编程
1.什么是委托(delegate)?
delegate是C#中的一种类型,它是一个能够持有对某个方法的引用的类。与其它类不同的是,delegate类能够拥有一个签名(signature),并且它"只能持有与其签名相匹配的方法的引用"。委托可以看成寻址方法的.NET版本(可对比C++中的函数指针进行理解),你可以传递类A的方法f给类B的对象,使得类B的对象能够调用这个方法f。不过,函数指针只是一个指向内存位置的指针(无法判断指针的实际指向,也无从知晓参数和返回类型等),不是类型安全的,而delegate是面向对象、类型安全、可靠的受控(managed)对象(即运行时能保证delegate指向一个有效的方法,无须担心它会指向无效地址或者越界地址)。
2.为什么要使用委托?
使用委托使程序员可以将方法引用封装在委托对象内。然后可以将该委托对象传递给可调用所引用方法的代码,而不必在编译时知道将调用哪个方法。与C或C++中的函数指针不同,委托是面向对象,而且是类型安全的。
2.1用的时机
当要把方法传递给其他方法时,需要使用委托。我们习惯于把数据作为参数传递给方法,如 int i = int.Parse("99");而有时某个方法执行的操作并不是针对数据进行,而是要对另外一个方法进行操作。在编译时,我们不知道第二个方法是什么,其信息只能在运行时得到。很明显的示例有:
- 启动线程和任务:在计算机并行运行某些新的执行序列的同时运行当前的任务,这样的序列就叫做线程。在其中一个基类System.Threading,Thread的一个实例上使用方法Start(),就可以启动一个线程。在告诉计算机启动一个新的执行序列时,必须为其提供开始启动的方法的细节,即Thread类的构造函数必须带有一个参数,该参数定义了线程调用的方法。
- 通用库类:许多库包含执行各种标准任务的代码。例如,需要编写一个类,它带有一个对象数组,将它们按照升序排列。但是,排序的部分过程会涉及到重复使用数组的两个对象进行比较。若要编写的类能够对任何对象进行排序,就无法提前告诉计算机应该如何比较对象。处理类中对象数组的客户端代码必须给类传递某个可以调用并进行这种比较的合适方法的细节。
在C/C++中,只能提取函数的地址,并作为一个参数传递它。这种直接方法不仅会导致一些关于类型安全性的问题,且没有意思到:在进行面向对象编程时,几乎没有方法是孤立存在的,在调用方法前通常需要与类实例相关联。.NET Framwork在语法上不允许使用这种直接方法。如果要传递方法,必须将方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊类型的对象,其特殊点在于一般的对象都包含数据,而它包含的只是一个或多个方法的地址。
2.2声明委托
C#中使用一个类的两个阶段:(1)定义类,告诉编译器该类由什么字段和方法组成;(2)(除非只使用静态方法)实例化类的一个对象。
委托也要经过这两个步骤。定义委托的语法:delegate void IntMethodInvoker(int x); (定义时必须给出它所表示的方法的签名和返回类型等全部细节,以保证高的类型安全性)
它表示的方法可以不带参数。因为定义委托基本上是定义一个新类,所以可以在定义类的任何相同地方定义委托(可以是任何类的内部或外部,还可以在名称空间中把委托定义为顶层对象)。
同样在其定义上,可以应用常见的访问修饰符:public、private、protected等。
2.3使用委托
private delegate string GetAStringent(); static void Main() { int x = 40; //x.ToString后面不能加(),否则就会返回一个不能赋予委托变量的字符串对象。只能将方法的地址赋予委托变量 GetAString firstStringMethod = new GetAString(x.ToString); //实例化类型为GetAString 的一个委托,并对它进行初始化,使它引用整型变量x的ToString()方法 Console.WriteLine("String is {0}", firstStringMethod()); //the above statement is equivalent to saying //Console.WriteLine("String is {0}", x.ToString()); }
给委托实例提供圆括号与调用委托类的Invoke()方法完全相同。即firstStringMethod()可以替换为firstStringMethod.Invoke()。
为了减少输入量,只要需要委托实例,就可以只传送地址的名称。这叫委托推断。
委托推断可以在需要委托实例的任何地方使用,它也可用于事件,因为事件基于委托(后面会讲到)
为了做此说明,对上面的代码进行扩展:
使用firstStringMethod委托在另一个对象上调用其它两个方法,一个是实例方法,一个是静态方法。
namespace Wrox.ProCSharp.Delegates { struct Currency { public uint Dollars; public ushort Cents; public Currency(uint dollars, ushort cents) { this.Dollars = dollars; this.Cents = cents; } public override string ToString() { return string.Format("${0}.{1,-2:00}", Dollars, Cents); } public static string GetCurrencyUnit() { return "Dollar"; } public static explicit operator Currency(float value) { //explicit关键字的作用是强制转换用户自定义的类型转换运算符.通常前面用static后面用operator,一般是把当前类型转换成另一个类型(将原类型的转换成目标类型) checked { uint dollars = (uint)value; ushort cents = (ushort)((value - dollars) * 100); return new Currency(dollars, cents); } } public static implicit operator float(Currency value) { //implicit关键字用于声明隐式的用户定义类型转换运算符。 如果可以确保转换过程不会造成数据丢失,则可使用该关键字在用户定义类型和其他类型之间进行隐式转换 return value.Dollars + (value.Cents / 100.0f); } public static implicit operator Currency(uint value) { return new Currency(value, 0); } public static implicit operator uint(Currency value) { return value.Dollars; } } }
下面使用GetAString实例:
using System; namespace Wrox.ProCSharp.Delegates { class Program { private delegate string GetAString(); static void Main() { int x = 40; GetAString firstStringMethod = x.ToString; Console.WriteLine("String is {0}", firstStringMethod()); Currency balance = new Currency(34, 50); // firstStringMethod references an instance method firstStringMethod = balance.ToString; Console.WriteLine("String is {0}", firstStringMethod()); // firstStringMethod references a static method firstStringMethod = new GetAString(Currency.GetCurrencyUnit); Console.WriteLine("String is {0}", firstStringMethod()); } } }
运行结果:
上面的例子,实际上并未说明委托的本质,或者说将一个委托传递给另一个方法的具体过程。下面继续举例说明,没有委托就难完成的工作:
namespace Wrox.ProCSharp.Delegates { class MathOperations { public static double MultiplyByTwo(double value) { return value * 2; } public static double Square(double value) { return value * value; } } }
using System; namespace Wrox.ProCSharp.Delegates { delegate double DoubleOp(double x); class Program { static void Main() { //实例化了一个委托数组DoubleOp DoubleOp[] operations = { MathOperations.MultiplyByTwo, MathOperations.Square }; for (int i = 0; i < operations.Length; i++) { Console.WriteLine("Using operations[{0}]:", i); ProcessAndDisplayNumber(operations[i], 2.0); ProcessAndDisplayNumber(operations[i], 7.94); ProcessAndDisplayNumber(operations[i], 1.414); Console.WriteLine(); } } static void ProcessAndDisplayNumber(DoubleOp action, double value) { double result = action(value); //调用action委托实例封装的方法,返回结果存储在result中 Console.WriteLine( "Value is {0}, result of operation is {1}", value, result); } } }
上例说明了使用委托的一种方式:把方法组合到一个数组中,就可以循环调用不同的方法。
- BubbleSorter示例
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Wrox.ProCSharp.Delegates { class BubbleSorter { static public void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison) { //冒泡排序的思想:重复遍历数组,比较每一对数字,交换位置,从而把最大或者最小的数字逐步移动到数组的最后! bool swapped = true; do { swapped = false; for (int i = 0; i < sortArray.Count - 1; i++) { if (comparison(sortArray[i+1], sortArray[i])) { T temp = sortArray[i]; sortArray[i] = sortArray[i + 1]; sortArray[i + 1] = temp; swapped = true; } } } while (swapped); } } }
其中,使用委托Func<T1, T2, TResult>传递一个封装的方法,用于比较两个新类的大小。
需要定义另一个类,建立起要排序的数组。这里构造一个员工列表,根据他们的薪水进行排序:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Wrox.ProCSharp.Delegates { class Employee { public Employee(string name, decimal salary) { this.Name = name; this.Salary = salary; } public string Name { get; private set; } public decimal Salary { get; private set; } public override string ToString() { return string.Format("{0}, {1:C}", Name, Salary); } public static bool CompareSalary(Employee e1, Employee e2) { return e1.Salary < e2.Salary; } } }
为了匹配Func<T, T, bool>委托的签名,在该类中必须定义CompareSalary,它的参数是两个Employee引用,并返回一个布尔值。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Wrox.ProCSharp.Delegates { class Program { static void Main() { Employee[] employees = { new Employee("Bugs Bunny", 20000), new Employee("Elmer Fudd", 10000), new Employee("Daffy Duck", 25000), new Employee("Wile Coyote", 1000000.38m), new Employee("Foghorn Leghorn", 23000), new Employee("RoadRunner", 50000) }; BubbleSorter.Sort(employees, Employee.CompareSalary); foreach (var employee in employees) { Console.WriteLine(employee); } } } }
- 多播委托
前面的每个委托都只包含了一个方法调用。如果要调用多个方法,则需要多次显式调用这个委托。委托也可以包含多个方法,称为多播委托。
using System; namespace Wrox.ProCSharp.Delegates { class MathOperations { public static void MultiplyByTwo(double value) { double result = value * 2; Console.WriteLine("Multiplying by 2: {0} gives {1}", value, result); } public static void Square(double value) { double result = value * value; Console.WriteLine("Squaring: {0} gives {1}", value, result); } } }
using System; namespace Wrox.ProCSharp.Delegates { class Program { static void Main() { Action<double> operations = MathOperations.MultiplyByTwo; operations += MathOperations.Square; ProcessAndDisplayNumber(operations, 2.0); ProcessAndDisplayNumber(operations, 7.94); ProcessAndDisplayNumber(operations, 1.414); Console.WriteLine(); } static void ProcessAndDisplayNumber(Action<double> action, double value) { Console.WriteLine(); Console.WriteLine("ProcessAndDisplayNumber called with value = {0}", value); action(value); } } }
通过多播委托调用多个方法可能导致一个大问题:多播委托包含一个逐个调用的委托集合,如果通过委托调用其中一个方法抛出异常,整个迭代就会停止。
另外,如果我们要获得委托集合所有的返回值,可以使用GetInvocationList方法(Returns the invocation list of this multicast delegate, in invocation order.)。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Wrox.ProCSharp.Delegates { class Program { static void One() { Console.WriteLine("One"); throw new Exception("Error in one"); } static void Two() { Console.WriteLine("Two"); } static void Main() { Action d1 = One;//Action:Encapsulates a method that has no parameters and does not return a value. d1 += Two; Delegate[] delegates = d1.GetInvocationList(); foreach (Action d in delegates) { try { d(); } catch (Exception) { Console.WriteLine("Exception caught"); } } } } }
3.Lambda表达式与委托的关系?
两者是直接相关的,当参数是委托类型时,就可以使用Lambda表达式实现委托引用的方法。
using System; namespace Wrox.ProCSharp.Delegates { class Program { static void Main() { SimpleDemos(); int someVal = 5; Func<int, int> f = x => x + someVal; someVal = 7; Console.WriteLine(f(3)); } static void SimpleDemos() { Func<string, string> oneParam = s => String.Format("change uppercase {0}", s.ToUpper()); Console.WriteLine(oneParam("test")); Func<double, double, double> twoParams = (x, y) => x * y; Console.WriteLine(twoParams(3, 2)); Func<double, double, double> twoParamsWithTypes = (double x, double y) => x * y; Console.WriteLine(twoParamsWithTypes(4, 2)); Func<double, double> operations = x => x * 2; operations += x => x * x; ProcessAndDisplayNumber(operations, 2.0); ProcessAndDisplayNumber(operations, 7.94); ProcessAndDisplayNumber(operations, 1.414); Console.WriteLine(); } static void ProcessAndDisplayNumber(Func<double, double> action, double value) { double result = action(value); Console.WriteLine( "Value is {0}, result of operation is {1}", value, result); } } }
4.委托和事件的关系及区别?
事件基于委托,为其提供了一种发布/订阅机制,换句话说事件是特殊类型的多路广播委托,仅可从声明它们的类或结构(发布者类)中调用。如果其他类或结构订阅了该事件,则当发布者类引发该事件时,会调用其事件处理程序方法。
不过,event在delegate的基础上作了两点限制:
- 外部类只能看到和使用委托所提供的+=和-=行为,不能直接对其赋值(即=操作),即使继承类也是如此,这样可以不影响委托对其他观察者的通知.(委托的invoke或GetInvocationList等方法在event中不能使用)
- 只有声明类可以调用(或触发)一个事件,外部类不可以直接调用其事件。
思考:如何方便地移除事件的订阅?
参考:通过反射实现
4.1事件发布程序
示例:事件用于连接CarDealer类和Consumer类。CarDealer类提供一个新车到达时触发的事件。Consumer类订阅该事件,以获得新车到达的通知。
using System; namespace Wrox.ProCSharp.Delegates { public class CarInfoEventArgs : EventArgs { public CarInfoEventArgs(string car) { this.Car = car; } public string Car { get; private set; } } public class CarDealer { public event EventHandler<CarInfoEventArgs> NewCarInfo; public void NewCar(string car) { Console.WriteLine("CarDealer, new car {0}", car); if (NewCarInfo != null) { NewCarInfo(this, new CarInfoEventArgs(car)); } } } }
public event EventHandler<CarInfoEventArgs> NewCarInfo; 定义事件是C#的简化记法。编译器会创建一个EventHandler<CarInfoEventArgs>委托类型的变量,并添加方法,以便从委托中订阅和取消。该简化记法的较长形式如下所示:
public delegate EventHandler<CarInfoEventArgs> NewCarInfo; public event EventHandler<CarInfoEventArgs> NewCarInfo { add { newCarInfo += value; } remove { newCarInfo = value; } }
4.2事件侦听器
using System; namespace Wrox.ProCSharp.Delegates { public class Consumer { private string name; public Consumer(string name) { this.name = name; } public void NewCarIsHere(object sender, CarInfoEventArgs e) { Console.WriteLine("{0}: car {1} is new", name, e.Car); } } }
连接事件发布程序和订阅器:使用CarDealer类的NewCarInfo事件,通过“+=”创建一个订阅。通过“-=”取消订阅。
namespace Wrox.ProCSharp.Delegates { class Program { static void Main() { var dealer = new CarDealer(); var michael = new Consumer("Michael"); dealer.NewCarInfo += michael.NewCarIsHere; //消费者michael(变量)订阅了事件 dealer.NewCar("Mercedes"); //一辆Mercedes到达,Michael得到了通知 var nick = new Consumer("Nick"); dealer.NewCarInfo += nick.NewCarIsHere; dealer.NewCar("Ferrari"); dealer.NewCarInfo -= michael.NewCarIsHere; dealer.NewCar("Toyota"); } } }
4.3弱事件
通过事件,直接连接到发布程序和侦听器,这样垃圾回收会存在问题:如果侦听器不再直接引用,发布程序仍有一个引用。垃圾回收器不能清空侦听器的内存,因为发布程序仍保有一个引用,会针对侦听器触发事件。
这种强连接可以通过弱事件模式来解决,即使用WeekEventManager作为发布程序和侦听器之间的中介。
- 弱事件管理器
using System.Windows; namespace Wrox.ProCSharp.Delegates { public class WeakCarInfoEventManager : WeakEventManager { public static void AddListener(object source, IWeakEventListener listener) { CurrentManager.ProtectedAddListener(source, listener); } public static void RemoveListener(object source, IWeakEventListener listener) { CurrentManager.ProtectedRemoveListener(source, listener); } public static WeakCarInfoEventManager CurrentManager //静态属性CurrentManager创建了一个WeakCarInfoEventManager类型的对象(如果它不存在),并返回对该对象的引用 { get { WeakCarInfoEventManager manager = GetCurrentManager(typeof(WeakCarInfoEventManager)) as WeakCarInfoEventManager; if (manager == null) { manager = new WeakCarInfoEventManager(); SetCurrentManager(typeof(WeakCarInfoEventManager), manager); } return manager; } } protected override void StartListening(object source) //重写:添加第一个侦听器时调用该方法 { (source as CarDealer).NewCarInfo += CarDealer_NewCarInfo; } void CarDealer_NewCarInfo(object sender, CarInfoEventArgs e) { DeliverEvent(sender, e); //把事件传递给侦听器 } protected override void StopListening(object source) //重写:删除最后一个侦听器时调用该方法 { (source as CarDealer).NewCarInfo -= CarDealer_NewCarInfo; } } }
- 事件侦听器
using System; using System.Windows; namespace Wrox.ProCSharp.Delegates { public class Consumer : IWeakEventListener //实现IWeakEventListener接口 { private string name; public Consumer(string name) { this.name = name; } public void NewCarIsHere(object sender, CarInfoEventArgs e) { Console.WriteLine("{0}: car {1} is new", name, e.Car); } bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) //触发事件时,从弱事件管理器中调用IWeakEventListener定义的ReceiveWeakEvent方法 { NewCarIsHere(sender, e as CarInfoEventArgs); return true; } } }
Main()方法:
namespace Wrox.ProCSharp.Delegates { class Program { static void Main() { var dealer = new CarDealer(); var michael = new Consumer("Michael"); WeakCarInfoEventManager.AddListener(dealer, michael); dealer.NewCar("Mercedes"); var nick = new Consumer("Nick"); WeakCarInfoEventManager.AddListener(dealer, nick); dealer.NewCar("Ferrari"); WeakCarInfoEventManager.RemoveListener(dealer, michael); dealer.NewCar("Toyota"); } } }
实现了弱事件模式后,发布程序和侦听器就不再强连接了。当不再引用侦听器时,他就会被垃圾回收。