C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

9.2 委托

当要把方法传送给其他方法时,就需要使用委托。要了解具体的含义,可以看看下面一行代码:

        int i = int.Parse("99");

我们习惯于把数据作为参数传递给方法,如上面的例子所示。所以,给方法传递另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行调用。更麻烦的是,在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法。这听起来很令人迷惑,下面用几个示例来说明:

● 启动线程和任务——在C#中,可以告诉计算机并行运行某些新的执行序列,同时运行当前的任务。这种序列就称为线程,在一个基类System.Threading.Thread的实例上使用方法Start(),就可以启动一个线程。如果要告诉计算机启动一个新的执行序列,就必须说明要在哪里启动该序列;必须为计算机提供开始启动的方法的细节,即Thread类的构造函数必须带有一个参数,该参数定义了线程调用的方法。

● 通用库类——许多库包含执行各种标准任务的代码。这些库通常可以自我包含,这样在编写库时,就会知道任务该如何执行。但是有时在任务中还包含子任务,只有使用该库的客户端代码才知道如何执行这些子任务。例如,假设要编写一个类,它带有一个对象数组,并把它们按升序排列。但是,排序的部分过程会涉及重复使用数组中的两个对象,比较它们,看看哪一个应放在前面。如果要编写的类必须能对任何对象数组排序,就无法提前告诉计算机应如何比较对象。处理类中对象数组的客户端代码也必须告诉类如何比较要排序的特定对象。换言之,客户端代码必须给类传递某个可以调用并进行这种比较的合适方法的细节。

● 事件——一般的思路是通知代码发生了什么事件。GUI编程主要处理事件。在引发事件时,运行库需要知道应执行哪个方法。这就需要把处理事件的方法作为一个参数传递给委托。这些将在本章后面讨论。

在C和C++中,只能提取函数的地址,并作为一个参数传递它。C没有类型安全性,可以把任何函数传递给需要函数指针的方法。但是,这种直接方法不仅会导致一些关于类型安全性的问题,而且没有意识到:在进行面向对象编程时,几乎没有方法是孤立存在的,而是在调用方法前通常需要与类实例相关联。所以.NET Framework在语法上不允许使用这种直接方法。如果要传递方法,就必须把方法的细节封装在一种新的对象类型中,即委托。委托只是一种特殊类型的对象,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是一个或多个方法的地址。

9.2.1 声明委托

在C#中使用一个类时,分两个阶段操作。首先,需要定义这个类,即告诉编译器这个类由什么字段和方法组成。然后(除非只使用静态方法),实例化该类的一个对象。使用委托时,也需要经过这两个步骤。首先必须定义要使用的委托,对于委托,定义它就是告诉编译器这种类型的委托表示哪种类型的方法。然后,必须创建该委托的一个或多个实例。编译器在后台将创建表示该委托的一个类。声明委托的语法如下:

        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等:

        public delegate string GetAString();

注意:实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生自基类System. MulticastDelegate的类,System.MulticastDelegate又派生自基类System.Delegate。C#编译器能识别这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况。这是C#与基类共同合作以使编程更易完成的另一个范例。

定义好委托后,就可以创建它的一个实例,从而用该实例存储特定方法的细节。

注意:但是,此处在术语方面有一个问题。类有两个不同的术语:“类”表示较广义的定义,“对象”表示类的实例。但委托只有一个术语。在创建委托的实例时,所创建的委托的实例仍称为委托。必须从上下文中确定所使用委托的确切含义。

9.2.2 使用委托

下面的代码段说明了如何使用委托。这是在int值上调用ToString()方法的一种相当冗长的方式(代码文件GetAStringDemo/Program.cs):

        private delegate string GetAString();
        public static void Main()
        {
          int x = 40;
          GetAString firstStringMethod = new GetAString(x.ToString);
          WriteLine($"String is {firstStringMethod()}");
          // With firstStringMethod initialized to x.ToString(),
          // the above statement is equivalent to saying
          // Console.WriteLine($"String is {x.ToString()}");
        }

在这段代码中,实例化类型为GetAString的委托,并对它进行初始化,使其引用整型变量x的ToString()方法。在C#中,委托在语法上总是接受一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中,如果用不带参数并返回一个字符串的方法来初始化firstStringMethod变量,就会产生一个编译错误。注意,因为int.ToString()是一个实例方法(不是静态方法),所以需要指定实例(x)和方法名来正确地初始化委托。

下一行代码使用这个委托来显示字符串。在任何代码中,都应提供委托实例的名称,后面的圆括号中应包含调用该委托中的方法时使用的任何等效参数。所以在上面的代码中,Console.WriteLine()语句完全等价于注释掉的代码行。

