c#之委托
委托
前言:当要把方法传递给其他的方法时,需要使用委托。要了解他们的含义,可以看看下面的一行代码:
int number = int.Parse("99");
我们习惯于把数据作为参数传递给方法,如上面的例子所示。所以,给方法传递另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要针对另一个方法进行操作。更麻烦的是,在编译时我们不知道第二个方法是什么,这个信息只能在运行时知道,所以需要把第二个方法中作为参数传递个第一个方法。
一:委托的声明:
使用委托时,也需要经过这两个步骤:(1):首先要定义使用的委托,对于委托,定义它就是要告诉我们的编译器这种类型的委托表示哪种类型的方法。(2):创建该委托的一个或者多个实例,定义委托的语法如下:
delegate void IntMethodInvoker(int x);
在这个实例中,定义了一个IntMethodInvoker,并指向该委托的实例的每个实例都可以包含一个方法的引用,该方法带有一个int类型的参数,并返回void。理解委托的一个要点是它们的类型是非常安全的。在定义委托时,必须给出它所表示的方法的签名和返回类型等全部的细节。
理解委托的一种好方式就是把委托当作是这样的一件事情,它给方法的签名和返回类型指定名称。
假定我们要定义一个委托TwoLongsOp,该委托表示的方法有两个long型参数,返回类型为double,代码如下:
delegate double TwoLongsOp(long first, long second);
或者是定义一个委托,它表示的方法不带参数,返回一个string类型的值,可以如下的编写代码:
delegate string GetAString();
其语法类似于方法的定义,但是没有方法体,定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何的地方定义委托。也就是说,可以在类的内部定义,也可以在类的外部定义,还可以在命名空间中把委托定义为顶层对象。可以在委托的定义应用任意常见的访问修饰符:public private protected。
定义好一个委托后,就可以创建它的一个实例,从而用它存储特定方法的细节。
给定委托的实例可以引用任何类型的任何对象上的实例方法或者静态方法——主要方法的签名匹配与委托的签名。
二:简单的委托的实例:
定义一个数学运算类(MathOperations),它有两个静态的方法,对double类型的值执行两个操作。然后使用委托调用这些方法,数学类代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _12简单的委托的实例 { /// <summary> /// 数学类 /// </summary> public class MathOperstions { /// <summary> /// 该方法是数字乘以2 /// </summary> /// <param name="value">数字</param> /// <returns>返回相乘的数字</returns> public static double MultiplyByTwo(double value) { return value * 2; } /// <summary> /// 数字的平方的方法 /// </summary> /// <param name="value">传入的数字</param> /// <returns>返回数值</returns> public static double Square(double value) { return value*value; } } }
在这里我们可以实例化一个DoubleOp委托的数组,一旦定义了委托类,基本上就可以实例化它的实例,就像处理一般的类那样——所以把一些委托的实例放在数组中是可以的。该数组的每个元素都初始化为由MathOperations类实现的不同操作。然后遍历这个数组,把每个操作应用于3个不同的值上。这说明了使用委托的一种方式——把方法组合到一个数组中来使用。这样就可以在循环中调用不同的方法了。
这段代码的关键是把每个委托传递给ProcessAndDisplayNumber方法,例如:
ProcessAndDisplayNumber(operations[i], 2.0);
其中传递了委托名但是不带任何的参数。假定operations[i]是一个委托,其语法是:
operations[i]表示这个“委托”。换言之,就是表示委托的方法。
operations[i](2.0):表示实际调用这个方法,参数放在圆括号中。
ProcessAndDisplayNumber 方法定义为把一个委托作为其中的一个参数:
static void ProcessAndDisplayNumber(DoubleOp action, double value)
然后在这个方法中调用:
//委托的调用 double result = action(value);
客户端代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; namespace _12简单的委托的实例 { class Program { /// <summary> /// 委托的定义 /// </summary> /// <param name="x"></param> /// <returns></returns> private delegate double DoubleOp(double x); static void Main(string[] args) { //实例化委托的数组 DoubleOp[] operations = { MathOperstions.MultiplyByTwo, MathOperstions.Square }; //Func<double, double>[] operations = {MathOperstions.MultiplyByTwo, MathOperstions.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(); } Console.ReadKey(); } /// <summary> /// 处理和显示数量的方法,把一个委托定义作为其中的一个参数 /// </summary> /// <param name="action">委托的类型</param> /// <param name="value">值</param> static void ProcessAndDisplayNumber(DoubleOp action, double value) { //委托的调用 double result = action(value); Console.WriteLine("Value is {0},result of operstion is {1}", value, result); } } }
运行的结果:
三:Action<T>和Func<T>委托:
除了为每个参数和返回类型定义一个新委托类型时,还可以使用Action<T>和Func<T>委托。泛型Action<T>委托表示引用一个void返回类型的方法。这个委托可以存在不同的变体。可以传递至16种类型不同的参数类型。没有泛型参数的Action类可调用没有参数的方法(指无参数无返回值)。
Func<T>委托可以已类似的方式使用,Func<T>允许调用带返回类型的方法。与Action<T>类似,Func<T>也定义了不同的个体,至多可以传递16个参数类型和一个返回类型。Func<out TResult>委托类型可以调用带返回类型且无参数的方法。Func<in T,out TResult>:表示带一个参数的方法。
在小结的实例中我们是声明了一个委托:
delegate double DoubleOp(double x);
同样的我们也可以这样声明:
private Func<double, double> Del;
当然除了自定义委托DoubleOp之外,我们还可以使用Func<in T,out TResult>,可以声明一个该委托类型的变量或者是该委托类型的数组。如下所示:
Func<double, double>[] operations = {MathOperstions.MultiplyByTwo, MathOperstions.Square};
我们还可以定义数组:
Func<double, double>[] opers = { MathOperstions.MultiplyByTwo, MathOperstions.Square }
四:冒泡分类法示例:
下面我们将展示委托的真正的用途,我们要编写一个BubbleSorter(冒泡分类法),它将实现一个静态的Sort();这个方法的第一个参数是一个数组对象,把该数组按照升序重新的进行排列,例如:假定我们的传递的是{0,5,6,2,1},则返回的结果是{0,1,2,5,6}。首先我们先来实现简单的冒泡排序:
for (int i = 0; i < nums.Length - 1; i++) { for (int j = 0; j < nums.Length - 1 - i; j++) { if (nums[j] > nums[j + 1]) { int temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; } } }
它非常适用于int,但是我们希望Sore()方法能给任何对象排序。换言之,如果某段客户端代码包含定义类的其他类和结构的数组,就需要对该数字进行排序。这样,上面的代码if(nums[j]>nums[j+1])就会有问题了。因为他需要比较数组中的两个对象,看看哪一个更大。可以对 int进行这样的比较,但如何对没有实现的"<"运算符的新类进行比较?答案是能识别该类的客户端的代码必须在委托中传递一个封装的方法,这个方法可以进行比较。另外,不给temp变量使用int类型,而是使用泛型类型即可以实现泛型方法Sort()。
对于接收类型T的泛型方法Sore<T>,就需要一个比较的方法,其中两个参数是T,if比较返回的是布尔类型。这个方法我们可以使用Func<T,T, out TResult>委托中引用。所以我们给Sore<T>方法指定如下的签名:
public static void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison)
这个方法说明,comparison必须引用一个方法,该方法带有两个参数,如果第一个参数的值”小于“第二个参数的值,就返回true,进行交换。完整的代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _13泡沫分类法 { class BubbleSorter { /// <summary> /// 在一个数组中比较两个数字的大小(进行升序的排序) /// </summary> /// <typeparam name="T">泛型</typeparam> /// <param name="sortArray">要比较的数组</param> /// <param name="comparison">比较的方法(委托类型有参数一个返回的类型)</param> public static void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison) { //冒泡排序:第一次要遍历的趟数 for (int i = 0; i < sortArray.Count - 1; i++) { //第二次要具体的比较 for (int j = 0; j < sortArray.Count - 1 - i; j++) { //委托指向的方法的使用:这个方法要比较两个参数的大小,如果返回true就进行交换 if (comparison(sortArray[i + 1], sortArray[i])) { //变量的交换(面试题) T temp = sortArray[i]; sortArray[i] = sortArray[i + 1]; sortArray[i + 1] = temp; } } } } } }
为了使用这个类,从而要建立要排序的数组,在本例中,假定移动电话公司有一个员工列表,要根据他们的薪水进行排序。完整的代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _13泡沫分类法 { /// <summary> /// 员工薪资 /// </summary> class Employee { //员工的名字 private string _name; public string Name { get { return _name; } set { _name = value; } } //薪资 private decimal _salary; public decimal Salary { get { return _salary; } set { _salary = value; } } /// <summary> /// 构造函数的定义 /// </summary> /// <param name="name">名字</param> /// <param name="salary">工资</param> public Employee(string name, decimal salary) { this.Name = name; this.Salary = salary; } /// <summary> /// 重写的字符串 /// </summary> /// <returns></returns> public override string ToString() { return string.Format("{0},{1}", this.Name, this.Salary); } /// <summary> /// 两个对象的比较 /// </summary> /// <param name="e1"></param> /// <param name="e2"></param> /// <returns></returns> 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; using System.Threading.Tasks; namespace _13泡沫分类法 { class Program { static void Main(string[] args) { //员工列表的数组 Employee[] employees = { new Employee("张三", 2000), new Employee("李四", 1000), new Employee("王五", 2500), new Employee("赵六", 10000.5m), new Employee("田七", 5000) }; //员工薪资的比较 Employee.CompareSalary:要比较的方法 BubbleSorter.Sort(employees, Employee.CompareSalary); //输出所有的员工的薪资 foreach (var employee in employees) { Console.WriteLine(employee); } Console.ReadKey(); } } }
五:多播委托:
前面使用的每个委托都只包含一个方法的调用。调用委托的次数与调用方法的次数相同。如果我们想要调用多个方法,就需要多次显示调用这个委托。但是委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托。就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void;否则就只能得到委托调用的最后一个方法的结果。
根据上面的例子我们也可以这样声明委托:
Action<double> operation = MathOperstions.MultiplyBy2; operation += MathOperstions.Square2;
多播委托可以识别运算符+和+=,而可以识别-和-=,以便从委托中删除方法调用。
为了说明多播委托的用法,现在需要委托引用返回为void的方法,我们重新声明一个MathOperstions类,让他们显示其结果,而不是返回他们,具体的代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _12简单的委托的实例 { /// <summary> /// 数学类 /// </summary> public class MathOperstions { /// <summary> /// 该方法是数字乘以2 /// </summary> /// <param name="value">数字</param> /// <returns>返回相乘的数字</returns> public static void MultiplyBy2(double value) { double res = value * 2; Console.WriteLine("具体的结果:{0}", res); } /// <summary> /// 数字的平方的方法 /// </summary> /// <param name="value">传入的数字</param> /// <returns>返回数值</returns> public static void Square2(double value) { double res2 = value * value; Console.WriteLine("具体的结果:{0}", res2); } } }
using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; namespace _12简单的委托的实例 { class Program { /// <summary> /// 委托的定义 /// </summary> /// <param name="x"></param> /// <returns></returns> private delegate double DoubleOp(double x); static void Main(string[] args) { Action<double> operationes = MathOperstions.MultiplyBy2; operationes += MathOperstions.Square2; //ProcessAndDisplayNumber(operationes, 2.0); operationes(2.0); operationes(5); Console.ReadKey(); } } }
最后的结果是: 4 4 10 25.
如果正在使用多播委托,就应该知道对同一个委托调用方法链的顺序并没有正式的定义。因此应避免编写依赖于已特定顺序调用方法的代码。
通过一个委托调用多个的方法还可能导致一个大的问题,多播委托包含一个逐个调用的委托的集合。如果通过委托的调用的其中的一个方法抛出异常。整个迭代就会停止。其中定义了两个简单的静态的方法,其中无参数无返回值。首先我们先看具体的代码的实现:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _14多播委托 { class ClassOne { public static void One() { Console.WriteLine("One"); throw new Exception("Error in one"); } public static void Two() { Console.WriteLine("Two"); } } }
在Main()方法中,创建了委托d1,它引用了方法One();接着把Two();方法的地址添加到同一个委托中。调用d1委托,就可以调用两个方法。在try/catch中进行捕获。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _14多播委托 { class Program { static void Main(string[] args) { Action d1 = ClassOne.One; d1 += ClassOne.Two; #region 异常捕获 try { d1(); } catch (Exception) { Console.WriteLine("未处理的异常捕获"); } #endregion Console.ReadKey(); } } }
显示的方法如下:
这时候我们发现委托只是调用了第一个方法,因为第一个方法抛出了一个异常,所以委托的迭代就会停止,不在调用Two();的方法。没有指定调用方法的顺序时,结果会有所不同。
在这种情况下,为了避免这个问题,应该自己迭代方法列表。Delegate类定义了GetInvocationList()方法,它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,进行下一次的迭代。
客户端代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _14多播委托 { class Program { static void Main(string[] args) { Action d1 = ClassOne.One; d1 += ClassOne.Two; // GetInvocationList:按照调用的顺序返回此多路广播委托的调用列表 Delegate[] delegates = d1.GetInvocationList(); foreach (Action d in delegates) { try { d(); } catch (Exception) { Console.WriteLine("未知的异常捕获"); } } Console.ReadKey(); } } }
调用的截图如下:
六:匿名方法:
到现在为止,要想使用委托工作,方法必须已经存在(即委托是用它将调用的方法的相同签名定义的)。但是还有另一种使用委托的方式:即通过匿名方法。匿名方法是用作委托的参数的一段代码。我们先来看一段代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _15匿名方法 { class Program { static void Main(string[] args) { string mid = ",middle part,"; Func<string, string> anonDel = delegate (string param) { param += mid; param += "and this was added to the string"; return param; }; Console.WriteLine(anonDel("Start of string")); Console.ReadKey(); } } }
Func<string,string>委托接收一个字符串的参数,返回类型也是字符串类型。anonDel是这种委托类型的变量。不是把方法名赋予这个变量,而是使用一段简单的代码:它前面的关键字使用delegate,后面是一个字符串参数。
匿名方法的优点是减少了要编写的代码。不必定义仅有委托使用的方法。在为事件定义委托时,这是非常的明显的。这有助于降低代码的复杂性。尤其是定义好几个事件的时候,代码会显得非常的简单。使用匿名方法时,代码的执行的速度并没有加快。编译扔定义了一个方法,该方法只有一个自动指定的名称,我们不需要在知道这个名称。
在使用匿名方法时,必须遵循两条规则。在匿名方法中不能使用跳转语句(break goto 或者 continue)调到该署名方法的外部。反之亦然。匿名方法外部的语句不能跳转到匿名方法的内部。我们也可以使用Lambda表达式替代匿名方法。我们会在下一篇博客中进行介绍。