文章目录
委托
可以认为委托是持有一个或多个方法的对象。当然,正常情况下你不会想要“执行”一个对象,但委托与典型的兑现不同。可以执行委托,这是委托会执行它所“持有”的方法。
如果你有C++背景,理解委托最快的方法就是把它看成一个类型安全的、面向对象的C++函数指针。
我们将从下面的示例代码开始。如果此时有些东西弄不明白,不必担心,接下来会介绍委托的细节。
delegate void Mydel(int value); //声明委托类型
class Program
{
void PrintLow(int value)
{
Console.WriteLine("{0} - Low Value",value);
}
void PrintHigh(int value)
{
Console.WriteLine("{0} - High Value",value);
}
static void Main(string[] args)
{
Program program = new Program();
Mydel del; //声明委托变量
//创建随机整数生成器对象,并得到0到99之间的一个随机数
Random random = new Random();
int randomValue = random.Next(99);
//创建一个包含PrintLow或PrintHigh的委托对象并将其赋值给del变量
del = randomValue < 50 ? new Mydel(program.PrintLow) : new Mydel(program.PrintHigh);
del(randomValue); //执行委托
}
}
- 代码开始部分声明了一个委托类型
MyDel
(没错,是委托类型不是委托对象。我们很快就会介绍这一点)。 Program
类声明了3个方法:PrintLow
、PrintHigh
和Main
。接下来要创建的委托对象将持有PrintLow
或PrintHigh
方法,但到底使用哪个要到运行时才能确定。- Main声明了一个局部变量
del
,将持有一个MyDel
类型的委托对象的引用。这并不会创建对象,只是创建持有委托对象引用的变量,在几行之后便会创建这个委托对象,并将其赋值给这个变量。 Main
创建了一个Random
类的对象,这是一个随机数生成器类。接着程序调用该对象的Next
方法,将99作为方法的输入参数。这会返回介于0到99之间的随机整数,并将这个值保存在局部变量randomValue
中。- 下面一行检查这个返回并存储的随机值是否小于50(注意,我们使用三元表达符来返回两个委托之一)。
- 如果该值小于50,就创建一个
MyDel
委托对象并初始化,让它持有PrintLow
方法的引用。 - 否则,就创建一个持有
PrintHigh
方法的引用的MyDel
委托对象
- 如果该值小于50,就创建一个
- 最后,
Main
执行委托对象del
,这将执行它持有的方法(PrintLow
或PrintHigh
)。
委托概述
下面我们来看细节。委托和类一样,是一种用户自定义的类型。但类表示的是数据和方法的集合,而委托则持有一个或多个方法,以及一系列预定义操作。
可以通过以下操作步骤来使用委托。我们会在之后逐个详细学习每一步。
-
声明一个委托类型。委托声明看上去和方法声明类似,只是没有实现块。
-
使用该委托类型声明一个委托变量
-
创建委托类型的对象,把它赋值给委托变量。新的委托对象包括指向某个方法的引用,这个方法和第一步定义的签名和返回类型一致。
-
你可以选择为委托对象增加其他方法。这些方法必须与第一步定义的委托类型有相同的签名和返回类型。
-
在代码中你可以像调用方法一样调用委托。在调用委托的时候,其包含的每一个方法都会被执行。
观察之前的步骤,你可能注意到了,这和创建与使用类的步骤差不多。
你可以把delegate看作一个包含有序方法列表的对象,这些方法具有相同的签名和返回类型。
- 方法的列表成为调用列表
- 委托保存的方法可以来自任何类或结构,只要它们在下面两点匹配:。
- 委托的返回类型。
- 委托的签名(包括
ref
和out
修饰符)。
- 调用列表中的方法可以是实例方法也可以是静态方法。
- 在调用委托的时候,会执行其调用列表中的所有方法。
声明委托类型
正如上面所述,委托就是类型,就好像类是类型一样。与类一样,委托类型必须被用来创建变量以及类型的对象之前声明。如下示例代码声明了委托类型。
委托类型的声明看上去和方法的声明很类似,有返回类型和签名。返回类型和签名指定了委托接受的方法的形式。
上面的声明指定了MyDel类型的委托只会接受没有返回值并且由单个int参数的方法。如图左边演示了委托类型,右边演示了委托对象。
委托类型在两个方面与方法声明不同。委托类型声明:
- 以
delegate
关键字开头。 - 没有方法主体。
虽然委托类型声明看上去和方法的声明一样,但它不需要在类内部声明,因为它是类型声明。
创建委托对象
委托是引用类型,因此有引用和对象。在委托类型声明之后,我们可以声明变量并创建类型的对象。如下代码演示了委托类型的变量声明:
MyDel delVar; //MyDel是委托类型 delVar是变量
有两种创建委托对象的方式,第一种是使用带new运算符的对象创建表达式,如下面代码所示。new运算符的操作数的组成如下。
- 委托类型名
- 一组圆括号,其中包含作为调用列表中第一个成员的方法的名字。方法可以是实例方法或静态方法。
delVar = new MyDel(myInstObj.MyM1); //创建委托并保存引用
dVar = new MyDel(SClass.OtherM2); //创建委托并保存引用
我们还可以使用快捷语法,它仅有方法说明符构成,如下面代码所示。这段代码和之前的代码是等价的。这种快捷语法能够工作是因为在方法名称和其相应的委托类型之间存在隐式转换。
delVar = myInstObj.MyM1; //创建委托并保存引用
dVar = SClass.OtherM2; //创建委托并保存引用
例如,下面的代码创建了两个委托对象,一个具有实例方法,而另外一个具有静态方法。
delegate void MyDel(int x); //声明委托类型
MyDel delVar,dVar; //创建两个委托变量
delVar = new MyDel(myInstObj.MyM1); //创建委托并保存引用
dvar = new MyDel(SClass.OtherM2); //创建委托并保存引用
这段代码假设有一个叫做myInstobj
的类对象,它有一个叫做MyM1
的方法,该方法接受一个int作为参数,没有返回值。还假设有一个名为SClass
的类,她有一个OtherM2
静态方法,该方法具有与MyDel
委托相匹配的返回类型与签名。
下图演示了委托的实例化。
除了为委托分配内存,创建委托对象还会把第一个方法放入委托的调用列表。
我们还可以使用初始化语法在用一条语句中创建变量和初始化对象。例如,下面的语句还产生了与上图所示的相同的配置。
MyDel delVar = new MyDel(myInstObj.MyM1);
MyDel dVar = new MyDel(SClass.OtherM2);
如下语句使用快捷语法,也产生了上图所示的结果。
MyDel delVar = myInstObj.MyM1;
MyDel dVar = SClass.OtherM2;
给委托赋值
由于委托是引用类型,我们可以通过给它赋值来改变包含在委托变量中的引用。旧的委托对象会被垃圾回收器回收。
例如,下面的代码设置并修改了delVar
的值。
MyDel delVar;
delVar = myInstObj.MyM1; //创建委托对象并赋值
delVar = SClass.OtherM2; //创建委托对象并赋值
组合委托
迄今为止,我们见过的所有委托在调用列表中都只有一个方法。委托可以使用额外的运算符来“组合”。这个运算符最终会创建一个新的委托,其调用列表连接了作为操作数的两个委托的调用列表副本。
例如,如下代码创建了3个委托。第3个委托由前两个委托组合而成。
MyDel delA = myinstObj.MyM1;
MyDel delB = SClass.OtherM2;
MyDel delC= delA+delB; //组合调用列表
尽管术语组合委托(combining delegate)让我们觉得好像操作数委托被修改了,其实它们并没有被修改。事实上,委托是恒定的。委托对象被创建后不能再被改变。(类似字符串的不可变性)
为委托添加方法
尽管通过上面的内容我们知道了委托其实是不变的,不过C#提供了看上去可以为委托添加方法的语法,及使用+=
运算符。
例如,如下代码为委托的调用列表“增加”了两个方法。方法加在了调用列表的底部。
MyDel delVar = inst.MyM1; //创建并初始化
delVar += SCl.m3; //增加方法
delVar +=X.Act; //增加方法
为委托添加方法的结果。其实由于委托是不可变的,所以为委托的调用列表添加2个方法后的结果其实是变量指向的一个全新的委托。
当然,再使用+=
运算符时,实际发生的是创建了一个新的委托,其调用列表是左边的委托加上右边方法的组合。然后将这个新的委托赋值给delVar。
从委托移除方法
我们可以使用-=
运算符从委托移除方法。如下代码演示了-=
运算符的使用。
delVar -= SCl.m3; //从委托移除方法
与为委托增加方法一样,其实是创建了一个新的委托。新的委托是旧委托的副本——只是没有了已经被移除方法的引用。
如下是移除委托时需要记住的一些事项。
- 如果在调用列表中的方法有多个实例,
-=
运算符从列表最后开始搜索,并且移除第一个与方法匹配的实例。 - 试图删除委托中不存在的方法没有效果。
- 试图调用空委托会抛出异常。我们可以通过把委托与
null
进行比较来判断委托的调用列表是否为空。如果调用列表为空,则委托为null
。
调用委托
可以像调用方法一样简单地调用委托。用于调用委托的参数将会用于调用列表中的每一个方法(除非有输出参数)。
例如,如下代码中的delVar
委托接受了一个整数输入值。使用参数调用委托就会使用相同的参数值(在这里是55)调用它的调用列表中的每一个成员。
MyDel delVar = inst.MyM1;
delVar += SCl.m3;
delVar += X.Act;
...
delVar(55);
如果一个方法在调用列表中出现多次,当委托被调用时,每次在列表中遇到这个方法时它都会被调用一次。
委托的示例
如下代码定义并使用了没有参数和返回值的委托。有关代码的注意事项如下:
- Test类定义了两个打印函数。
- Main方法创建了委托的示例并增加了另外3个方法。
- 程序随后调用了委托,也就是调用了它的方法。然后在调用委托之前,程序将进行检测以确保它不是
null
。
//定义一个没有返回值和参数的委托类型
delegate void PrintFunction();
class Test
{
public void print1()
{
Console.WriteLine("Print1 --- instance");
}
public static void print2()
{
Console.WriteLine("Print2 --- static");
}
}
class Program
{
static void Main(string[] args)
{
Test t = new Test();
PrintFunction pf;
pf = t.print1;
pf += Test.print2;
pf += t.print1;
pf += Test.print2;
if (null!=pf)
{
pf();
}
else
{
Console.WriteLine("Delegate is empty");
}
}
}
这段代码运行结果如下:
调用带有返回值的委托
如果委托带有返回值并且在调用列表中有一个以上的方法,会发生下面的情况。
- 调用列表中最后一个方法的返回值就是委托调用返回的值。
- 调用列表中所有其他方法的返回值都会被忽略。
例如,如下代码声明了带有返回int值的委托。Main创建了委托对象并增加了另外两个方法。然后,它在WriteLine
语句中调用委托并打印了它的返回值。
delegate int MyDel();
class MyClass
{
int IntValue = 5;
public int Add2()
{
IntValue += 2;
return IntValue;
}
public int Add3()
{
IntValue += 3;
return IntValue;
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
MyDel myDel = mc.Add2;
myDel += mc.Add3;
myDel += mc.Add2;
Console.WriteLine($"Value : {myDel()}");
Console.ReadKey();
}
}
这段代码运行结果如图:
调用带有引用参数的委托
如果委托有引用参数,参数值会根据调用列表中的一个或多个方法的返回值而改变。
- 在调用委托列表中的下一个方法时,参数的新值(不是初值)会传给下一个方法。
例如,如下代码调用了具有引用参数的委托。
delegate void MyDel(ref int x);
class MyClass
{
public void Add2(ref int x)
{
x += 2;
}
public void Add3(ref int x)
{
x += 3;
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
MyDel myDel = mc.Add2;
myDel += mc.Add3;
myDel += mc.Add2;
int x = 5;
myDel(ref x);
Console.WriteLine($"Value : {x}");
Console.ReadKey();
}
}
这段代码运行结果如图:
引用参数的值在调用间发生改变。
匿名方法
至此,我们已经见过了使用静态方法或实例方法来初始化委托。对于这种情况,方法本身可以被代码的其他部分显示调用,当然,这个部分也必须是某个类或结构的成员。
然而,如果方法只会被使用一次——用来初始化委托会怎么样❓在这种情况下,除了创建委托的语法需要,没有必要创建独立的具名方法 。匿名方法允许我们避免使用独立的具名方法。
- 匿名方法(anonymous method)是在初始化委托时内联 (inline) 声明的方法。
例如,如下演示了同一个类的两个版本。第一个版本声明并使用了一个名称为Add2()
的方法。第二个版本使用了匿名方法来替代。
delegate int MyDel(int InParam);
class Program
{
public static int Add20(int x)
{
return x += 20;
}
static void Main(string[] args)
{
MyDel myDel = Add20;
Console.WriteLine($"{myDel(5)}");
Console.WriteLine($"{myDel(10)}");
Console.ReadKey();
}
}
delegate int MyDel(int InParam);
class Program
{
static void Main(string[] args)
{
MyDel myDel = delegate (int x) { return x += 20; };
Console.WriteLine($"{myDel(5)}");
Console.WriteLine($"{myDel(10)}");
Console.ReadKey();
}
}
使用匿名方法
我们可以在如下地方使用匿名函数。
- 声明委托变量时作为初始化表达式。
- 组合委托时在赋值语句的右边。
- 为委托增加事件时在赋值语句的右边。
匿名方法的语法
匿名方法表达式的语法包含如下组成部分。
-
delegate
类型关键字。 -
参数列表,如果语句块没有任何参数则可以省略。
-
语句块,它包含了匿名方法的代码。
delegate ( 参数列表 ) { 代码块 }
-
返回类型
匿名方法不会显示声明返回值 。然而,实现代码本身的行为必通过返回一个在类型上与委托的返回类型相同的值来匹配委托的返回类型。如果委托有
void
类型的返回值,匿名方法就不能返回值。例如,在如下代码中,委托的返回类型是
int
。匿名方法的实现代码因此也必须在代码路径中返回int
。delegate int OtherDel(int InParam); static void Main() { OtherDel del=delegate (int x){ return x+20;}; }
-
参数
除了数组参数 ,匿名方法的参数列表必须在如下3方面与委托匹配:
- 参数数量。
- 参数类型及位置。
- 修饰符。
我们可以通过使用圆括号为空或省略圆括号来简化匿名方法的参数列表,但必须满足以下两个条件:
- 委托的参数列表不包含任何
out
参数。 - 匿名方法不使用任何参数。
例如,如下代码声明了没有任何
out
参数的委托,匿名方法也没有使用任何参数。由于两个条件都满足了,我们就可以省略匿名方法的参数列表。delegate void SomeDel(int x); SomeDel del=delegate { PrintMessage(); CleanUp(); };
-
Params参数
如果委托声明的参数列表包含了
params
参数,那么匿名方法的参数列表将忽略params
关键字。例如,在如下代码中:- 委托类型声明指定最后一个参数是
params
类型的参数。 - 然而,匿名方法参数列表忽略了
parmas
关键字。
delegate void SomeDel (int x,params int [] y); SomeDel del=delegate(int x,int []y){ ... };
- 委托类型声明指定最后一个参数是
变量和参数的作用域
参数以及声明在匿名方法内部的局部变量的作用域限制在实现方法的主体之内。
例如,下面的匿名方法定义了参数y
和局部变量z
。在匿名方法主体结束后,y
和z
就不在作用域内了。最后一行的代码会产生编译错误。
delegate void MyDel(int x);
MyDel del=delegate (int y)
{
int z=10;
Console.WriteLine($"{z},{y}");
};
Console.WriteLine($"{y},{z}"); //编译错误
-
外部变量
与委托的具名方法不同,匿名方法可以访问它们外围作用域的局部变量和环境。
- 外围作用域的变量叫做外部变量 。
- 用在匿名方法实现代码中的外部变量称为被方法捕获 。
-
捕获变量的声明周期的扩展
只要捕获方法还是委托的一部分,即使变量已经离开了作用域,捕获的外部变量也会一直有效。
例如,图中的代码演示了被捕获变量的生命周期的扩展。
- 局部变量
x
在块中声明和初始化。 - 然后,委托
mDel
用匿名方法初始化,该匿名方法捕获了外部变量x
。 - 如果取消块关闭之后的
WriteLine
语句的注释,就会产生编译错误。因为它引用的x
现在已经离开了作用域。 - 然而,
mdel
委托中的匿名方法在它的环境中保留了x
,并在调用mDel
时输出了它的值。
Lambda表达式
我们刚刚已经看到了,C#2.0引入了匿名方法。然而它的语法有一点麻烦,而且需要一些编译器已经知道的信息。C#3.0 引入了Lambda表达式,简化了匿名方法的语法,从而避免包含这些多余的信息。我们可能会希望使用Lambda表达式来代替匿名方法。其实,如果先引入Lambda表达式,就不会有匿名方法了。
在匿名方法的语法中,delegate
关键字有点多余,因为编译器已经知道我们在将方法赋值给委托。我们可以很容易地通过如下步骤把匿名方法转换为Lambda表达式。
- 删除
delegate
关键字。 - 在参数列表和匿名方法主体之间放Lambda运算符
=>
。Lambda运算符读作“goes to”。
如下的代码演示了这种转换。第一行演示了将匿名方法赋值为变量del
。第二行演示了同样的匿名方法在被转换成Lambda表达式之后,赋值给了变量del
。
MyDel del = delegate (int x) {return x+1;}; //匿名方法
MyDel del = (int x) => {return x+1;}; //Lambda表达式
术语Lambda表达式来源于数学家 Alonzo Church等人在1920年到1930年期间发明的Lambda积分。Lambda积分是用于表示函数的一套系统,它使用希腊字母Lambda(λ)来表示无名函数。 函数式编程语言(如Lisp)使用这个术语来表示可以直接用于描述函数定义的表达式,表达式不再需要有名字了。
这种简单的转换少了一些多余的东西,看上去也更简洁了,但是只省了6个字符。然而,编译器可以通过推断,允许我们进一步简化Lambda表达式,如下面的代码所示。
MyDel del = delegate (int x) {return x+1;}; //匿名方法
MyDel el1 = (int x) => {return x+1;}; //Lambda表达式
MyDel el2 = (x) => {return x+1;}; //Lambda表达式
MyDel el3 = x => {return x+1;}; //Lambda表达式
MyDel el4 = x => x+1 ; //Lambda表达式
- 编译器可以从委托的生命中知道委托参数的类型,因此Lambda表达式允许我们省略类型参数,如
el2
的赋值代码所示。- 带有类型的参数列表称为显示类型。
- 省略类型的参数列表称为隐式类型。
- 如果只有一个隐式类型参数,我们可以省略周围的圆括号,如
el3
的赋值代码所示。 - 最后,Lambda表示式允许表达式的主体是语句块或表达式。如果语句块包含了一个返回语句,我们可以将语句块替换为
return
关键字后的表达式,如el4
的赋值代码所示。
最后一种形式的Lambda表达式的字符只有原始匿名方法的1/4,更简洁,更容易理解。
有关Lambda表达式的参数列表的要点如下。
- Lambda表达式参数列表中的参数必须在参数数量、类型和位置上与委托相互匹配。
- 表达式的参数列表中的参数不一定需要包含类型(隐式类型)、除非委托有
ref
或out
参数——此时必须注明类型(显示类型)。 - 如果只有一个参数,并且是隐式类型的,周围的圆括号可以被省略,否则必须有括号。
- 如果没有参数,必须使用一组空的圆括号。