委托、lambda表达式
委托是寻址方法的.NET版本。在C++中,函数指针只不过是一个指向内存地址的指针,它不是类型安全的。我们无法判断这个指针实际指向什么,像参数和返回类型等项就更无从知晓了。而.NET委托完全不同,委托是类型安全的一个类,它定义了返回值类型和参数的类型。委托类不仅包含对方法的引用,也可以包含对多个方法的引用。
当要把方法传递给其他方法时,就需要使用委托。
我们都习惯于把数据作为参数传递给方法,例如下面的例子:
int num = int.Parse("666");
- 所以给方法传递另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作。更麻烦的是,在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要将第二个方法作为参数传递给第一个方法。
首先我们要定义一个委托,定义委托是要注意两点:
- 通过delegate关键字定义委托(访问修饰符+delegate+返回值+委托类型名称+参数列表)
- 委托其实就是指向一个方法,委托的传入参数就是方法的传入参数,委托的返回值就是方法的返回值
public delegate void DTest(string name, int age);
- 以上代码定义了一个名为DTest的委托,并且要求传入方法的返回值为void,参数列表为 (string name, int age) ,下面我们定义以下4个方法,并调用委托:
public static void Method1(string name)
{
Console.WriteLine(name);
}
public static bool Method2(string name, int age)
{
Console.WriteLine(name + ":" + age);
return true;
}
public static void Method3(string name, int age)
{
Console.WriteLine(name + ":" + age);
}
public static void Method4(string name, int age)
{
Console.WriteLine("我是 {0} ,我 {1} 岁!", name, age);
}
static void Main(string[] args)
{
//声明委托对象
DTest DG1, DG2;
//进行对象初始化,并调用委托
//DG1 = new DTest(Method1);//错误:参数列表不一致
//DG1("张三");
//DG1 = new DTest(Method2);//错误:返回值类型不一致
//DG1("张三", 20);
DG1 = new DTest(Method3);//正确
DG1("张三", 20);
//便捷的初始化方式,不使用new关键字
DG2 = Method3;
DG2("张三", 20);
DG2 = Method4;//为委托对象重新赋值,将执行新指定的方法
DG2("张三", 20);
Console.ReadKey();
}
前面使用的每个委托都只包含一个方法调用。调用委托的次数和调用方法的执行次数相同。如果要多次调用一个方法,就要多次显式调用委托
但是一个委托也可以包含多个方法,这种委托称为多播委托。如果调用多播委托就可以按顺序连续调用多方法。此时要求调用方法的返回值为void,否则就只能得到委托调用的最后一个方法的结果。
static void Main(string[] args)
{
DTest DG1, DG2, DG3;
DG1 = Method3;
DG2 = Method4;
DG3 = DG1 + DG2;
DG3("张三", 20);//将同时执行Method3和Method4
DG3 = DG3 - DG1;
DG3("张三", 20);//只执行Method4
Console.ReadKey();
}
static void Main(string[] args)
{
DTest DG;
DG = Method3;
DG += Method4;
DG("张三", 40);//同时执行Method3和Method4
DG -= Method3;
DG("张三", 40);//只执行Method4
Console.ReadKey();
}
多播委托实际上是一个派生自System.MulticastDelegate的类,System.MulticastDelegate又派生自基类System.Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接为一个列表。
但是使用多播委托会存在一个大问题。当通过多播委托调用多个方法的时候,如果其中一个方法抛出一个异常,那么整个迭代就会停止,也就是说调用方法列表后边的方法将不会被执行。
例如,我们定义2个方法Method1和Method2,在Method1中抛出一个异常,并通过委托调用这两个方法
class MyDelegate
{
//定义委托
delegate void DG();
public void Main()
{
DG dg = Method1;
dg += Method2;
dg();
Console.ReadKey();
}
public void Method1()
{
Console.WriteLine("Method1()执行... ...");
throw new Exception("Error!");
}
public void Method2()
{
Console.WriteLine("Method()执行... ...");
}
}
- 结果如图所示:
- 我们可以看到Method2()并没有被执行。为了避免这种情况,我们应自己迭代方法列表。Delegate类定义了GetInvocationList()方法,它返回一个Delegate对象数组。我们可以使用这个委托调用与委托相关的方法,捕获异常,并继续下一次迭代。
public void Main()
{
DG dg = Method1;
dg += Method2;
Delegate[] methodList = dg.GetInvocationList();
foreach (DG d in methodList)
{
try
{
d();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
- 如图,即使方法中存在异常,但是仍然可以继续执行下一个方法。
泛型委托Action<T>和Func<T>
泛型委托
Action<T>
表示引用一个返回值类型为void的方法,该方法可以有0-16个传入参数。例如,Action<in T1>
调用带有一个参数的方法;Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8>
调用带有8个参数的方法。泛型委托
Func<T>
表示表示引用一个具有返回值的方法,该方法可以有0-16个传入参数。Func<in T2, out TResult>
调用带有一个参数,返回值类型为TResult的方法;Func<in T1, in T2, in T3, in T4, out TResult>
调用带有4个参数,返回值为TResult的方法。我们定义以下4个方法:
public void Method_Action()
{
Console.WriteLine("Action<T>调用0个参数,无返回值的方法!");
}
public void Method_Action(int param1, int param2, int param3, int param4)
{
Console.WriteLine("Action<T>调用4个参数,无返回值的方法!");
}
public bool Method_Func()
{
Console.WriteLine("Func<T>调用0个参数,返回值为bool类型的方法!");
return true;
}
public int Method_Func(int param1, int param2, int param3, int param4)
{
Console.WriteLine("Func<T>调用4个参数,返回值为int类型的方法!");
return param1 + param2 + param3 + param4;
}
- 我们通过
Action<T>
和Func<T>
调用它们,代码如下:
public void Main()
{
Action action1 = new Action(Method_Action);
action1();
Action<int, int, int, int> action2 = new Action<int, int, int, int>(Method_Action);
action2(1, 2, 3, 4);
Func<bool> func1 = new Func<bool>(Method_Func);
func1();
Func<int, int, int, int, int> func2 = new Func<int, int, int, int, int>(Method_Func);
func2(1, 2, 3, 4);
Console.ReadKey();
}
使用匿名方法
以上讲的内容,在使用委托时,必须要下定义方法。但是我们也可以使用匿名方法,使用匿名函数就不需要预先定义委托要执行的方法,而是在对委托对象进行初始化时为其指定要执行的代码。
使用匿名方法与前边讲到的内容不同之处在于委托的初始化,例如以下代码:
class MyDelegate
{
delegate void DG1();
delegate bool DG2(string name);
public void Main()
{
DG1 dg1 = delegate { Console.WriteLine("匿名委托~~~~"); };
dg1();
DG2 dg2 = delegate(string name)
{
Console.WriteLine(name);
return true;
};
dg2("张三");
}
}
我们可以看到以上代码在对委托对象进行初始化实时为其指定了要执行的代码,而不是为其指定了一个方法。从C#3.0开始,我们可以使用lambda表达式替换使用匿名方法。
使用匿名方法时需要注意一下几点:
- 在匿名方法中不能使用跳转语句(break、goto或continue)跳转到匿名方法外部;
- 匿名方法外部的跳转语句不能跳转到匿名方法内部;
- 在匿名方法内部不能访问不安全的代码;
- 在匿名方法内部不能访问外部使用的ref和out参数,但可以使用在匿名方法外部定义的其他变量;
- 如果匿名方法需要多次编写,就尽量不要使用匿名方法,使用声明过的方法会更合适,因为只需要编写一次代码就可以多次使用
lambda表达式
lambda表达式的本质就是匿名方法。lambda表达式可以用于类型为委托的任意地方。类型是Expression或
Expression<T>
时,也可以使用lambda表达式。此时编译器会创建一个表达式树。lambda运算符”=>”读作”goes to”;运算符的左边列出需要的参数,右边则定义了要执行方法的实现代码。
public void Main()
{
//没有参数的方法,"=>"左边用()表示
Action action1 = () => Console.WriteLine("无参的lambda表达式");
action1();
//只有一个参数的方法,"=>"左边可以直接写参数名,不用使用()括起来
Action<string> action2 = name => Console.WriteLine("My name is " + name);
action2("张三");
//当有多个参数时,"=>"左边必须使用()将参数括起来
Action<string, int> action3 = (name, age) => Console.WriteLine("My name is " + name + ", I'm " + age + " years old!");
action3("李四", 20);
//当方法的执行语句有多行时,"=>"右边可以使用{}将执行语句括起来,当只有一条执行语句时可以不使用{}
Action<int, int> action4 = (num1, num2) =>
{
int res = num1 + num2;
Console.WriteLine("{0}+{1}={2}", num1, num2, res);
};
action4(30, 40);
//当执行方法有返回值,并且方法的执行语句只有一行时,可以不写return语句,编译器会添加一条隐式的return语句,否则的话,必须使用return语句和{}
Func<int, int, int> func = (num1, num2) => num1 + num2;
Console.WriteLine(func(20, 30));
}
- lambda表达式的简单应用,我们可以参考下边的代码:
//定义一个int类型的集合
List<int> list = new List<int>() { 20, 2, 4, 40, 29, 59, 24, 37 };
//查找出集合中大于30的所有数字,Where()是一个扩展方法,它要求传入一个Func<int,bool>的委托对象,即有一个int类型的传入参数,返回值类型为bool类型的委托
IEnumerable<int> res = list.Where(num => num > 30);
foreach (int item in res)
{
Console.WriteLine(item);
}
//计算出集合中多有数字的和
int sum = 0;
//ForEach()遍历list集合中的每个元素,要求传入一个Action<int>的委托对象
list.ForEach(num => sum += num);
Console.WriteLine(sum);
- 代码执行结果:
闭包
在上边的例子中,计算集合中所有数字的和时,lambda表达式语句块中使用了外部的变量sum。这种lambda表达式语句块中调用外部变量的行为,称作闭包。
闭包看似用着很方便,但是使用不当也容易引发问题。
//定义一个int类型的集合
var values = new List<int>() { 10, 20, 30 };
//定义一个元素为Func<int>委托对象的集合
var funcs = new List<Func<int>>();
//向集合funcs中添加委托对象,每个委托对象执行的方法就是返回values中的元素
foreach (var value in values)
{
funcs.Add(() => value);
}
//遍历调用funcs集合中的每个委托
foreach (var f in funcs)
{
Console.WriteLine(f());
}
以上代码在C#4.0或更早版本的编译器中的输出结果是30,30,30;而不是我们预想的10,20,30;原因是,在第一个foreach循环中使用闭包时,所创建的函数中变量value并没有被赋值,而是在方法在第二个foreach中被调用时才被赋值,而且都将集合values中的最后一个元素30赋值给了变量value(编译器会为foreach语句创建一个while循环,在C#4.0或更早版本的编译器中,编译器在while循环外部定义了循环变量,在每次迭代是重用这个变量,因此,在循环结束时,该变量的值就是最后一次迭代的值30),这就导致最后的输出结果是30,30,30。
如果想要在使用C#4.0或更早版本的编译器中输出结果10,20,30,就必须对代码进行修改,使用一个局部变量,并将该局部变量传入lambda表达式。
var values = new List<int>() { 10, 20, 30 };
var funcs = new List<Func<int>>();
foreach (var value in values)
{
var v = value;//定义局部变量,将局部变量传入lambda表达式
funcs.Add(() => v);
}
foreach (var f in funcs)
{
Console.WriteLine(f());
}
- 在C#5.0中,不在需要做这种代码修改(即将代码修改为局部变量)。C#5.0会在while循环的代码块中创建一个不同的局部循环变量,所以值会自动保留。这是C#4.0和C#5.0的区别。