C#笔记——2.委托

C#委托简介:

委托是C#语言提供的回调函数机制,是一种类型安全的回调机制。
从数据结构上来讲,委托和类一样是一种用户自定义类型;
从设计模式上来讲,委托提供了方法的抽象,每个委托对象都是一个包装了方法和调用方法时要操作的对象的包装器。

委托的简单构成

  • 声明委托类型
  • 必须有一个方法包含了要执行的代码
  • 创建一个委托实例
  • 调用(invoke)委托实例

1.声明委托类型
委托类型实际上只是参数类型的一个列表以及一个返回类型。它规定了类型的实例所能表示的操作。
例如定义一个自定义委托StringProcessor

delegate void StringProcessor(string input);

2.为委托实例的操作找到一个恰当的方法

声明过一个委托类型之后,要做的就是提供一个具有和声明的委托相同签名的方法。

3.创建委托实例

已经有了一个委托类型和有着正确签名的方法,接着要做的是创建委托类型的一个实例,指定在调用委托实例的时候就执行该方法。(我们可以称方法为委托实例的“操作”)

  • 如果要创建一个StringProcessor的委托实例,需要有个带字符串类型的参数并且无返回值的方法。

至于具体使用什么形式来创建委托实例,取决于操作使用的方法是静态方法还是实例方法:
如果是静态方法,则指定类型名称就可以了;如果是实例方法,就需要首先创建类型或者它的派生类型的一个实例(我们可以称这个对象为委托实例操作的“目标”,当调用委托实例的时候,就会为这个对象调用方法)

public static class StaticMethod {
    public static void PrintString(string value) {
        Console.WriteLine("The logs : {0} .",value);
    }
}

public class InstaiceMethod {
    public void PrintString(string value) {
        Console.WriteLine("The logs : {0} .", value);
    }
}
class Program
{
    delegate void StringProcessor(string input);
    static void Main(string[] args)
    {
        StringProcessor proc1, proc2;
        proc1 = new StringProcessor(StaticMethod.PrintString);

        InstaiceMethod instanceMethod = new InstaiceMethod();
        proc2 = new StringProcessor(instanceMethod.PrintString);

        proc1.Invoke("Use The Invoke Method");
        proc2("Use The Delegate Variable");

        Console.ReadKey();
    }
}

4.调用委托实例

调用一个委托实例实际上就是调用委托实例的一个叫做invoke()的方法。在委托类型中,该方法以委托类型的形式出现,并且具有和委托类型相同的参数列表和返回值类型

void Invoke(string input);

调用 Invoke 方法会执行委托实例的操作,向委托实例的操作提供调用Invoke时指定的参数。

proc1.Invoke("Use The Invoke Method");

C#将其简化:当有一个委托类型的变量时,就可以把它当做Invoke方法本身使用

proc2("Use The Delegate Variable");

委托推断

当需要一个委托实例的时候,可以只传送地址的名称,称之为委托推断。

例如代码:

    proc1 = new StringProcessor(StaticMethod.PrintString);

    proc2 = new StringProcessor(instanceMethod.PrintString);

可简写为:

    proc1 = StaticMethod.PrintString;

    proc2 = instanceMethod.PrintString;

使用委托推断,C#编译器为我们在内部创建的代码是一样的,编译器会用 pro1 检测需要的委托类型,因此编译器会创建一个相应的StringProcessor 委托类型的一个实例,将给定的方法的地址传递给委托的构造函数。

协变性和逆变性

变体Type Various机制,即逆变Contravarious与协变Covarious确定某种类型的值在什么情况下可以转换成另外一种类型的值。

当我们在为委托实例引用方法时,C#允许委托引用方法返回值类型的逆变以及方法参数类型的协变。
委托实例所引用的方法的参数类型可以是委托签名参数类型的基类,称为委托的逆变性。
委托实例所引用的方法的返回值类型可以使委托签名的返回值类型的派生类,称为委托的协变性

委托内部实现机制