实际上,给委托实例提供圆括号与调用委托类的Invoke()方法完全相同。因为firstStringMethod是委托类型的一个变量,所以C#编译器会用firstStringMethod.Invoke()代替firstStringMethod()。

        firstStringMethod();
        firstStringMethod.Invoke();

为了减少输入量,在需要委托实例的每个位置可以只传送地址的名称。这称为委托推断。只要编译器可以把委托实例解析为特定的类型,这个C#特性就是有效的。下面的示例用GetAString委托的一个新实例初始化GetAString类型的firstStringMethod变量:

        GetAString firstStringMethod = new GetAString(x.ToString);

只要用变量x把方法名传送给变量firstStringMethod,就可以编写出作用相同的代码:

        GetAString firstStringMethod = x.ToString;

C#编译器创建的代码是一样的。由于编译器会用firstStringMethod检测需要的委托类型,因此它创建GetAString委托类型的一个实例,用对象x把方法的地址传送给构造函数。

注意:调用上述方法名时,输入形式不能为x.ToString()(不要输入圆括号),也不能把它传送给委托变量。输入圆括号会调用一个方法,而调用x.ToString()方法会返回一个不能赋予委托变量的字符串对象。只能把方法的地址赋予委托变量。

委托推断可以在需要委托实例的任何地方使用。委托推断也可以用于事件,因为事件基于委托(参见本章后面的内容)。

委托的一个特征是它们的类型是安全的,可以确保被调用的方法的签名是正确的。但有趣的是,它们不关心在什么类型的对象上调用该方法,甚至不考虑该方法是静态方法还是实例方法。

注意:给定委托的实例可以引用任何类型的任何对象上的实例方法或静态方法——只要方法的签名匹配委托的签名即可。

为了说明这一点,扩展上面的代码段,让它使用firstStringMethod委托在另一个对象上调用其他两个方法,其中一个是实例方法,另一个是静态方法。为此,使用本章前面定义的Currency结构。Currency结构有自己的ToString()重载方法和一个与GetCurrencyUnit()签名相同的静态方法。这样,就可以用同一个委托变量调用这些方法了(代码文件GetAStringDemo/Currency.cs):

        struct Currency
        {
          public uint Dollars;
          public ushort Cents;
          public Currency(uint dollars, ushort cents)
          {
            this.Dollars = dollars;
            this.Cents = cents;
          }
          public override string ToString() => $"${Dollars}.{Cents,2:00}";
          public static string GetCurrencyUnit() => "Dollar";
          public static explicit operator Currency (float value)
          {
            checked
            {
              uint dollars = (uint)value;
              ushort cents = (ushort)((value-dollars) * 100);
              return new Currency(dollars, cents);
            }
          }
          public static implicit operator float (Currency value) =>
            value.Dollars + (value.Cents / 100.0f);
          public static implicit operator Currency (uint value) =>
            new Currency(value, 0);
          public static implicit operator uint (Currency value) =>
            value.Dollars;
        }

下面就可以使用GetAString实例,代码如下所示(代码文件GetAStringDemo/Program.cs):

        private delegate string GetAString();
        public static void Main()
        {
          int x = 40;
          GetAString firstStringMethod = x.ToString;
          WriteLine($"String is {firstStringMethod()}");
          var balance = new Currency(34, 50);
          // firstStringMethod references an instance method
          firstStringMethod = balance.ToString;
          WriteLine($"String is {firstStringMethod()}");
          // firstStringMethod references a static method
          firstStringMethod = new GetAString(Currency.GetCurrencyUnit);
          WriteLine($"String is {firstStringMethod()}");
        }

这段代码说明了如何通过委托来调用方法,然后重新给委托指定在类的不同实例上引用的不同方法,甚至可以指定静态方法,或者指定在类的不同类型实例上引用的方法,只要每个方法的签名匹配委托定义即可。

运行此应用程序,会得到委托引用的不同方法的输出结果:

        String is 40
        String is $34.50
        String is Dollar

但是,我们实际上还没有说明把一个委托传递给另一个方法的具体过程,也没有得到任何特别有用的结果。调用int和Currency对象的ToString()方法要比使用委托直观得多!但是,需要用一个相当复杂的示例来说明委托的本质,才能真正领会到委托的用处。下一节会给出两个委托的示例。第一个示例仅使用委托来调用两个不同的操作。它说明了如何把委托传递给方法,如何使用委托数组,但这仍没有很好地说明:没有委托,就不能完成很多工作。第二个示例就复杂得多了,它有一个类BubbleSorter,该类实现一个方法来按照升序排列一个对象数组。没有委托,就很难编写出这个类。

