在C和C++ 中,只能提取函数的地址,并作为一个参数传递它。C没有类型安全性。可以把任何函数传递给需要函数指针的方法。但是,这种直接方法不仅会导致一些关于类型安全性的问题,而且没有意识到:在进行面向对象编程时,几乎没有方法是孤立存在的,而是在调用方法前通常需要与类实例相关联。所以.NET FmmewOrk在语法上不允许使用这种直接方法。如果要传递方法,就必须把方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊类型的对象,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是一个或多个方法的地址。
1.声明委托
定义委托的语法如下:
delegate void MyMethodInvoker(int x);
在这个示例中,定义了一个委托MyMethodInvoker,并指定该委托的每个实例都可以包含一个方法的引用,该方法带有一个int参数,并返回void。理解委托的一个要点是它们的类型安全性非常高。在定义委托时,必须给出它所表示的方法的签名和返回类型等全部细节。
假设要定义一个返回值为string不带参数的委托,可以编写如下代码:
delegate string MyMethodInvoker();
根据定义的可见性,和委托的作用域,可以在委托的定义上应用任意常见的访问修饰符:public、private、protected等:
public delegate string MyMethodInvoker();
2.使用委托
下面的代码段说明了如何使用委托。
private delegate string GetAString();
static void Main()
{
int x = 40;
GetAString firststringMethod = new GetAString(x.ToString);
Console.WriteLine("string is (0)", firststringMethod());
}
在这段代码中,实例化了类型为GetAString的一个委托,并对它进行了初始化,使它引用整型变量x的ToString方法。。在C#中,委托在语法上总是接受一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。
实际上,给委托实例提供圆括号与调用委托类的Invoke方法完全相同。因为firststringMethod是委托类型的一个变量,所以C#编译器会用firststringMethod.Invoke()代替firststringMethod()。
为了减少输入量,只要需要委托实例,就可以只传送地址的名称,这称为委托推断,只要编译器可以把委托实例解析为特定的类型,这个C#特性就是有效的。所以上面的委托初始化也可简写为:
GetAString firststringMethod=x.ToString;
3.Action<T>和Func<T>委托
除了为每个参数和返回类型定义一个新委托类型之外,还可以使用Action<T>和Func<T>委托。泛型Action<T>委托表示引用一个vod返回类型的方法。因为这个委托类存在不同的变体,所以可以传递至多16种不同的参数类型。没有泛型参数的Action类可调用没有参数的方法。Action<T>调用带一个参数的方法,Action<T1,T2>调用带两个参数的方法
Func<T>可以以类似的方式使用,Func<T>允许调用带返回类型的方法。。与Action<T>类似,Func<T>也定义了不同的变体,至多也可以传递16个参数类型和一个返回类型。Func<out TResult>委托类型可以调用带返回类型且无参数的方法,Func<T,out TResult>调用带一个参数的方法。
所以之前自定义的委托GetAString也可以用Func<T>代替。
Func<string> firststringMethod=x.ToString;
4.Bubblesorter示例
下面的示例将说明委托的真正用途。我们要编写一个类Bubblesorter,它实现一个静态方法Sort(),这个方法的第一个参数是一个对象数组,把该数组按照升序重新排列。
要实现对任何类型的对象数组排序,就必须要实现泛型方法Sort<T>(),另外该方法还需一个比较函数,其两个参数的类型是T,返回值为bool。因此,可以跟Sort<T>()指定如下签名:
static public void Sort<T>(IList<T> array,Func<T,T,bool> comparison);
下面定义Bubblesorter类:
class Bubblesorter
{
static public void Sort<T>(IList<T> array, Func<T, T, bool> comparison)
{
bool swapped = true;
do
{
swapped = false;
for (int i = 0; i < array.Count; i++)
{
if (comparison(array[i + 1], array[i]))
{
swapped = true;
T tmp = array[i + 1];
array[i + 1] = array[i];
array[i] = tmp;
}
}
} while (swapped);
}
}
为了使用
Bubblesorter类,下面再定义一个Employee类。
class Employee
{
public string Name { get; private set; }
public float Salary { get; private set; }
public Employee(string name, float salary)
{
Name = name;
Salary = salary;
}
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引用,并返回一个布尔值。
下面编写客户端代码,完成程序。
static void Main()
{
Employee[] employee ={
new Employee("小王",4000),
new Employee("小赵",3200),
new Employee("小韩",3500),
new Employee("小宋",5000)
};
Bubblesorter.Sort<Employee>(employee, Employee.CompareSalary);
foreach (Employee e in employee)
{
Console.WriteLine(e);
}
}
运行这段代码,结果如下:
5.多播委托
前面使用的每个委托都只包含一个方法调用。调用委托的次数与调用方法的次数相同。如果要调用多个方法,就需要多次显式调用这个委托。但是,委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void,否则,就只能得到委托调用的最后一个方法的结果。
多播委托可以识别运算符+和+=,另外多播委托还识别-和-=,以从委托中删除方法调用。
为了说明多播委托的用法,下面定义一个MathOperation类,它可以做两个整型数的相加和想减。
class MathOperation
{
public static void Add(int a,int b)
{
Console.WriteLine("{0}+{1}={2}",a,b,a+b);
}
public static void Sub(int a, int b)
{
Console.WriteLine("{0}-{1}={2}", a, b, a - b);
}
}
下面测试多播委托
static void Main()
{
Action<int, int> action = MathOperation.Add;
action += MathOperation.Sub;
action(5, 3);
//从action中删除MathOperation.Sub
action -= MathOperation.Sub;
action(5, 3);
}
运行程序,结果如下:
如果正在使用多播委托,就应知道对同一个委托调用方法链的顺序并未正式定义。因此应避免编写依赖于以特定顺序调用方法的代码。
通过一个委托调用多个方法还可能导致一个大问题。多播委托包含一个逐个调用的委托集合。如果通过委托调用的其中一个方法抛出一个异常,整个迭代就会停止。
为了说明这种情况,下面修改MathOperation类,在Add中抛出异常。
class MathOperation
{
public static void Add(int a,int b)
{
Console.WriteLine("{0}+{1}={2}",a,b,a+b);
throw new Exception();
}
public static void Sub(int a, int b)
{
Console.WriteLine("{0}-{1}={2}", a, b, a - b);
}
}
然后在主函数中也稍作修改
static void Main()
{
Action<int, int> action = MathOperation.Add;
action += MathOperation.Sub;
try
{
action(5, 3);
}
catch
{
Console.WriteLine("抛出异常……");
}
}
运行结果为:
委托只调用了Add方法。因为Add方法抛出了一个异常,所以委托的迭代会停止。
在这种情况下,为了避免这个问题,应自己迭代方法列表。Delegate类定义GetInvocationList方法,它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,并继续下一次迭代。
static void Main()
{
Action<int, int> action = MathOperation.Add;
action += MathOperation.Sub;
foreach (Action<int, int> d in action.GetInvocationList())
{
try
{
d(5, 3);
}
catch
{
Console.WriteLine("抛出异常……");
}
}
}
修改了代码后运行应用程序,会看到在捕获了异常后,将继续迭代下一个方法。
6.匿名方法
到目前为止,要想使委托工作,方法必须己经存在(即委托是用它将调用的方法的相同签名定义的)。但还有另外一种使用委托的方式:即通过匿名方法。匿名方法是用作委托的参数的一段代码。
用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就有区别了。下面是一个非常简单的控制台应用程序,它说明了如何使用匿名方法:
static void Main()
{
Action<int, int> action = delegate(int a,int b)
{
Console.WriteLine("{0}+{1}={2}",a,b,a+b);
};
action(5, 3);
}