[C#] 委托与事件(2)

网上讲C#委托和事件的博文已经非常多了,其中也不乏一些深入浅出、条理清晰的文章。我之所以还是继续写,主要是借机整理学习笔记、归纳总结从而理解更透彻,当然能够以自己的理解和思路给其他人讲明白更好。
另外,太长的文章会让很多读者失去兴趣,所以我决定把这篇分成四个部分来介绍。分别是委托的基础、委托的进阶、事件的基础和事件的进阶。对使用委托与事件要求不高的同学可以跳过进阶部分。

本文接着讲委托的高级知识,上一节请参见C#委托与事件(1)


4. 委托的高级知识

(1) 委托的类组成

前面我们介绍过了,委托实际上也是一个类,只不过它的对象不是一个普通的变量,而是一个方法。但是我们实际使用时并不需要定义这个类,而只是声明一下委托即可。这是因为当C#编译器处理委托类型时,会自动产生一个派生自System.MulticastDelegate的密封类,这个类与它的基类System.Delegate一起为委托提供必要的基础设施。
下面我们就以上一节中声明的委托delegate void GreetingDelegate(string s);来看看该类的组成。

public sealed class GreetingDelegate : System.MulticastDelegate
{
    public void Invoke(string s);
    public IAsyncResult BeginInvoke(string s, AsyncCallback cb, object state);
    public void EndInvoke(IAsyncResult result);
}

可以看到,该类中定义了三个公共方法:
(a) Invoke(),它被用来以同步方式调用委托对象维护的每个方法。所谓同步是指调用者必须等待调用完成才能继续执行。Invoke()方法定义的参数和返回值完全匹配我们要定义的类GreetingDelegate。另外,Invoke()不能直接调用,而是在后台调用。
(b) BeginInvoke(),用于异步调用,它最前面的参数列表是GreetingDelegate定义的方法类的参数列表,此外,还有两个参数AsyncCallbackobject用于异步方法调用。
(c) EndInvoke(),与BeginInoke()联合用于异步调用,它的返回值与委托声明的返回值一致,而它的唯一参数则是BeginInvoke()返回的类型IAsyncResult接口。

另外,上面我们定义的委托没有返回值,也可以定义返回值类型,这样Invoke()EndInvoke()对应的返回值就不是void了。委托还可以指向包含任意数量out或者ref参数的方法,按道理只有Invoke()BeginInvoke()方法与委托的参数列表有关,需要加上相应的out或者ref参数,但是由于异步调用时需要通过EndInvoke()来返回结果,所以EndInvoke()的参数列表中需要加上out或者ref参数。

下面简单介绍一下委托类的父类System.MulticastDelegate

public abstract class MulticastDelegate : Delegate
{
    // 返回所指向的方法列表
    public sealed override Delegate[] GetInvocationList();
    // 重载等于和不等于操作符
    public static bool operator ==(MulticastDelegate d1, MulticastDelegate  d2);
    public static bool operator !=(MulticastDelegate d1, MulticastDelegate d2);

    // 用来在内部管理委托所维护的方法列表
    private IntPtr _invocationCount;
    private object _invocationList;
}
public abstract class Delegate : IConeable, ISerializable
{
    // 与函数列表交互的方法
    // 给委托维护的列表添加一个方法,在C#中使用重载+=操作符调用此方法
    public static Delegate Combine(params Delegate[] delegates);
    public static Delegate Combine(Delegate a, Delegate b);
    // 从调用列表中移除一个或所有的方法,在C#中使用-=操作符调用此方法
    public static Delegate Remove(Delegate source, Delegate value);
    public static Delegate RemoveAll(Delegate source, Delegate value);

    // 重载操作符
    public static bool operator ==(Delegate d1, Delegate d2);
    public static bool operator !=(Delegate d1, Delegate d2);
 
    // 扩展委托目标的属性
    public MethodInfo Method { get; } //用以表示委托维护的静态方法的详细信息
    public object Target { get; } // 如果方法调用是定义在对象级别的,
                                  // Target返回表示委托维护的方法的对象;
                                  // 如果调用的方法时一个静态成员,
                                  // Target返回null
}
(2) 泛型委托

C#允许我们定义泛型委托,即当我们定义的委托接受的参数可能会不同时,我们可以通过类型参数来构建。下面我们来改写一下上一节中定义的那个委托,使得它不仅支持传入string,还支持传入整型。

namespace TestDelegate
{
    delegate void GreetingDelegate<T>(T arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            // do something (hug or shake hand...)
        }

        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            // do something (hug or wave hand...)
        }

        public static void GreetingTimes(int n)
        {
            Console.WriteLine("  Greeting {0} times!", n); 
        }

        static void MakeGreeting<T>(T name, GreetingDelegate<T> greeting)
        {
            greeting(name);
        }

        static void Main(string[] args)
        {
            GreetingDelegate<string> d1 = Hello;        //定义委托的一个对象(将方法绑定到委托)
            d1 += Goodbye;                              // 在d1上再绑定一个委托
            GreetingDelegate<int> d2 = GreetingTimes;   //定义委托的另一个对象
            MakeGreeting("April", d1);
            MakeGreeting(99, d2);
        }
    }
}

