• 委托与事件 在.net的争霸战 ,你选择了谁?(异步委托产生的原因)


    如果你对委托和事件尚有模糊的地方请参阅上2篇博文。

    如果你对下面8个问题,可以轻而易举的回答,那博文对你没什么作用。

    1.为什么在发布者与订阅者的模式中,我们使用了事件而不使用委托变量?

    2.为什么我们通常的多播委托的返回类型都是void?

    3.如何让事件只允许一个方法注册?

    4.非void多播委托如何返回多个返回值?

    5.当委托链表的注册方法异常时,如何解决?

    6.如何解决事件中的委托方法的延时效果?

    7.实现异步委托...?

    8.保密

    <磨刀>

    理清思路:
    委托 好比中介所,你在我这里注册了方法,我就代替你完成任务。
    事件 好比微博,凡是收听我微博的人,只要我更新了微博(自己触发什么条件),收听我的人就会知道我更新了,以便做出自己的动作(评论/转发)。
    即:事件必须自己触发。

    <正文>
    下面来解决疑问:
    1.为什么在发布者与订阅者的模式中,我们使用了事件而不使用委托变量?

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub sub = new Sub();
    pub.Display += sub.OnDisplay;
    pub.ChangeNumber();
    pub.Display(100);

    }
    }
    public delegate void DisplayEventHandle(int number);
    class Pub
    {
    public DisplayEventHandle Display;

    public void ChangeNumber()
    {
    for (int i = 0; i < 10; i++)
    {
    if (i == 2)
    {
    if (Display != null)//如果注册方法
    {
    Display(i);
    }
    }
    }
    }
    }

    class Sub
    {
    public void OnDisplay(int i)
    {
    Console.WriteLine("The number is {0}",i);
    }
    }

    分析:("="是赋值操作,"+="是注册方法)
    .对于使用委托变量,那么在类的内部 委托变量(字段)必须是Public,这样不安全。
    . pub.Display(100);事件本来是需要调用ChangeNumber()方法当i=2的时候触发的,然后订阅者作出一系列的动作,但是现在 pub.Display(100);委托自己就可以调用订阅者的动作,影响到了所有订阅者。

    修改下代码:
     

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub sub = new Sub();
    pub.Display += sub.OnDisplay;
    pub.ChangeNumber();
    //pub.Display(100); 报错

    }
    }
    public delegate void DisplayEventHandle(int number);
    class Pub
    {
    public event DisplayEventHandle Display;

    public void ChangeNumber()
    {
    for (int i = 0; i < 10; i++)
    {
    if (i == 2)
    {
    if (Display != null)//如果注册方法
    {
    Display(i);
    }
    }
    }
    }
    }

    class Sub
    {
    public void OnDisplay(int i)
    {
    Console.WriteLine("The number is {0}",i);
    }
    }

    分析:
    .对象再也无法直接调用委托变量了,因为加了event事件,本质是生成了 私有private的委托。所以无法进行赋值操作,也不能直接调用。
    .这样给客户端 少了一些使用类内部变量的权力。

    2.为什么我们通常的多播委托的返回类型都是void?
    对于单播委托,咱们不讨论了,太简单了。
    对于多播委托:
    委托变量 +=方法1;
    委托变量 +=方法2;

    试想下,咱们都知道委托变量的声明,参数和返回类型都是和方法一样的,那么委托调用方法的时候,如果有返回值,到底这个返回值是 方法1,还是方法2的。

    比如:拿上面的微博案例来说,我是发布者,我可能今天心情不好,然后发布了一条微博,我根本不关心,谁收听我,也不关心收听者对我评论。即:事件发布者,他运行了某个动作之后,如果这个动作内的条件满足,那么就触发 订阅者的动作。事件发布者根本不需要关心,你订阅者的返回值。

    但是,你要说,我就是想知道返回值是多少?好,我们做个测试:

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub1 sub1 = new Sub1();
    Sub2 sub2 = new Sub2();
    pub.Display += sub1.OnDisplay;
    pub.Display += sub2.OnDisplay;
    pub.ChangeNumber();//I am Sub2

    }
    }
    public delegate string DisplayEventHandle();
    class Pub
    {
    public event DisplayEventHandle Display;

    public void ChangeNumber()
    {
    for (int i = 0; i < 10; i++)
    {
    if (i == 2)
    {
    if (Display != null)//如果注册方法
    {
    string str=Display();
    Console.WriteLine(str);
    }
    }
    }
    }
    }

    class Sub1
    {
    public string OnDisplay()
    {
    return "I am Sub1";
    }
    }

    class Sub2
    {
    public string OnDisplay()
    {
    return "I am Sub2";
    }
    }



    答案是:最后一个订阅者的返回值。

    3.如何让事件只允许一个方法注册?
    可能有时候,我的微博只想让一个人收听,不想让 前任女友收听,怎么办?
    从技术层面分析:
    1.我们订阅的时候,如何追加订阅者 通过符号"+="是吧?但是我们说过,“=”符号也可以委托定义,从这个角度着手,可不可以让事件变成不可访问的(private),然后在内部利用“=”进行订阅,而不是让客户自己“+=”符号订阅。

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub1 sub1 = new Sub1();
    Sub2 sub2 = new Sub2();
    pub.AddDL(sub1.OnDisplay);
    pub.AddDL(sub2.OnDisplay);
    pub.Speaking();


    }
    }
    public delegate string DisplayEventHandle();
    class Pub
    {
    private event DisplayEventHandle Display;

    public void AddDL(DisplayEventHandle method)
    {
    Display = method;//这里是"="不是"+="
    }
    public void RemoveDL(DisplayEventHandle method)
    {
    Display -= method;
    }
    public void Speaking()
    {
    if (Display != null)
    {
    string str = Display();
    Console.WriteLine(str);
    }
    }
    }

    class Sub1
    {
    public string OnDisplay()
    {
    return "I am Sub1";
    }
    }

    class Sub2
    {
    public string OnDisplay()
    {
    return "I am Sub2";
    }
    }


    分析:
    .有朋友可能会问,这样不是和刚才一样吗?带返回值的委托变量,其实不是的,当你使用+=的时候,2个方法都会被加入委托链表,而使用"="只是覆盖。
    .有点类似于属性,对,就是 事件访问器。
    如下代码:

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub1 sub1 = new Sub1();
    Sub2 sub2 = new Sub2();
    //pub.Display = sub1.OnDisplay;//错误,因为Display本质还是私有的委托,只是这里定义了2个委托
    pub.Display += sub1.OnDisplay;
    pub.Display += sub2.OnDisplay;//覆盖sub1方法
    pub.Speaking();

    }
    }
    public delegate string DisplayEventHandle();
    class Pub
    {
    private DisplayEventHandle display;
    public event DisplayEventHandle Display
    {
    add { display = value; }
    remove { display -= value; }
    }
    public void Speaking()
    {
    if (display != null)
    {
    string str = display();
    Console.WriteLine(str);
    }
    }
    }

    class Sub1
    {
    public string OnDisplay()
    {
    return "I am Sub1";
    }
    }

    class Sub2
    {
    public string OnDisplay()
    {
    return "I am Sub2";
    }
    }



    分析:
    1.通过分析,发现事件的本质是 生成一个private的委托变量,这就是为什么 无法通过 事件变量=方法 操作的原因,因为这个变量是委托的变量无法访问。但是可以通过事件访问器访问,修改事件访问器可以限制方法进入 委托链表。


    4.非void委托如何返回多个返回值?
    思路:
    1.我们知道 多播委托注册方法之后,会生产一个委托链表,那么我们可以把这个委托的注册方法遍历出来吗?

    View Code
    public delegate string DL();
    class Program
    {
    static void Main(string[] args)
    {
    DL one = DoSomething;
    one += Attacking;
    //delegate[] dls; 这样是错误的,下面解释了
    Delegate[] dlArray =one.GetInvocationList();
    foreach(var n in dlArray)
    {
    Console.WriteLine(n.Method.Name);//遍历方法名字
    }


    }

    static string DoSomething()
    {
    return "do it";
    }

    static string Attacking()
    {
    return "Attack";
    }
    }


    首先,我们要先理清出一个概念:
    delegate 与Delegate有什么区别?
    Delegate:是一个抽象基类,它引用静态方法或引用类实例及该类的实例方法。然而,只有系统和编译器可以显式地从 Delegate 类派生出委托类型。
    MulticastDelegate:是一个继承于Delegate的类,其拥有一个带有链表格式的委托列表,该列表称为调用列表,在调用多路广播委托时,将按照调用列表中的委托出现的顺序来同步调用这些委托。平常我们声明一个delegate的类型,都是继承于MulticastDelegate类的(注意:不能显式地从此类进行派生。这点与Delegate类是一样的,只有系统和编译器也可以显示地进行派生)。
    delegate 是一个C#关键字,用来定义一个新的委托类型(继承自MulticastDelegate类)。

    2.方法名遍历出来了,咱们利用List<>把每个方法的结果遍历出来

    View Code
    public delegate string DL();
    class Program
    {
    static void Main(string[] args)
    {
    DL one = DoSomething;
    one += Attacking;
    //delegate[] dls; 这样是错误的,下面解释了
    Delegate[] dlArray =one.GetInvocationList();
    List<string> lists = new List<string>();
    foreach(var n in dlArray)
    {
    Console.WriteLine(n.Method.Name);
    DL newone = (DL)n;//把Delegate显示转换成DL类型,因为DL类的基类是Delegate类
    string str = newone();
    lists.Add(str);
    }
    foreach (var n in lists)
    {
    Console.WriteLine(n);
    }
    }
    static string DoSomething()
    {
    return "do it";
    }

    static string Attacking()
    {
    return "Attack";
    }
    }



    应用于 事件中:

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub1 sub1 = new Sub1();
    Sub2 sub2 = new Sub2();
    pub.Display +=new DisplayEventHandle(sub1.OnDisplay);//注册方法
    pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注册方法

    List<string> lists=pub.Doing();//事件由发布者的某个条件触发
    foreach (var n in lists)
    {
    Console.WriteLine(n);
    }


    }
    }
    public delegate string DisplayEventHandle();

    class Pub //发布者
    {
    public event DisplayEventHandle Display;
    List<string> lists = new List<string>();
    Delegate[] dls;
    public List<string> Doing()
    {
    if (Display != null)
    {
    dls = Display.GetInvocationList();
    foreach (var n in dls)
    {
    Console.WriteLine(n.Method.Name);
    DisplayEventHandle one=(DisplayEventHandle)n;
    string str = one();
    lists.Add(str);
    }
    }

    return lists;
    }

    }

    class Sub1//订阅者
    {
    public string OnDisplay()
    {
    return "I am Sub1";
    }
    }
    class Sub2
    {
    public string OnDisplay()
    {
    return "I am Sub2";
    }
    }
    }


    事实上,发布者根不关心这些订阅者返回什么,它关心的是订阅者注册的方法是否正确,是否会报错,影响发布者的方法执行和后面订阅者方法的执行,所以 这种技术主要用于 返回 订阅者方法的异常处理信息。

    5.当委托链表的注册方法异常时,如何解决?

    源代码:

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub1 sub1 = new Sub1();
    Sub2 sub2 = new Sub2();
    pub.Display += new DisplayEventHandle(sub1.OnDisplay);//注册方法
    pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注册方法

    pub.Doing();//事件由发布者的某个条件触发

    }
    }
    public delegate string DisplayEventHandle(object sender, EventArgs e);

    class Pub //发布者
    {
    public event DisplayEventHandle Display;

    public void Doing()
    {
    if (Display != null)
    {
    Display(this,EventArgs.Empty);
    }


    }

    }

    class Sub1//订阅者
    {
    public string OnDisplay(object sender, EventArgs e)
    {
    return "I am Sub1";
    }
    }
    class Sub2
    {
    public string OnDisplay(object sender, EventArgs e)
    {
    return "I am Sub2";
    }
    }



    思考:如果订阅者方法异常了怎么办?对,我们利用try catch调试。
    修改代码:

    View Code
    public void Doing()
    {
    if (Display != null)
    {
    try
    {
    string str=Display(this, EventArgs.Empty);
    Console.WriteLine(str);
    }
    catch (Exception e)
    {
    Console.WriteLine(e.Message.ToString());
    }
    }
    }

    class Sub1//订阅者
    {
    public string OnDisplay(object sender, EventArgs e)
    {
    //return "I am Sub1";
    throw new Exception("sub1方法异常了");
    }
    }



    如果Sub1方法出了异常的话,那么就会终止 对 Sub2方法的调研,虽然 Doing()可以执行下去了。但是 影响了其他订阅者。
    从这个层面思考,我们把 注册的方面 按照上面提到过的遍历一下,就能解决,因为在Foreach循环内当一个方法出了问题,只影响到问题方法本身。

    修改代码如下:
     

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub1 sub1 = new Sub1();
    Sub2 sub2 = new Sub2();
    pub.Display +=new DisplayEventHandle(sub1.OnDisplay1);//注册方法
    pub.Display += new DisplayEventHandle(sub2.OnDisplay2);//注册方法

    List<string> lists=pub.Doing();//事件由发布者的某个条件触发
    foreach (var n in lists)
    {
    Console.WriteLine(n);
    }


    }
    }
    public delegate string DisplayEventHandle(object sender,EventArgs e);

    class Pub //发布者
    {
    public event DisplayEventHandle Display;
    List<string> lists = new List<string>();
    Delegate[] dls;
    public List<string> Doing()
    {
    if (Display != null)
    {
    dls = Display.GetInvocationList();
    foreach (var n in dls)
    {
    try
    {
    Console.WriteLine(n.Method.Name);
    DisplayEventHandle one = (DisplayEventHandle)n;
    string str = one(this, EventArgs.Empty);
    lists.Add(str);
    }
    catch (Exception e)
    {
    Console.WriteLine(e.Message.ToString());
    }
    }
    }

    return lists;
    }

    }

    class Sub1//订阅者
    {
    public string OnDisplay1(object sender,EventArgs e)
    {
    throw new Exception("Sub1方法异常了");
    }
    }
    class Sub2
    {
    public string OnDisplay2(object sender, EventArgs e)
    {
    return "I am Sub2";
    }
    }



    输出:
    OnDisplay1
    Sub1方法异常了
    OnDisplay2
    I am Sub2

    总结:这样即知道了哪个方法异常了,又不影响其他订阅者调用自己的方法。


    6.如何处理事件中的委托方法的超时?
    上面可知,订阅者的注册方法如果有问题,会导致异常,然后影响到发布者的Doing()方法,还有一种让到发布者的Doing()方法经过很长时间执行的,就是超时。
    但是超时不会影响发布者把 订阅者感兴趣的信息发布给订阅者,也不影响发布者的正常执行,只是执行Doing()会很长时间而已。

    分析下:
    1.发布者  执行某个动作的时候(事件由发布者自己触发),根据订阅者感兴趣的信息会调用 订阅者的注册方法(比如 当一个数字大于10的时候)。
    2.我们按F11调试的时候都会发现,当触发事件的时候,就会转到 订阅者的内部方法上去,也就是说,当前线程在 执行 订阅者的方法,所以 Main函数内部的客户端就在等待方法执行完毕之后,才能继续下面的代码操作。

    这里有点深度的:我举个例子
    当Main函数的在执行一个发布者的方法的时候
    比如 计算1-100的和,如果一个感兴趣的参数是和,当和大于10的时候,这个时候,线程就会转到 订阅者的方法上去,这个时候,客户端(Main函数的方法还能执行吗?)显然不可以继续执行了,必须等待订阅者执行完,才能继续下面的计算操作。

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub1 sub1 = new Sub1();
    Sub2 sub2 = new Sub2();
    pub.Display += new DisplayEventHandle(sub1.OnDisplay1);//注册方法
    pub.Display += new DisplayEventHandle(sub2.OnDisplay2);//注册方法

    List<string> lists=pub.Doing();//事件由发布者的某个条件触发
    foreach (var n in lists)
    {
    Console.WriteLine(n);
    }

    Console.WriteLine("线程已经回到Main函数");


    }
    }
    public delegate string DisplayEventHandle(object sender,EventArgs e);

    class Pub //发布者
    {
    public event DisplayEventHandle Display;
    List<string> lists = new List<string>();
    Delegate[] dls;
    public List<string> Doing()
    {
    if (Display != null)
    {
    dls = Display.GetInvocationList();
    foreach (var n in dls)
    {

    Console.WriteLine("现在是方法:"+n.Method.Name);
    DisplayEventHandle one = (DisplayEventHandle)n;
    string str = one(this, EventArgs.Empty);
    lists.Add(str);


    }
    }

    return lists;
    }

    }

    class Sub1//订阅者
    {
    public string OnDisplay1(object sender,EventArgs e)
    {
    Thread.Sleep(TimeSpan.FromSeconds(5));
    return "线程已转到Sub1,等待5秒,Sub1方法执行";
    }
    }
    class Sub2
    {
    public string OnDisplay2(object sender, EventArgs e)
    {
    return "线程已转到Sub2,Sub2方法执行。。。";
    }
    }



    输出:
    现在是方法:OnDisplay1
    现在是方法:OnDisplay2
    线程已转到Sub1,等待5秒,Sub1方法执行
    线程已转到Sub2,Sub2方法执行。。。
    线程已经回到Main函数


    我们是发布者,我们需要是立刻输出:  线程已经回到Main函数,而订阅者的超时影响了我发布者的延迟输出。
    还是拿微博来说:我是博主,我发微博就是抒发感情,和谁收听我,以及收听到我的信息没有以及如何对我做出反应都不关心。
    但是现在,我必须 等待订阅者 方法结束了,我才可以操作,太让人生气了,为了解决这个问题。
    怎么办?怎么办?IL看结构:
     


    分析:
    1.事件的本质我们知道,是生成 一个 private的委托变量
    2.委托的本质我们知道,是生成 一个完整的继承与MulticastDelegate的类,委托本质是个类
    这个类里包括:
    BeginInvoke()、EndInvoke()、Invoke()3个方法。
    我们记得要调用委托方法的时候是这样操作的:
    委托变量();其实 实质就是 委托变量.Invoke();
    对,就是这个方法是凶手,他妨碍了我们的发布者,让我们等待。

    KO它,开始 异步委托

    7.实现异步委托...?
    异步就是 一个主线程执行了(Main函数),你要是委托调用方法,那是你的事情,你自己重新开辟新线程去搞,别影响我的主线程。
    异步一般是 Begin 和End 出现。

    1.BeginInvoke()执行时,从线程池抓取一个 "没事干的"的线程来替我去告诉 订阅者调用委托方法。
    注:对于调用BeginInvoke()方法的时候,让线程去调用委托方法,这个委托变量必须只能有一个1个方法被绑定,如果是多播委托,必须像上面那么GetInvocationList()获得所有委托对象,先遍历出所有委托对象,再使用BeginInvoke()方法。
    2.Main()继续自己的线程执行下面的工作
    3.EndInvoke();当订阅者方法异常的时候,我们知道可以try catch捕获,但是只有在EndInvoke()才会抛出。(其实发布者并不关心这些抛出异常的信息),并且抛出异常也是在另一个进程上。

    好了,开始写代码:

    View Code
    class Program
    {
    static void Main(string[] args)
    {
    Pub pub = new Pub();
    Sub1 sub1 = new Sub1();
    Sub2 sub2 = new Sub2();
    pub.Display += new DisplayEventHandle(sub1.OnDisplay);//注册方法
    pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注册方法

    pub.Doing();//事件由发布者的某个条件触发
    Console.WriteLine("线程还在Main()");

    Console.ReadKey();//为什么这样写,因为主线程是Main函数,当他执行完之后,程序就结束了,可能子线程还没结束呢

    }
    }
    public delegate void DisplayEventHandle(object sender, EventArgs e);

    class Pub //发布者
    {
    public event DisplayEventHandle Display;

    public void Doing()
    {
    if (Display != null)
    {
    Delegate[] dels = Display.GetInvocationList();
    foreach (var n in dels)
    {
    DisplayEventHandle newone = (DisplayEventHandle)n;
    IAsyncResult result = newone.BeginInvoke(this, EventArgs.Empty, null, null);//新开辟一个线程

    //newone.EndInvoke(result);//加上这个效果如何?效果还是需要等待订阅者方法的结果,所以这个是没要添加的

    }
    }


    }

    }

    class Sub1//订阅者
    {
    public void OnDisplay(object sender, EventArgs e)
    {
    Thread.Sleep(TimeSpan.FromSeconds(3));
    Console.WriteLine("Sub1线程");

    }
    }
    class Sub2
    {
    public void OnDisplay(object sender, EventArgs e)
    {
    Console.WriteLine("Sub2线程");

    }
    }
    }



    输出结果:
    线程还在Main()
    Sub2线程
    Sub1线程

    总结:和我们预期的一样效果,注意上面的 Console.ReadKey(),不写这个,程序运行完成将导致子线程的输出无法完成。可能难以理解
    分析下:
    1.对于主线程就是 Main()函数,对于子线程分为 前台线程和后台线程,我们这里的就是后台线程,对于后台线程,只要主线程运行结束程序就结束了,不会管这些后台线程,但是对于前台线程注意了,必须前台线程结束了,主线程才会结束。

    线程这块知识暂时不讨论了,会有专门的博文发布。
    2.这里是 并行执行的,不要以为因为Foreach遍历就会先执行Sub1,其实是2个一起执行的。也就是说 后台线程最长利用了3秒钟。


    哈哈,是不是觉得异步调用委托方法学完了?错,这才刚刚开始:
    注意到没,上面有一个Console.ReadKey();
    必须输入一个 键,程序才会退出,所以异步调用就需要更多的控制,比如当后台线程执行完毕了,自动告诉客户端,我结束了,可以关闭程序了这类问题,比如 客户端需要 后台线程 执行的结果。

    总结:带着这些问题,将在明天发布《线程与异步调用委托方法的渊源》

  • 相关阅读:
    MATLAB画图之多个图以子图的形式合为一个图
    MATLAB画图之图中画局部放大的图中图
    MATLAB画图之自定义图片大小
    "廖雪峰的Git教程"学习笔记
    读书笔记之《程序员必读的职业规划书》
    Python初学者的资源总结
    时间管理去何方
    21天战拖记——Day21:《小强升职记》学习感受(2014-05-24)
    21天战拖记——Day20:整理自己的桌面(2014-05-23)
    21天战拖记——Day19:甘特图的使用(2014-05-22)
  • 原文地址:https://www.cnblogs.com/IAmBetter/p/2344115.html
Copyright © 2020-2023  润新知