C#学习札记——委托

C#学习笔记——委托

委托

委托包含具有相同签名和返回类型的有序方法列表。

    方法的列表称为调用列表。

         当委托被调用时,它调用列表中的每一个方法。

        

包含单个方法的委托和C++的函数指针相似。然而,与函数指针不同的是,委托是面向对象的并且是类型安全(type-safe)的。

 

调用列表中的方法

    由委托保存的方法可以来自任何类或结构,只要它们同时匹配委托的如下两点:

    返回值

    签名(包括refout修饰符)

    调用列表中的方法可以是实例方法或是静态方法。

        

        

声明委托类型

    委托是类型,就好像类是类型一样。与类一样,委托类型必须在被用来创建变量以及类型的对象之前声明。如下示例代码声明了委托类型。

    委托类型声明和所有类型声明一样,不需要在类内部声明。

 

关键词        委托类型名

delegate void MyDel(int x);

 

委托类型的声明看上去与方法的声明很相似,有返回类型和签名。返回类型和签名指定了委托接受的方法的形式。

 

委托类型声明在两个方面与方法声明不同。委托类型声明:

delegate关键词开头。

没有方法主体。

 

 

 

创建委托对象

    委托是引用类型,因此有引用和对象。在委托类型声明之后,我们可以声明变量并创建类型的对象。如下代码演示了委托类型的变量的声明:

委托类型 变量

MyDel delVar;

 

有两种创建委托对象的方式,第一种是使用带new运算符的对象创建表达式,如下面代码所示。

new运算符的操作数的组成如下:

    委托类型名。

    一组圆括号,其中包含作为调用列表中第一个成员的方法的名字。方法可以是实例方法或静态方法。

        

 

                    实例方法

delVar = newMyDel(myInstObj.MyM1);  //创建委托并保存引用

dVar = new MyDel(SClass.OtherM2);   //创建委托并保存引用

                    静态方法

                                              

    我们还可以使用快捷语法,它仅由方法说明符构成,如下面代码所示。这段代码和之前的代码是等价的。使用快捷语法是因为在方法名称和其相应的委托类型之间有隐式转换。

 

delVar = myInstObj.MyM1;//创建委托并保存引用

dVar = SClass.OthertM2;//创建委托并保存引用

 

    例如,下面的代码创建了两个委托对象——一个具有实例方法,而另外一个具有静态方法。这段代码假设有一个叫做nlylnstObj的类对象,它有一个叫做MyM1的方法,该方法接受一个int作为参数,不返回值。还假设有一个名为SClass的类,它有一个OtherM2静态方法,该方法具有与MyDel委托相匹配的返回类型和签名。

delegate void MyDel(int x);  //声明委托类型

MyDel delVar,dVar;    //创建两个委托变量

 

                    实供方法

delVar=new MyDel(myInstObj.MyM1);    //创建委托并保存引用

dVar = new MyDel(SClass.OtherM2);    //创建委托并保存引用

                    静态方法

 

                                              

除了为委托分配内存,创建委托对象还会把第一个方法放入委托的调用列表。我们还可以在同一条语句中创建变量和初始化对象。

例如,下面的语句产生了与图15-3所示的相同的配置。

MyDel delVar =new MyDel(myInstObj.MyM1);

MyDel dVar = new MyDel(SClass.OtherM2);

如下语句使用快捷语法,

MyDel delVar = myInstObj.Ml1;

MyDel dVar = SClass.OtherM2;

 

 

 

赋值委托

    由于委托是引用类型,我们可以通过给它赋值来改变包含在委托变量中的引用。旧的委托对象会被垃圾回收器回收。

    例如,下面的代码设置并修改了delVar的值。

 

MyDel delVar;

delVar=myInstObj.MyM1; //Create and assigrt the delegate object.

...

delVar=SClass.OtherM2;//Create and assigrt the delegate object.

 

组合委托

    迄今为止,我们见过的所有委托在调用列表中都只有一个方法。委托可以使用额外的运算符来“组合”。这个运算最终会创建一个新的委托,其调用列表是两个操作数的委托调用列表的副

本的连接。

    例如,如下代码创建了3个委托。第三个委托由前两个委托组合。

MyDel delA = myInstObj.MyM1;

MyDel delB = SClass.OtherM2;

MyDel delB = delA+delB;    //组合调用列表

    尽管术语组合委托(combining delegate)让我们觉得好像操作数委托被修改了,其实它们并没有被修改,委托是恒定的。委托对象被创建后不会再被改变。

 

 

