本章包含的如下认知内容
委托概述
如何声明委托?
如何使用委托?
Action
多播委托是什么?
匿名方法
委托概述:
委托是寻址方法的.Net版本,是类型安全的类,它定义了返回类型和参数类型。委托类不仅包含对单个方法的引用,也可以包含对多个方法的引用。
委托是一个特殊类型的对象,其特殊之处在于,我们之前定义的所有对象包含数据,而委托包含的只是一个或多个方法的地址。
委托和lambda表达式直接相关,当参数是委托的时候,我们可以使用lamabda表达式实现委托引用的方法。
如何声明委托?
我们都知道在C#中使用一个类的时候分为两个阶段。首先,需要定义这个类(告诉编译器这个类由什么字段和方法组成)。然后实例化类的一个对象(静态方法除外)。使用委托时,我们也需要经过类似以上的两个步骤。首先必须定义要使用的委托,对于委托,定义它就是告诉编译器这种类型的委托表示哪种类型的方法。然后,必须创建该委托的一个或多个实例。编译器在后台将创建表示该委托的类。
委托的基本语法如下:
delegate void IntMethodInvoker(int x);
注释:上述基本语法案例是说定义了一个委托IntMethodInvoker,并且指定了该委托的每个实例都可以包含一个方法(该方法带有一个int参数)的引用,并且方法返回值是void(空)。理解委托的一个要点是他们的类型安全性非常高。在定义委托时,必须给出它所表示的方法的签名和返回类型等全部细节。说白了就是给方法的签名和返回类型指定名称。
假如我们有如下需求:有一个方法需要使用委托,该方法的接收两个long型参数,并且返回值是一个double类型,那我我们为该方法建立的委托如下:
delegate double TwoLongsOp(long first, long second);
注释:其语法类似于方法的定义,但是没有方法体。定义的前面需要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何相同地方定义委托。也就是说,可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在名称空间中把委托定义为顶层对象。根据定义的可见性和委托的作用域,可以在委托的定义上应用任意常用的访问修饰符:Public、private、protected等。
实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生自基类System.MulticastDelegate的类,System.MulticastDelegate又派生自基类System.Delegate.C#编译器能识别这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况。这是C#与基类共同合作,使编程更易完成的另一个范例。
定义好委托后,就可以创建它的一个实例,从而用它存储特定方法的细节。
但是,在术语方面有一个问题,类有两个不同的术语:“类”表示较广义的定义,“对象”表示类的实例。但委托只有一个术语。在创建委托的实例,所创建的委托的实例仍称为委托,必须从上下文中确定委托的确切含义。
如何使用委托:
初接触:
下面这段代码说明如何使用委托(简单应用):
using System; namespace Wrox.ProCSharp.Delegates { class Program { private delegate string GetAString(); static void Main() { int x = 40; GetAString firstStringMethod = new GetAString(x.ToString); Console.WriteLine("String is {0}", firstStringMethod.Invoke()); Console.ReadLine(); } } }
在这段代码中,实例化了类型为GetString的一个委托,并对它进行初始化,使它引用整型变量x的ToString()方法。在C#中,委托在语法上总是接收一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中,如果不带x.ToString来初始化变量firstStringMethod变量会报编译错误。注意:因为int.ToString()是一个实例方法(不是静态方法),所以需要指定实例(x)和方法名来正确地初始化委托。
在上述代码中的firstStringMethod.Invoke()和firstStringMethod()方法是会产生完全相同的效果的。Invoke()是委托类的方法。因为firstStringMethod是委托类型的一个变量,所以C#编译器会用firstStringMethod.Invoke()代替firstStringMethod()。
为了减少输入量,只要需要委托实例,就可以只传送地址名称。这就称为委托推断。只要编译器可以把委托实例解析为特定的类型,这个C#特性就是有效的,所以上面的代码还有另外一种写法:
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.Invoke()); Console.ReadLine(); } } }
注意:调用上述方法名时输入形式不能为x.ToString()(不要输入圆括号),也不能把它传送给委托变量。输入圆括号调用一个方法。调用x.ToString()方法会返回一个不能赋予委托变量的字符串对象。只能把方法的地址赋予委托变量。
动静态方法的委托调用:
委托的一个特征是它们的类型是安全的,可以确保被调用的方法签名是正确的。但是它不关心在什么类型的对象上调用该方法,甚至不考虑该方法是静态方法,还是实例方法。
首先建立一个动静态都包含的Currency.cs类:
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"; } } }
然后使用使用GetAString实例:
using System; namespace Wrox.ProCSharp.Delegates { class Program { private delegate string GetAString(); static void Main() { Currency balance = new Currency(34, 50); //firstStringMethod reference an instance method GetAString firstStringMethod = balance.ToString; Console.WriteLine("String is{0}", firstStringMethod.Invoke()); //firstStringMethod reference a static method firstStringMethod = Currency.GetCurrencyUnit; Console.WriteLine("String is {0}", firstStringMethod.Invoke()); Console.ReadLine(); } } }
注释:上面说明了如何通过委托调用动静态方法,只要每个方法的签名匹配委托定义即可。
简单的委托示例
首先新建一个包含正方形和长方形的计算方法的类MathOperation.cs,该类包含动静态两种方法:
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[] operations ={ MathOperations.MultiplyByTwo, MathOperations.Square }; for (int i = 0; i < operations.Length; i++) { Console.WriteLine("Using operation[{0}]", i); ProcessAndDisplayNumber(operations[i], 2.0); ProcessAndDisplayNumber(operations[i], 7.94); ProcessAndDisplayNumber(operations[i], 1.414); Console.WriteLine(); } Console.ReadLine(); } static void ProcessAndDisplayNumber(DoubleOp action, double value) { double result = action(value); Console.WriteLine("Value is {0},result of operation is {1}", value, result); } } }
运行结果:
Using operation[0] Value is 2,result of operation is 4 Value is 7.94,result of operation is 15.88 Value is 1.414,result of operation is 2.828 Using operation[1] Value is 2,result of operation is 4 Value is 7.94,result of operation is 63.0436 Value is 1.414,result of operation is 1.999396
注释:在这段代码中,实例化了一个DoubleOp委托数组(需要我们了解的是,一旦定义了委托类,基本上就可以实例化它的实例,就像处理一般的类那样--所以把一些委托的实例放在数组中是可以的)。该数组的每个元素都初始化为由MathOperation类实现的不同操作。然后遍历这个数组,每个操作应用到三个不同的值上。这就就说了使用委托的一种方式--把方法组合到一个数组中来使用,这样就可以在循环中调用不同的方法了。
简单案例的代码解析:
这段代码的最为关键的一行是把每个委托传递给ProcessAndDisplayNumber方法,比如:
ProcessAndDisplayNumber(operations[i], 2.0);
其中传递了委托名,但不带任何参数。其语法是:
1、operations[i]表示“这个委托”,也可以说成是委托表示的方法。
2、2.0表示的是一个参数。
3、然后调用 double result = action(value);这个实际上是调用委托实例封装的方法。其返回结果存储在result中。
Action和Func委托
除了为每个参数和返回类型定义一个新委托类型之外,还可以使用Action
泛型Action
Func
因此利用Func
using System; namespace Wrox.ProCSharp.Delegates { class Program { static void Main() { Func[] operations ={ MathOperations.MultiplyByTwo, MathOperations.Square }; for (int i = 0; i < operations.Length; i++) { Console.WriteLine("Using operation[{0}]", i); ProcessAndDisplayNumber(operations[i], 2.0); ProcessAndDisplayNumber(operations[i], 7.94); ProcessAndDisplayNumber(operations[i], 1.414); Console.WriteLine(); } Console.ReadLine(); } static void ProcessAndDisplayNumber(Func action, double value) { double result = action(value); Console.WriteLine("Value is {0},result of operation is {1}", value, result); } } }
多播委托
如果要调用多个方法,就需要多次显示调用这个委托。但是,委托也可以包含多个方法。这种包含多个方法的委托就被称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托签名就必须返回void。否则,就只能得到委托调用的最后一个方法结果。
我们明显可以利用的是返回类型为void的Action
首先建立一MathOperations.cs类:
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() { Actionoperations = MathOperations.MultiplyByTwo; operations += MathOperations.Square; ProcessAndDispalyNumber(operations, 2.0); ProcessAndDispalyNumber(operations, 7.94); ProcessAndDispalyNumber(operations, 1.414); Console.WriteLine(); Console.ReadLine(); } static void ProcessAndDispalyNumber(Action action, double value) { Console.WriteLine(); Console.WriteLine("ProcessAndDispalyNumber called with value ={0}", value); action(value); } } }
注释:多播委托可以识别运算符“+”和“+=”、“-”和“-=”。多播委托将两个执行方法通过“+”和“+=”连在一起(叫做扩展),还可以利用“-”和“-=”删除方法调用。多播实际上是一个派生自System.MulticastDelegate的类,System.MuticastDelegate又派生自基类System.Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接一个列表。
注意:如果使用多播委托,我们需要了解对同一个委托调用方法链的顺序并未正式定义。因此应避免依赖于以特定顺序调用方法代码。还有个问题是多播委托包含一个逐个调用的委托集合。如果通过委托调用其中的一个方法抛出异常,则整个迭代都会停止。
下面将展示如果多播委托第一个抛出错误第二个方法无法正常执行的案例:
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; d1 += Two; try { d1(); } catch (Exception) { Console.WriteLine("Exception caught"); } } } }
注释:one()方法我们故意加了个异常问题,那么执行Main()的时候会自动的在One()方法执行的时候进入catch
我们如果解决上面的这种多播委托存在的问题的,我们可以使用Delegate定义GetInvocationList()方法,返回一个Delegate对象数组。然后循环迭代方法列表,捕获异常进行下次迭代。下面代码是重写后的Main()函数:
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; d1 += Two; Delegate[] delegates = d1.GetInvocationList(); foreach (Action d in delegates) { try { d.Invoke(); } catch (Exception) { Console.WriteLine("Exception caught"); } } } } }
匿名方法:
这是C# 2.0之前的表示方式,在C#3.0里面可以使用lambda表达式代替匿名函数。我们熟悉的是要想使用委托工作,方法必须是已经存在的。但是还有另外一种使用委托的方式:匿名方法
实际上匿名方法与实例委托的区别只是在实例化委托的时候,那么我们首先先看下面的代码是如何使用匿名方法的:
using System; namespace Wrox.ProCSharp.Delegates { class Program { static void Main() { string mid = ", middle part,"; FuncanonDel = delegate(string param) { param += mid; param += " and this was added to the string."; return param; }; Console.WriteLine(anonDel("Start of string")); } } }
注释:Func
在使用匿名方法的时候,必须遵循的原则有两条:一是不能使用跳转语句(break、go或continue)跳转到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳转到该匿名方法的内部。二是在匿名方法的内部不能访问不安全的代码,另外也不能访问匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。
注意:如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法了。此时与复制代码相比,编写一个命名方法比较好,因为该方法只需要编写一次,以后可以通过名称引用它。
实战的应用委托BubbleSorter
为了说明委托的实际用途。我们编写一个类BubbleSorter,它实现一个静态方法Sort(),这个方法的第一参数是一个对象数组,把该数组按照升序重新排列。
需求是本公司员工的工资需要按照需要的顺序进行排序:
首先定义一个BubbleSorter类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Wrox.ProCSharp.Delegates { class BubbleSorter { static public void Sort(IList sortArray, Func 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); } } }
定义员工类,该员工类Employee.cs如下:
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; } } }
编写客户端代码,完成我们需要排序的员工数据:
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); } Console.ReadLine(); } } }
排序后我们员工的工资如下:
Elmer Fudd, ¥10,000.00 Bugs Bunny, ¥20,000.00 Foghorn Leghorn, ¥23,000.00 Daffy Duck, ¥25,000.00 RoadRunner, ¥50,000.00 Wile Coyote, ¥1,000,000.38
说明:如果对您有所帮助请点赞,如果需要帮助请留言。