当我们在开发中使用delegate关键字定义一个自定义的新的委托类型时

    delegate void StringProcessor(string input);

C#编译器会为我们的StringProcessor委托类型定义一个完整的类,包含4个方法:

  • 构造方法
  • Invoke
  • BeginInvoke
  • EndInvoke

说明了委托是类,凡是可以定义类的地方都可以定义委托,所以委托可以在全局范围中定义,也可以嵌套在一个类中定义

我们定义的StringProcessor委托类型的类的继承关系为:

StringProcessor ——> System.MulticastDelegate ——> System.Delegate ——> System.Object

System.MulticastDelegate 类

我们定义的所有委托都继承自System.MulticastDelegate 类,所以它们都继承了System.MulticastDelegate类
System.MulticastDelegate类中几个重要的非公有字段:

  • _target (System.Object类型) 引用调用委托所指向的方法时要操作的对象
  • _methodPtr (System.IntPtr类型) 运行时使用该字段标识要回调的方法
  • _iinvocationList (System.Object类型) 该字段值常为null;当构造委托链时该字段引用一个委托数组

System.Delegate 类

System.Delegate 类 主要包括两个静态方法Combine和Remove,且方法参数都是Delegate类型的。
因为我们定义的所有委托都继承自System.MulticastDelegate 类,System.MulticastDelegate类又继承自System.Delegate类,所以我们自定义的委托类型都可以作为这两个静态方法的参数。

实现细节

所有委托都有一个获取两个参数的构造方法,这两个参数分别是对对象的引用,以及一个IntPtr类型的用来引用方法的句柄:

public StringProcessor (Object @object,IntPtr method);

回顾我们构造委托实例时的代码:

    StringProcessor proc1 = new StringProcessor(StaticMethod.PrintString);

当C#编译器知道要创建委托实例时,会自动分析要引用的是哪个对象的哪个方法,分析完毕,将这两个参数传递给委托的构造函数,之后会保存至_target以及_methodPtr这两个私有字段中,此时的_invocationList字段会被设为null

当我们调用委托变量时,编译器会自动调用委托实例的Invoke方法,根据之前保存在_target以及_methodPtr中的对象及方法的引用,成功调用所需的方法。

总结:每个委托对象其实都是一个包装了方法和调用该方法时要操作的对象的包装器。

多播委托的内部实现

可将上方客户端代码写为:

    InstaiceMethod ins = new InstaiceMethod();

    StringProcessor procs = null;

    StringProcessor proc1 = new StringProcessor(StaticMethod.PrintString);
    StringProcessor proc2 = new StringProcessor(ins.PrintString);
    //我们可以通过Delegate.Combine和Delegate.Remove来合并委托和删除委托,C#也为委托类型的实例重载了“+=”和“-=”的操作符
    //procs = (StringProcessor)Delegate.Combine(procs,proc1);
    //procs = (StringProcessor)Delegate.Combine(procs,proc2);
    procs += proc1;
    procs += proc2;

    proc("多播委托");

具体实现:

  • 首先声明了委托变量procs并且初始化其值为null;
  • 第一次当我们使用Delegate.Combine或者+=来进行procs以及proc1的委托合并时,Delegate.Combine方法发现是null与proc1进行合并,Delegate.Combine方法会直接返回proc1所引用的委托实例;
  • 第二次当我们继续进行procs以及proc2的委托合并时,Delegate.Combine方法会发现要合并的并不是一个null值与一个委托实例,Delegate.Combine方法会重新构建一个新的委托实例,此时这个新的委托实例会对其_target以及_methodPtr这两个私有化字段进行初始化,并且之前的值一直为null的_invocationList字段会初始化为一个对委托实例数组的引用,此时,该数组的第一个元素为proc1的委托实例,第二个元素为proc2的委托实例;
  • 之后如果要继续合并委托,则会继续重新构造一个新的委托实例,以此类推。移除委托方法的内部实现也与之相似。