为委托增加方法

    尽管通过之前的内容我们知道了委托其实是不变的,C#提供了看上去可以为委托增加方法的语法,以这种方式考虑,它非常棒。我们可以通过使用扣运算符来为委托增加方法或另一个委托。

    例如,如下代码为委托的调用列表“增加”一了两个方法。方法加在了调用列表的底部。

例:

MyDel delVar=myInstObj.MyM1;

delVar      += SCl.m3;

deIVar      += X.Act;

 

当然,在使用+=运算符时,实际发生的是创建了一个新的委托,其调用列表是左边的委托加上右边方法的组合。

 

 

 

 

从委托移除方法

可以使用-=运算符从委托移除方法。

delVar -= SCl.m3;            //Remove the method from the delegate.

与为委托增加方法一样,其实是创建了一个新的委托。新的委托是旧委托的副本——只是没有了已经被移除方法的引用。

如下是移除委托时需要记住的一些事项:

1. 如果在调用列表中的方法有多个实例,-=运算符将从列表最后开始搜索,并且移除第一个与方法匹配的实例。

2. 试图删除委托中不存在的方法没有效果。

3. 试图调用空委托会抛出异常。

4. 我们可以通过把委托和null进行比较来判断委托的调用列表是否为空。如果调用列表为空,则委托是null。

 

 

调用委托

    可以像调用方法一样简单地调用委托。用于调用委托的参效将会用于调用调用列表中的每一个方法(除非有一个参数是输出参数,我们稍后会介绍)。

例如,如下代码中的delVar委托接受了一个int型输入值。使用参数调用委托就会使用相同的参数值(在这里是55)调用它的调用列表中的每一个成员。

 

MyDel delVar = inst.MyM1;

delVar += SCl.m3;

deIVar += X.Act;

……

delVar( 55 );   //调用委托

 

......

一个方法可以在调用列表中出现多次。如果这样,当委托被调用时,每次在列表中遇到这个方法时它都会被调用一次。

 

 

 

 

委托的示例

如下代码定义并使用了没有参数和返回值的委托。有关代码的注意事项如下:

Test类定义了两个打印函数。

Main方法创建了委托的实例并增加了另外三个方法。

程序然后调用了委托,也就调用了它的方法。然而在调用委托之前,它检测以确保它不是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()

{

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 emtpy”);

 

}

}

 

 

调用带返回值的委托

    如果委托有返回值并且在调用列表中有一个以上的方法,会发生下面的情况:

        调用列表中最后一个方法返回的值就是委托调用返回的值。

        调用列表中所有其他方法的返回值都会被忽略。

例如,如下代码声明了返回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()

    {

MyClass mc=new MyClass ();

MyDel mDel = mc.Add2;    //创建并初始化委托

mDel+=mc.Add3;    //增加方法

mDel+=mc.Add2;    //增加方法

Console.WriteLine(“Value: {0}”,mDel());    调用委托方法并使用返回值。

}

}

 

输出结果:Value: 12

 

 

 

调用带引用参数的委托

如果委托有引用参数,参数值会根据调用列表中的一个或多个方法的返回值而改变。

在调用委托列表中的下一个方法时,参数的新值(不是初始值)会传给下一个方法。

例如,如下代码调用了具有引用参数的委托。

delegate void MyDel( ref int X );

class MyClass

{

   public void Add2(ref int x) { x += 2; }

   public void Add3(ref int x) { x +=3; }

   static void Main()

   {

MyClass mc=new MyClass ();

MyDel mDel = mc.Add2;    //创建并初始化委托

mDel+=mc.Add3;    //增加方法

mDel+=mc.Add2;    //增加方法

 

int x=5;

mDel(ref x);

 

Console.WriteLine(“Value: {0}”,x);

 

}

}

 

输出结果:Value: 12

 

 

 

匿名方法

    至此,我们已经见过了使用静态方法或实例方法来初始化委托。对于这种情况,方法本身可以被代码的其他部分显式调用,当然,也就必须是某个类或结构的成员。

    然而,如果方法只会被使用一次,比如用来初始化委托会怎么样呢?在这种情况下,除了创建委托语法的需要,没有必要创建独立的具名方法。匿名方法允许我们避免使用独立的具名方法。

    匿名方法(anonymous method)是在初始化委托时内联(inline)声明的方法。

右边的版本使用了匿名方法来替代。没有底色的代码部分对于两个版本是一样的。

 

 