输出内容:

  Hello, April!
  Goodbye, April!
  Greeting 99 times!

有了泛型委托,很多方法都可以用一个委托模板表示出来,因此C#中提供了两个常用的泛型委托Action<T>Func<T>来避免用户手工构建自定义委托的麻烦。

Action<T>

Action<T>泛型委托定义的方法,参数列表可以多至16个(使用时需要指定各个参数的类型),返回值为void
例如,我们有一个方法为static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount),若把它作为Action<T>委托的一个目标,则委托的实例化时需要这样写:
Action<string, ConsoleColor, int) actionTarget = new Action<string, ConsoleColor, int>(DisplayMessage);
调用委托方法时:
actionTarget("your input string", ConsoleColor.Green, 2);

Func<T>

Func<T>泛型委托也可以指向多至16个参数的方法,但与Action<T>不同的是,它具有自定义的返回值,具体用法类似,不再赘述。

(3) 委托的异步调用

在说委托的异步调用之前,我们先对第一节最早的delegate例子做一个简单的改进,看看它的工作流程。首先,MakeGreeting()方法中除了调用greeting()之外,需要再调用一个额外的方法FuncAfterGreeting()。然后,我们定义了GreetingDelegate的一个对象d1,并在d1上绑定了Hello()Goodbye()方法。最后我们调用MakeGreeting()方法来看看输出结果。

namespace AsyncDelegateTest
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 1 second");
            Thread.Sleep(1000);
            Console.WriteLine("  Finished Hello");
        }

        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            Console.WriteLine("  Waiting for 2 second");
            Thread.Sleep(2000);
            Console.WriteLine("  Finished Goodbye");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            greeting(name); // 这里相当于是greeting.Invoke(name);
            FuncAfterGreeting();
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     //定义委托的一个对象(将方法绑定到委托)
            d1 += Goodbye;                  //定义委托的另一个对象
            MakeGreeting("April", d1);
        }
    }
}

输出结果:

  Hello, April!
  Waiting for 1 second
  Finished Hello
  Goodbye, April!
  Waiting for 2 second
  Finished Goodbye
  Do some other things...
  Waiting for 3 second
  Finished FuncAfterGreeting

从输出我们可以看到MakeGreeting()方法中,首先调用了Hello()方法,并运行完毕;然后调用了Goodbye()方法,并运行完毕;最后调用FuncAfterGreeting(),并运行完毕;至此,整个MakeGreeting()方法运行完毕。
这就是采用同步的方式调用委托,这样委托对象绑定的每个方法要依次执行,而且后者必须等前者执行完毕之后才能开始执行。另外,只有把委托对象绑定的所有方法执行完毕后才能回到MakeGreeting()方法中继续往下执行。

