委托
什么是委托
可以认为委托是持有一个或多个方法的对象。当然,正常情况下你不想“执行”一个对象,但委托与典型对象不同。可以执行委托,这时委托会执行它所“持有”的方法。
我们从下面的示例代码开始。具体细节将在本章剩余内容介绍。
- 代码开始部分声明了一个委托类型MyDel(没错,是委托类型不是委托对象)
- Program类声明了3个方法:PrintLow、PrintHigh和Main。接下来要创建的委托对象将持有PrintLow或PrintHigh方法,但具体使用哪个运行时确定
- Main声明了局部变量del,持有一个MyDel类型的委托对象的引用。这不会创建对象。只是创建持有委托对象引用的变量,在几行后便会创建委托对象,并将值赋给这个变量
- Main创建了Random类的对象,这是个随机数生成器类。接着调用该对象Next方法,将99作为参数。这会返回介于0到99间的随机整数,并将这个值保存在局部变量randomValue中
- 下面一行检查这个随机值是否小于50
- 小于50,就创建一个MyDel委托对象并初始化,让它持有PrintLow方法的引用
- 否则,就创建一个持有PrintHigh方法引用的MyDel委托对象
- 最后,Main执行委托对象del,这将执行它持有的方法(PrintLow或PrintHigh)
如果你有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() { Program program=new Program(); MyDel del; //声明委托变量 var rand=new Random(); var randomValue=rand.Next(99); del=randomValue<50 ?new MyDel(program.PrintLow) :new MyDel(program.PrintHigh); del(randomValue); //执行委托 } }
委托概述
委托和类一样,是用户自定义类型。但类表示的是数据和方法的集合,而委托持有一个或多个方法,以及一系列预定义操作。
可以通过以下操作步骤来使用委托。
- 声明一个委托类型。委托声明看上去和方法声明相似,只是没有实现块
- 使用该委托类型声明一个委托变量
- 创建委托类型的对象,把它赋值给委托变量。新的委托对象包括指向某个方法的引用,这个方法和第一步定义的签名和返回类型一致
- 你可以选择为委托对象增加其他方法。这些方法必须与第一步中定义的委托类型有相同的签名和返回类型
- 在代码中你可以像调用方法一样调用委托。在调用委托时,其包含的每个方法都会被执行
你可以把delegate看做一个包含有序方法列表的对象,这些方法的签名和返回类型相同。
- 方法的列表称为调用列表
- 委托保存的方法可以来自任何类或结构,只要它们在下面两点匹配
- 委托的返回类型
- 委托的签名(包括ref和out修饰符)
- 调用列表中的方法可以是实例方法也可以是静态方法
- 在调用委托时,会执行其调用列表中的所有方法
声明委托类型
与类一样,委托类型必须在被用来创建变量以及类型的对象前声明。声明格式如下。
关键字 委托类型名 ↓ ↓ delegate void MyDel(int x); ↑ ↑ 返回类型 签名
虽然委托类型声明看上去和方法声明一样,但它不需要在类内部声明,因为它是类型声明。
创建委托对象
委托是引用类型,因此有引用和对象。委托类型声明后,我们可以声明变量并创建类型的对象。
有两种创建委托对象的方法,一种是使用带new运算符的对象创建表达式,如下面代码所示。
delVar=new MyDel(myInstObj.MyM1); dVar=new MyDel(SClass.OtherM2);
我们还可以使用快捷语法,它仅由方法说明符构成。这种快捷语法能够工作是因为在方法名称和其相应的委托类型间存在隐式转换。
delVar=myInstObj.MyM1;
dVar=SClass.OtherM2;
创建委托对象会为委托分配内存,还会把第一个方法放入委托调用列表。
给委托赋值
由于委托是引用类型,我们可以通过给它赋值来改变包含在委托变量中的引用。旧的委托对象会被GC回收。
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;
当然,使用+=运算符时,实际发生的是创建了一个新的委托,其调用列表是左边的委托加上右边的组合。然后将这个新的委托赋值给delVar。
从委托移除方法
我们可以使用-=运算符从委托移除方法。
delVar-=SCL.m3;
与为委托增加方法一样,其实是创建了一个新的委托。新的委托是旧委托的副本–只是没有了已经被移除方法的引用。
移除委托时需要记住以下事项:
- 如果在调用列表中有多个实例,-=运算符将从列表最后开始搜索,并且移除第一个与方法匹配的实例
- 试图删除委托中不存在的方法没有效果
- 试图调用空委托会抛出异常。我们可以通过把委托和null进行比较来判断委托列表是否为空。如果调用列表为空,则委托是null
调用委托
可以像调用方法一样简单地调用委托。调用委托的参数将会用于调用列表中的每个方法(除非有输出参数,我们稍后介绍)。
例:delVar委托接受一个整数值。使用参数调用委托会使用相同的参数值调用它调用列表中的每个成员
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() { var 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"); } } }
调用带返回值的委托
如果委托有返回值并且调用列表中有一个以上方法,会发生下面的情况:
- 调用列表中最后一个方法返回的值就是委托调用的返回值
- 调用列表中其他返回值被忽略
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() { var mc=new MyClass(); MyDel mDel=mc.Add2; mDel+=mc.Add3; mDel+=mc.Add2; Console.WriteLine("Value: {0}",mDel()); } }
调用带引用参数的委托
如果委托有引用参数,参数值会根据调用列表中的一个或多个方法的返回值而改变。
在调用委托列表中的下一个方法时,参数的新值会传给下一个方法。
delegate void MyDel(ref int X); class MyClass { public int Add2(ref int x) { x+=2; } public int Add3(ref int x) { x+=3; } static void Main() { var mc=new MyClass(); MyDel mDel=mc.Add2; mDel+=mc.Add3; mDel+=mc.Add2; int x=5; mDel(ref x); Console.WriteLine("Value: {0}",x); } }
匿名方法
匿名方法(anonymous method)是在初始化委托时内联(inline)声明的方法。
例:第一个声明了Add20方法,第二个使用匿名方法。
class Program { public static int Add20(int x) { return x+=20; } delegate int OtherDel(int InParam); static void Main() { OtherDel del=Add20; Console.WriteLine("{0}",del(5)); Console.WriteLine("{0}",del(6)); } } class Program { delegate int OtherDel(int InParam); static void Main() { OtherDel del=delegate(int x) { return x+20; }; Console.WriteLine("{0}",del(5)); Console.WriteLine("{0}",del(6)); } }
使用匿名方法
我们可以在如下地方使用匿名方法。
- 声明委托变量时作为初始化表达式
- 组合委托时在赋值语句的右边
- 为委托增加事件(第14章)时在赋值语句的右边
匿名方法的语法
匿名方法表达式语法包含如下:
关键字 参数列表 语句块 ↓ ↓ ↓ delegate(Parameters){ImplementationCode}
Lambda 表达式
在匿名方法的语法中,delegate关键字有点多余,因为编译器已经知道我们在将方法赋值给委托。我们可以很容易地通过如下步骤把匿名方法转换为Lambda表达式:
- 删除delegate关键字
- 在参数列表和匿名方法主体之间放Lambda运算符=>(读作goes to)。
MyDel del=delegate(int x) {return x+1;};//匿名方法 MyDel le1= (int x) => {return x+1;};//Lambda表达式
术语Lambda表达式来源于数学家Alonzo Church等人在1920到1930年期间发明的Lambda积分。Lambda积分是用于表示函数的一套系统,它使用希腊字母Lambda(λ)来表示无名函数。近来,函数式编程语言(如Lisp及其方言)使用这个术语来表示可以直接用于描述函数定义的表达式,表达式不再需要有名字了。
除了这种简单的转换,通过编译器的自动推断,我们可以更进一步简化Lambda表达式。
- 编译器还可以从委托的声明中知道委托参数的类型,因此Lambda表达式允许我们省略类型参数,如le2
- 带有类型的参数列表称为显示类型
- 省略类型的参数列表称为隐式类型
- 如果只有一个隐式类型参数,我们可以省略周围的圆括号,如le3
- 最后,Lambda表达式允许表达式的主体是语句块或表达式。如果语句块包含了一个返回语句,我们可以将语句块替换为return关键字后的表达式,如le4
MyDel del=delegate(int x) {return x+1;}; MyDel le1= (int x) => {return x+1;}; MyDel le2= (x) => {return x+1;}; MyDel le3= x => {return x+1;}; MyDel le4= x => x+1 ;
例:Lambda表达式完整示例
delegate double MyDel(int par); class Program { static void Main() { MyDel del=delegate(int x) {return x+1;}; MyDel le1= (int x) => {return x+1;}; MyDel le2= (x) => {return x+1;}; MyDel le3= x => {return x+1;}; MyDel le4= x => x+1 ; Console.WriteLine("{0}",del(12)); Console.WriteLine("{0}",le1(12)); Console.WriteLine("{0}",le2(12)); Console.WriteLine("{0}",le3(12)); Console.WriteLine("{0}",le4(12)); } }
有关Lambda表达式的参数列表的要点如下:
- Lambda表达式参数列表中的参数必须在参数数量、类型和位置上与委托相匹配
- 表达式的参数列表中的参数不一定需要包含类型(隐式类型),除非委托由ref或out参数–此时必须注明类型(显式类型)
- 如果只有一个参数,并且是隐式类型的,周围的圆括号可以省略
- 如果没有参数,必须使用一组空圆括号
请发表评论