使用匿名方法

我们可以在如下地方使用匿名方法:

      声明委托变量时作为初始化表达式。

      组合委托时在赋值语句的右边。

      为委托增加事件时在赋值语句的右边。

 

匿名方法的语法

    匿名方法表达式的语法包含如下组成部分:

      delegate类型关键字。

      参数列表,如果语句块没有使用任何参数则可以省略。

      语句块,它包含了匿名方法的代码。

关键词    参数列表    语句块

delegate(Pcuweters){ ImplementationCode}

 

1.返回类型

匿名方法不会显式声明返回值。然而,实现代码本身的行为必须通过返回一个在类型上与委托的返回类型相同的值来匹配委托的返回类型。如果委托有void类型的返回值,匿名方法就不能返回值。

 

    例如,在如下代码中,委托的返回类型是int。匿名方法的实现代码因此也必须在代码路径中返回int。

委托类型的返回类型

delegate int OtherDel(int InParam);

static void Main()

{

OtherDel del = delegate(int x)

            {

                   return x+20 ;

                };

}

 

2.参数

  除了数组参数,匿名方法的参数列表必须在如下三方面匹配委托:

    参数数量

    参数类型

    修饰符

  我们可以通过使圆括号为空或省略圆括号来简化匿名方法的参数列表,但是仅在下面两项都为真的情况下才可以这样做。

      委托参数列表不包含任何out参数。

      匿名方法不使用任何参数。

    例如,如下代码声明了没有任何out参数的委托,匿名方法也没有使用任何参数。由于两个条件都满足了,我们就可以省略匿名方法的参数列表。

delegate void SorneDel(int X)  //声明委托类型

SomeDel SDel=delegate    //省略的参数列表

    {

        PrintMessage();

        Cleanup();

};

 

3.params参数

如果委托声明的参数列表包含了params参数,那么params关键字就会被匿名方法的参数列表忽略。

例如,在如下代码中:

    委托类型声明指定最后一个参数为params类型的参数。

然而,匿名方法参数列表忽略了params关键字。

 

    delegate void SomeOel( int X,params int[] Y);    //在委托类型声明中使用params关键词

 

    SomeDel mDel=delegate (int X,int[] Y)    //在匹配的匿名方法中省略关键词

    {

};

 

 

 

变量和参数的作用域

    参数以及声明在匿名方法内部的局部变量的作用域限制在实现方法的主体之内。

delegate vold MyDel( int x );

MyDel mDel = delegate(int y)    //y和z的作用域

{

    int z=10;

Console.WriteLlne(“{0}.{1}”,y,z);     

};

Console.WriteLlne(“{0}.{1}”,y,z);  //编泽错误  离开作用域

 

例如,如下匿名方法定义了参数y和局部变量z。在匿名方法主体结束之后,y和z就不在作用域内了。最后一行代码将会产生编译错误。

 

1.外部变量

    与委托的命名方法不同,匿名方法可以访问它们外围作用域的局部变量和环境。

        外围作用域的变量叫做外部变量(outer variable)。

        用在匿名方法实现代码中的外部变量称为被方法捕获(captured)。

    例如,方法中的代码可以访问x并输出它的值。

int x=5;   //变量x定义在匿名方法作用域的前面

MyDel mDel=delegate //变量x可以在你们方法作用域之内使用

{

    Console.WriteLine(“{0}”,x);    //使用外部变量x

}

 

 

2.被捕获变量的生命周期的扩展

    只要捕获方法还是委托的一部分,即使变量已经离开了作用域,被捕获的外部变量也会一直有效。

 

例如,在匿名方法中捕获的变量

 

delegate void MyDel();

static void Main()

{

MyDel mDel;

 

{   //x的作用域

        int x=5;       //变量x被定义在外部块中,在匿名方法之外。

        mDel=delegate

             {

                Console.WriteLine(“Value of x: {0}” , x);

};

}

//Console.WriteLine(“Value of x: {0}” , x);     变量x离开了作用域并且会导致编译错误

 

if(null!=mDel)

    mDel();      //x在这里使用,在匿名方法内部

 

}

 

 

局部变量x在块中声明和初始化。

然后,委托mDel被匿名方法初始化,该匿名方法捕获了外部变量x。

块关闭时,x超出了作用域。

如果块关闭之后的WriteLine语句的注释被取消,就会产生编译器错误。因为它引用的x现在已经离开了作用域。