而在(1)中我们介绍的BeginInvoke()EndInvoke()函数能使委托实现异步调用,所谓异步调用,就是在上例中MakeGreeting()方法中的线程去执行greeting方法时利用线程池中的线程去实现调用,自己则继续往下执行。有了BeginInvoke()EndInvoke()这两个函数后,异步调用就很简单了,直接先用greeting调用BeginInvoke()函数,然后就可以做其他的事情,结束之间再调用EndInvoke()即可。

namespace AsyncDelegateTest2
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished Hello");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            IAsyncResult result = greeting.BeginInvoke(name, null, null);
            FuncAfterGreeting();
            greeting.EndInvoke(result);
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     //定义委托的一个对象(将方法绑定到委托)
            MakeGreeting("April", d1);
        }
    }
}

输出结果:

  Do some other things...
  Hello, April!
  Waiting for 3 second
  Waiting for 3 second
  Finished Hello
  Finished FuncAfterGreeting

为什么我这里把Goodbye方法去掉了,这是因为BeginInvoke()只能在绑定了单个方法的delegate上调用,如果我们在d1上还绑定了其他方法,那么去调用BeginInvoke()的时候会出现下面的异常:

Unhandled Exception: System.ArgumentException: The delegate must have only one target.

当然如果你一定要绑定多个方法这样用的话,可以先通过GetInvocationList()获得绑定的方法列表,然后依次调用BeginInvoke()方法。

namespace AsyncDelegateTest3
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished Hello");
        }

        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            Console.WriteLine("  Waiting for 2 second");
            Thread.Sleep(2000);
            Console.WriteLine("  Finished Goodbye");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            Delegate[] delArray = greeting.GetInvocationList();
            foreach (var d in delArray)
            {
                var del = (GreetingDelegate)d;
                IAsyncResult result = del.BeginInvoke(name, null, null);
            }
            FuncAfterGreeting();
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     // 定义委托的一个对象(将方法绑定到委托)
            d1 += Goodbye;                  // 定义委托的另一个对象
            MakeGreeting("April", d1);
            Console.ReadLine(); // 如果不加这行的话,很可能Hello方法还没执行完程序就退出了,
                                // 因为我们没有调用EndInvoke()去检查它们的状态
        }
    }
}

输出结果:

  Do some other things...
  Hello, April!
  Goodbye, April!
  Waiting for 2 second
  Waiting for 3 second
  Waiting for 3 second
  Finished Goodbye
  Finished FuncAfterGreeting
  Finished Hello

回到AsyncDelegateTest2,这里其实还存在一个问题。那就是MakeGreeting()方法中的这句话greeting.EndInvoke(result);,如果Hello()方法需要执行30s,那么3s后FuncAfterGreeting()方法就执行完毕了,主线程执行到EndInvoke()这句话。而这句话就相当于让主线程一直去查询Hello()方法是否执行完毕。那么问题来了,能不能不要这么麻烦主线程,而是让Hello()方法执行完毕后自动告诉主线程呢?这就是异步回调。

namespace AsyncDelegateTest4
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 5 second");
            Thread.Sleep(5000);
            Console.WriteLine("  Finished Hello");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            IAsyncResult result = greeting.BeginInvoke(name, new AsyncCallback(FuncForCallBack), "AsycState:OK");
            FuncAfterGreeting();
        }

        static void FuncForCallBack(IAsyncResult result)
        {
            // AsyncResult should using System.Runtime.Remoting.Messaging
            GreetingDelegate handler = (GreetingDelegate)((AsyncResult)result).AsyncDelegate;
            handler.EndInvoke(result);
            Console.WriteLine(result.AsyncState);
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;
            MakeGreeting("April", d1);
            Console.ReadLine();
        }
    }
}

输出结果:

  Do some other things...
  Hello, April!
  Waiting for 5 second
  Waiting for 3 second
  Finished FuncAfterGreeting
  Finished Hello
  AsycState:OK

这里我们定义了一个回调函数FuncForCallBack(),这样就不需要在MakeGreeting()方法的最后显示地去调用EndInvoke()去检查委托方法的执行状态了。

参考文献:
《精通C#》
张子阳的《C# 中的委托和事件》
Delegates Tutorial
也来说说C#异步委托
C#委托的异步调用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容