AnonymousMethod匿名方法

在上面简单委托的构成中提到,必须有一个方法包含需要委托进行的操作,并且该方法的参数和返回值必须符合委托的签名。C#2为我们提供了匿名方法的机制,使我们在实例化委托时可以使用不必声明方法的名称的匿名方法来简化语句。

简单的控制台应用程序:

    Func<string, string> echo = delegate (string input) {
        return "The Input is :" + input + " .";
    };

    Console.WriteLine(echo("C#"));
    Console.ReadKey();

使用匿名方法的规则:

  • 在匿名方法中不能使用跳转语句(break、goto或者continue)来跳转到该匿名方法外部,反之亦然,外部代码也不能通过跳转语句到匿名方法语句。

  • 在匿名方法内部不能访问不安全的代码

  • 匿名方法不能访问其外部的ref和out的参数,但可以使用其他外部变量

  • 匿名方法不支持参数的逆变性(必须指定和委托签名相配的参数类型)

  • 从C#3.0开始,可以使用Lambda表达式来替代匿名方法

Lambda表达式

从C#3.0开始,我们可以使用Lambda表达式来替代匿名方法,匿名方法可以做到的,Lambda也可以做到,它们本身并不是委托类型,但都可以隐式或者显式的转换成一个委托实例,Lambda表达式是匿名方法的进一步演化和简化。
上方例子使用Lambda可写为:

    Func<string, string> echo = input => "The Input is : " + input + " .";

    Console.WriteLine(echo("C#"));
    Console.ReadKey();

即Lambda运算符"=>"左边列出需要的参数,右边定义方法的实现代码。

lambda参数

lambda表达式有几种定义参数的方式,如果只有一种参数,则只写该参数的参数名即可:

    Func<string,string> onePara = s => String.Format("Change To Uppercase is : {0} .",s.ToUpper());

如果委托使用多个参数,则把参数名放在括号中:

    //可以把参数的类型写出来
    //Func<double,double,double>  twoPara = (double x ,double y) => x+y;


    Func<double,double,double>  twopPara = (x,y) => x+y;

lambda多行代码

当方法的代码内容只有一条语句时可以不写花括号和return语句,编译器会为我们添加一条隐式的return语句。

当方法的实现代码是多条语句时,则必须添加花括号和return语句;

闭包

和匿名方法一样,Lambda表达式也可以访问外部变量

    int initValue = 5;

    Func< int, int> calculateHandler = input => input + initValue;

    Console.WriteLine(calculateHandler(3).ToString());

    initValue = 7;

    Console.WriteLine(calculateHandler(3).ToString());

    Console.ReadKey();

lambda内部实现:
对于lambda表达式input => input + initValue,编译器会为我们创建一个匿名类,该匿名类会有一个构造函数来传递外部变量且该构造函数取决于从外部传递进来的变量的个数:

    public class AnonymousClass{
        private int initValue;
        public AnonymousClass(int initValue){
            this.initValue = initValue;
        }
        public int AnonymousMethod(int input){
            return input + initValue;
        }
    }

当我们调用该委托时,便会创建该匿名类的一个实例,并传递该外部值给匿名类的构造方法。

Action< T >和Func< T >泛型委托

除了为每个参数和返回值类型定义一个新的自定义的委托类型之外,我们可以使用Action< T >和Func< T >泛型委托。

泛型Action< T >委托表示引用void返回类型的方法,该委托类型存在不同的变体,可以传递最多16种不同的参数类型。没有泛型参数的Action委托类型可以调用没有参数的方法;Action< in T >调用带一个参数的方法;Action< in T1,in T2 >调用带有两个参数的方法。
例如上面的委托类型的定义和委托变量的可以使用Action< T >写作:

Action< string > proc1, proc2;

泛型Func< T >委托允许调用带返回类型的方法,和Action< T >委托相似,可以传递16个参数类型和一个返回类型。Func< out TResult > 可以调用没有参数带有一个返回值的方法;Func< in T1,out TResult >调用带有一个参数以及返回值的方法;Func< in T1,in T2,in T3 ,out TResult >调用带有三个参数并返回值的方法。