然而,mDel委托中的匿名方法在它的环境中保留了x.并在mDel被调用时输出了它的值。

 

这段代码产生了如下的输出:

Value of x: 5

 

 

Lambda表达式

    C# 2.0引入了匿名方法,它允许我们在创建或为委托增加方法时包含小段内联代码。然而,匿名方法的语法有一点麻烦,而且需要一些编译器已经知道的信息。C# 3.0引入了lambda表达式,简化了匿名方法的语法,从而避免包含这些多余的信息。我们可能会希望使用Iambda表达式来替代匿名方法。其实,如果lambda表达式被先引入,那么就不会有匿名方法。

    在匿名方法的语法中,delegate关键字是有点多余,因为编译器已经知道我们在将方法赋值给委托。我们可以通过如下步骤把匿名方法转换为lambda表达式:

        删除delegate关键字。

        在参数列表和匿名方法主体之间放lambda运算符=>。lambda运算符读作“goes to"。

    如下代码演示了这种转换。第一行演示了将匿名方法赋值绘变量del。第二行演示了同样的匿名方法在被转换成lambda表达式之后,被赋值给了变量le1。

MyDel del = delegate(int x)  {return x+1j'j    //匿名方法

MyDel le1 =  (int x) => { return x+1;};    //表达式

说明:

术语lambda 表达式来源于数学家Alonzo Church等人在1920年到1930年期间发明的lambda积分。lambda积分是用于表示函数的一套系统,它使用希腊字母lambda(λ)来表示无名函数。近来,诸如Lisp和其方言的函数式编程语言使用这个术语来表示可以直接用于描述函数定义的表达式,表达式不再需要有名字了。

 

这种简单的转换少了一些多余的东西,看上去也更简洁了,但是只省了6个字符。然而,编译器可以通过推断,允许我们更进一步简化lambda表达式,如下代码所示。

 

编译器还可以从委托的声明中知道委托参数的类型,因此lambda表达式允许我们省略类型参数,如le2的赋值代码所示。

带有类型的参数列表称为显式类型。

        省略类型的参数列表称为隐式类型。

如果只有一个隐式类型参数,我们可以省略周围的圆括号,如le3的赋值代码所示。

最后,lambda表达式允许表达式的主体是语句块或表达式。如果语句块包含了一个返回语句,我们可以将语句块替换为return关键字后的表达式,如le4的赋值代码所示。

 

MyDel del = delegate(int x)    { return x + 1; } ;  //匿名方法

MyDel le1 =        (int x)  => { return x + 1; } ;  //Lambda表达式

MyDel le2 =          (x)  => { return x + 1; } ;  //Lambda表达式

MyDel le3 =           x  => { return x + 1; } ;  //Lambda表达式

MyDel le4 =           x  => x+1;        //Lanbda表达式

最后一种形式的lambda表达式的字符只有原始匿名方法的1/4,更简洁,更容易理解。

 

    如下代码演示了完整的转换。Main的第一行演示了被赋值给变量del的匿名方法。第二行演示了被转换成lambda表达式后的相同匿名方法,并赋值给变量了le1。

delegate double MyDel(int par);

static void Main()

{

MyDel del = delegate(int x)     { return x + 1; } ;   //匿名方法

MyDel le1 =        (int x)  => { return x + 1; } ;  //Lambda表达式

MyDel le2 =          (x)  => { return x + 1; } ;  //Lambda表达式

MyDel le3 =           x  => { return x + 1; } ;  //Lambda表达式

MyDel le4 =           x  => x+1;        //Lambda表达式

Console.WriteLine ("{o}', del (12));

Console. WriteLine (“{o}”, le1 (12)); Console.WriteLine ( "{0}", le2 (12));

Console. WriteLine (“{o}”, le3 (12)); Console.WriteLine ( "{0}”, le4 (12));

}

 

有关lambda表达式的参数列表的要点如下:

lambda表达式参数列表中的参数必须在参数数量、类型和位置上与委托相匹配。

  表达式的参数列表中的参数不一定需要包含类型(如隐式类型),除非委托有ref或out参数——此时类型是必须的(如显式类型)。

  如果只有一个参数,并且是隐式类型的,周围的圆括号可以被省略,否则它就是必须的。

  如果没有参数,必须使用一组空的圆括号。

 

 

lambda表达式的语法由lambda运算符和左边的参数部分以及右边的lambda主体构成:

    (参数,参数,)

    (参数)        =>    {语句}

    参数                     表达式

    ()