9.2.3 简单的委托示例

在这个示例中,定义一个类MathOperations,它有两个静态方法,对double类型的值执行两种操作。然后使用该委托调用这些方法。MathOperations类如下所示:

        class MathOperations
        {
          public static double MultiplyByTwo(double value) => value * 2;
          public static double Square(double value) => value * value;
        }

下面调用这些方法(代码文件SimpleDelegate/Program.cs):

        using static System.Console;
        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++)
              {
                WriteLine($"Using operations[{i}]:);
                ProcessAndDisplayNumber(operations[i], 2.0);
                ProcessAndDisplayNumber(operations[i], 7.94);
                ProcessAndDisplayNumber(operations[i], 1.414);
                WriteLine();
              }
            }
            static void ProcessAndDisplayNumber(DoubleOp action, double value)
            {
              double result = action(value);
              WriteLine($"Value is {value}, result of operation is {result}");
            }
          }
        }

在这段代码中,实例化了一个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);

这实际上是调用action委托实例封装的方法,其返回结果存储在result中。运行这个示例,得到如下所示的结果:

        SimpleDelegate
        Using operations[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 operations[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

9.2.4 Action<T>和Func<T>委托

除了为每个参数和返回类型定义一个新委托类型之外,还可以使用Action<T>和Func<T>委托。泛型Action<T>委托表示引用一个void返回类型的方法。这个委托类存在不同的变体,可以传递至多16种不同的参数类型。没有泛型参数的Action类可调用没有参数的方法。Action<in T>调用带一个参数的方法,Action<in T1, in T2>调用带两个参数的方法,Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8>调用带8个参数的方法。

Func<T>委托可以以类似的方式使用。Func<T>允许调用带返回类型的方法。与Action<T>类似,Func<T>也定义了不同的变体,至多也可以传递16个参数类型和一个返回类型。Func<out TResult>委托类型可以调用带返回类型且无参数的方法,Func<in T, out TResult>调用带一个参数的方法,Func<in T1, in T2, in T3, in T4, out TResult>调用带4个参数的方法。

9.2.3节中的示例声明了一个委托,其参数是double类型,返回类型是double:

        delegate double DoubleOp(double x);

除了声明自定义委托DoubleOp之外,还可以使用Func<in T, out TResult>委托。可以声明一个该委托类型的变量,或者声明该委托类型的数组,如下所示:

        <double, double>[] operations =
        {
          MathOperations.MultiplyByTwo,
          MathOperations.Square
        };

使用该委托,并将ProcessAndDisplayNumber()方法作为参数:

        static void ProcessAndDisplayNumber(Func<double, double> action,
                                      double value)
        {
          double result = action(value);
          WriteLine($"Value is {value}, result of operation is {result}");
        }

9.2.5 BubbleSorter示例

下面的示例将说明委托的真正用途。我们要编写一个类BubbleSorter,它实现一个静态方法Sort(),这个方法的第一个参数是一个对象数组,把该数组按照升序重新排列。例如,假定传递给该委托的是int数组:{0, 5, 6, 2, 1},则返回的结果应是{0, 1, 2, 5, 6}。

冒泡排序算法非常著名,是一种简单的数字排序方法。它适合于一小组数字,因为对于大量的数字(超过10个),还有更高效的算法。冒泡排序算法重复遍历数组,比较每一对数字,按照需要交换它们的位置,从而把最大的数字逐步移动到数组的末尾。对于给int型数字排序,进行冒泡排序的方法如下所示:

        bool swapped = true;
        do
        {
          swapped = false;
          for (int i = 0; i < sortArray.Length-1; i++)
          {
          if (sortArray[i] > sortArray[i+1])) // problem with this test
          {
              int temp = sortArray[i];
              sortArray[i] = sortArray[i + 1];
              sortArray[i + 1] = temp;
              swapped = true;
            }
          }
        } while (swapped);

它非常适合于int型,但我们希望Sort()方法能给任何对象排序。换言之,如果某段客户端代码包含Currency结构或自定义的其他类和结构的数组,就需要对该数组排序。这样,上面代码中的if(sortArray[i] < sortArray[i+1])就有问题了,因为它需要比较数组中的两个对象,看看哪一个更大。可以对int型进行这样的比较,但如何对没有实现“<”运算符的新类进行比较?答案是能识别该类的客户端代码必须在委托中传递一个封装的方法,这个方法可以进行比较。另外,不对temp变量使用int类型,而使用泛型类型就可以实现泛型方法Sort()。

对于接受类型T的泛型方法Sort<T>(),需要一个比较方法,其两个参数的类型是T, if比较的返回类型是布尔类型。这个方法可以从Func<T1, T2, TResult>委托中引用,其中T1和T2的类型相同:Func<T, T, bool>。

给Sort<T>方法指定下述签名:

        static public void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison)