事件

在观察者模式中,主题Subject对象要具备通知已订阅的观察者Observer对象发生了特定事件的能力。在C#中,这种能力是通过在类型中定义的事件成员来实现的。

事件的简单使用:

  • 1.定义委托类型,确定回调方法原型
    public delegate void Handler(string para);
  • 2.定义事件成员

定义事件成员时要使用event关键字,并且每个事件成员的定义都要具备以下内容:

访问修饰符,为了让其他对象方便订阅一般为public
event关键字,表明正在定义事件成员
基础委托类型,由委托类型确定回调方法原型
事件成员的名称,事件的名称

    //访问修饰符 event关键字 基础委托类型 事件成员名称
    public event Handler OnHandler;
  • 3.定义触发事件的方法
    public void StartEvent(string para) {
        if (OnHandler != null) {
            OnHandler(para);
        }
    }

即事件的发布者类:

    class Publisher {
        public delegate void Handler(string para);

        public event Handler OnHandler;

        public void StartEvent(string para) {
            if (OnHandler != null) {
                OnHandler(para);
            }
        }
    }

事件的订阅者:

    class Subscriber1 {
        public void Method1(string input) {
            Console.WriteLine("Method1 output : {0}.",input);
        }

        public void Method2(string input) {
            Console.WriteLine("Method2 output :{0}.",input);
        }
    }

    class Subscriber2 {
        public void DoSomething1(string value) {
            Console.WriteLine("DoSomething1 {0}", value);
        }
        public void DoSomething2(string value)
        {
            Console.WriteLine("DoSomething2 {0}", value);
        }
    }

客户端代码:

    static void Main(string[] args)
    {
        Publisher pub = new Publisher();

        Subscriber1 sub1 = new Subscriber1();
        Subscriber2 sub2 = new Subscriber2();

        pub.OnHandler += sub1.Method1;
        pub.OnHandler += sub2.DoSomething2;

        pub.StartEvent("Test event!!!");

        Console.ReadKey();
    }

事件的内部实现

当我们在定义完委托类型后,定义事件成员时

    public delegate void Handler(string para);

    public event Handler OnHandler;

对于事件成员的定义,编译器会在幕后帮助我们将其转换为三个部分:

  • 一个私有化的委托字段 OnHandler,并且初始化为null
    private Handler OnHandler = null;
  • 一个公共方法 add_OnHandler ,用于其他方法订阅该事件

  • 一个公共方法 remove_OnHandler ,用于其他方法取消订阅该事件

1.私有委托字段OnHandler

在我们通过event 关键字定义好事件成员之后,编译器会首先为我们构造一个与事件同名的对应委托类型的委托变量OnHandler,用来引用委托列表,当事件被触发时便会通知委托类表中的委托,即调用委托列表中委托所以用的方法。委托变量Onhandler初始化为null,当其值为null时,证明没有方法订阅该方法;当有方法订阅该事件时,该委托变量就会引用为该方法自动构建的委托实例;当多个方法订阅事件时,即多播委托实现的,重新构建一个新的委托实例,初始化_target以及_methodPtr私有字段,以及_invocationList便引用包含多个相应的委托实例的数组。

需要注意的是,我们使用event 关键字定义的事件成员是public的,但是编译器为我们构造的委托变量是private的,这样可以有效防止我们在外部错误的调用。

2.公共方法 add_OnHandler
在编译器为我们构造一个与事件同名的对应委托类型的委托变量OnHandler之后,会继续构造一个add_OnHandler 的公共方法用于订阅事件。

3.公共方法 remove_OnHandler

公共方法 remove_OnHandler用于取消对事件的订阅。

REF

深入理解C#、C#高级编程、C#游戏脚本编程、Effective C#

原文地址:https://www.cnblogs.com/sylvan/p/8909125.html