这个方法的文档声明,comparison必须引用一个方法,该方法带有两个参数,如果第一个参数的值“小于”第二个参数,就返回true。

设置完毕后,下面定义BubbleSorter类(代码文件BubbleSorter/BubbleSorter.cs):

        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);
          }
        }

为了使用这个类,需要定义另一个类,从而建立要排序的数组。在本例中,假定Mortimer Phones移动电话公司有一个员工列表,要根据他们的薪水进行排序。每个员工分别由类Employee的一个实例表示,如下所示(代码文件BubbleSorter/Employee.cs):

        class Employee
        {
          public Employee(string name, decimal salary)
          {
            Name = name;
            Salary = salary;
          }
          public string Name { get; }
          public decimal Salary { get; private set; }
          public override string ToString() => $"{Name}, {Salary:C}";
          public static bool CompareSalary(Employee e1, Employee e2) =>
            e1.Salary < e2.Salary;
        }

注意,为了匹配Func<T, T, bool>委托的签名,在这个类中必须定义CompareSalary,它的参数是两个Employee引用,并返回一个布尔值。在实现比较的代码中,根据薪水进行比较。

下面编写一些客户端代码,完成排序(代码文件BubbleSorter/Program.cs):

        using static System.Console;
        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)
              {
                WriteLine(employee);
              }
            }
          }
        }

运行这段代码,正确显示按照薪水排列的Employee,如下所示:

        BubbleSorter
        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

9.2.6 多播委托

前面使用的每个委托都只包含一个方法调用。调用委托的次数与调用方法的次数相同。如果要调用多个方法,就需要多次显式调用这个委托。但是,委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void;否则,就只能得到委托调用的最后一个方法的结果。

可以使用返回类型为void的Action<double>委托(代码文件MulticastDelegates/Program.cs):

        class Program
        {
            static void Main()
            {
              Action<double> operations = MathOperations.MultiplyByTwo;
              operations += MathOperations.Square;

在前面的示例中,因为要存储对两个方法的引用,所以实例化了一个委托数组。而这里只是在同一个多播委托中添加两个操作。多播委托可以识别运算符“+”和“+=”。另外,还可以扩展上述代码中的最后两行,如下面的代码段所示:

        Action<double> operation1 = MathOperations.MultiplyByTwo;
        Action<double> operation2 = MathOperations.Square;
        Action<double> operations = operation1 + operation2;

多播委托还识别运算符“-”和“- =”,以从委托中删除方法调用。

注意:根据后台执行的操作,多播委托实际上是一个派生自System.MulticastDelegate的类,System.MulticastDelegate又派生自基类System.Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接为一个列表。

为了说明多播委托的用法,下面把SimpleDelegate示例转换为一个新示例MulticastDelegate。现在需要委托引用返回void的方法,就应重写MathOperations类中的方法,让它们显示其结果,而不是返回它们(代码文件MulticastDelegates/MathOperations.cs):

        class MathOperations
        {
          public static void MultiplyByTwo(double value)
          {
            double result = value * 2;
            WriteLine($"Multiplying by 2: {value} gives {result}");
          }
          public static void Square(double value)
          {
            double result = value * value;
            WriteLine($"Squaring: {value} gives {result}");
          }
        }

为了适应这个改变,也必须重写ProcessAndDisplayNumber()方法(代码文件MulticastDelegates/Program.cs):

        static void ProcessAndDisplayNumber(Action<double> action, double value)
        {
          WriteLine();
          WriteLine($"ProcessAndDisplayNumber called with value = {value}");
          action(value);
        }

下面测试多播委托,其代码如下:

        static void Main()
        {
          Action<double> operations = MathOperations.MultiplyByTwo;
          operations += MathOperations.Square;
          ProcessAndDisplayNumber(operations, 2.0);
          ProcessAndDisplayNumber(operations, 7.94);
          ProcessAndDisplayNumber(operations, 1.414);
          WriteLine();
        }

现在,每次调用ProcessAndDisplayNumber()方法时,都会显示一条消息,说明它已经被调用。然后,下面的语句会按顺序调用action委托实例中的每个方法:

        action(value);

运行这段代码,得到如下所示的结果:

        MulticastDelegate
        ProcessAndDisplayNumber called with value = 2
        Multiplying by 2: 2 gives 4
        Squaring: 2 gives 4
        ProcessAndDisplayNumber called with value = 7.94
        Multiplying by 2: 7.94 gives 15.88
        Squaring: 7.94 gives 63.0436
        ProcessAndDisplayNumber called with value = 1.414
        Multiplying by 2: 1.414 gives 2.828
        Squaring: 1.414 gives 1.999396

如果正在使用多播委托,就应知道对同一个委托,调用其方法链的顺序并未正式定义。因此应避免编写依赖于以特定顺序调用方法的代码。

通过一个委托调用多个方法还可能导致一个更严重的问题。多播委托包含一个逐个调用的委托集合。如果通过委托调用的其中一个方法抛出一个异常,整个迭代就会停止。下面是MulticastIteration示例,其中定义了一个简单的委托Action,它没有参数并返回void。这个委托打算调用One()和Two()方法,这两个方法满足委托的参数和返回类型要求。注意One()方法抛出了一个异常(代码文件MulticastDelegateWithIteration/Program.cs):

        using System;
        using static System.Console;
        namespace Wrox.ProCSharp.Delegates
        {
          class Program
          {
            static void One()
            {
              WriteLine("One");
              throw new Exception("Error in one");
            }
            static void Two()
            {
              WriteLine("Two");
            }

在Main()方法中,创建了委托d1,它引用方法One();接着把Two()方法的地址添加到同一个委托中。调用d1委托,就可以调用这两个方法。在try/catch块中捕获异常:

          static void Main()
          {
            Action d1 = One;
            d1 += Two;
            try
            {
              d1();
            }
            catch (Exception)
            {
              WriteLine("Exception caught");
            }
          }
        }
      }

委托只调用了第一个方法。因为第一个方法抛出了一个异常,所以委托的迭代会停止,不再调用Two()方法。没有指定调用方法的顺序时,结果会有所不同。

        One
        Exception Caught

注意:错误和异常的介绍详见第14章。

在这种情况下,为了避免这个问题,应自己迭代方法列表。Delegate类定义GetInvocationList()方法,它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,并继续下一次迭代:

        static void Main()
        {
          Action d1 = One;
          d1 += Two;
          Delegate[] delegates = d1.GetInvocationList();
          foreach (Action d in delegates)
          {
            try
            {
              d();
            }
            catch (Exception)
            {
              WriteLine("Exception caught");
            }
          }
        }

修改了代码后,运行应用程序,会看到在捕获了异常后将继续迭代下一个方法。

        One
        Exception caught
        Two

9.2.7 匿名方法

到目前为止,要想使委托工作,方法必须已经存在(即委托通过其将调用方法的相同签名定义)。但还有另外一种使用委托的方式:通过匿名方法。匿名方法是用作委托的参数的一段代码。

用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就会出现区别。下面是一个非常简单的控制台应用程序,它说明了如何使用匿名方法(代码文件AnonymousMethods/Program.cs):

        using static System.Console;
        using System;
        namespace Wrox.ProCSharp.Delegates
        {
          class Program
          {
            static void Main()
            {
              string mid = ", middle part, ";
              Func<string, string> anonDel = delegate(string param)
              {
                param += mid;
                param += " and this was added to the string.";
                return param;
              };
              WriteLine(anonDel("Start of string"));
            }
          }
        }

Func<string, string>委托接受一个字符串参数,返回一个字符串。anonDel是这种委托类型的变量。不是把方法名赋予这个变量,而是使用一段简单的代码:前面是关键字delegate,后面是一个字符串参数。

可以看出,该代码块使用方法级的字符串变量mid,该变量是在匿名方法的外部定义的,并将其添加到要传递的参数中。接着代码返回该字符串值。在调用委托时,把一个字符串作为参数传递,将返回的字符串输出到控制台上。

匿名方法的使用优点是减少了要编写的代码。不必定义仅由委托使用的方法。在为事件定义委托时,这一点非常明显(本章后面探讨事件)。这有助于降低代码的复杂性,尤其是在定义了好几个事件时,代码会显得比较简单。使用匿名方法时,代码执行速度并没有加快。编译器仍定义了一个方法,该方法只有一个自动指定的名称,我们不需要知道这个名称。

在使用匿名方法时,必须遵循两条规则。在匿名方法中不能使用跳转语句(break、goto或continue)跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。

在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。

如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法。此时与复制代码相比,编写一个命名方法比较好,因为该方法只需要编写一次,以后可通过名称引用它。

注意:匿名方法的语法在C# 2中引入。在新的程序中,并不需要这个语法,因为lambda表达式(参见下一节)提供了相同的功能,还提供了其他功能。但是,在已有的源代码中,许多地方都使用了匿名方法,所以最好了解它。

从C# 3.0开始,可以使用lambda表达式。