C#中的委托和事件

定義:委托是一種引用類型,表示對(duì)具有特定參數(shù)列表和返回類型的方法的引用。 在實(shí)例化委托時(shí),你可以將其實(shí)例與任何具有兼容簽名和返回類型的方法相關(guān)聯(lián)
目的:方法聲明和方法實(shí)現(xiàn)的分離,使得程序更容易擴(kuò)展

一、對(duì)委托的理解

1. 為什么將方法作為另一個(gè)方法的參數(shù)

先不解釋定義,看一段代碼

  public void Method1(object obj)
   {
      //內(nèi)部可以訪問(wèn)obj的成員
   }

這是隨便寫(xiě)的一個(gè)方法,沒(méi)有實(shí)際意義,但是,根據(jù)我們已掌握的關(guān)于類型的基礎(chǔ)知識(shí),應(yīng)該明白,這里的obj(引用類型)作為形參,存放的是對(duì)象的引用,既然獲取到了對(duì)象的引用,那么我們可以在run方法內(nèi)部對(duì)obj的成員進(jìn)行訪問(wèn)(一段廢話)。好了,現(xiàn)在我要問(wèn)一個(gè)問(wèn)題:為什么要將obj作為參數(shù)?

問(wèn)題先慢慢想著,我們?cè)俅慰匆幌挛械亩x,"是引用類型,對(duì)方法的引用",看下面的代碼

  public void Method2(delegate del)
   {
      //內(nèi)部可以訪問(wèn)del的什么?
      //只能執(zhí)行方法
      del();
   }

delegate 作為一種引用類型,引用的是個(gè)方法,我們能對(duì)方法做什么,只能執(zhí)行方法。
下面我們回答剛才的問(wèn)題,obj作為參數(shù)(類型聲明),將類型的聲明和類型的實(shí)例分離。當(dāng)然這樣做的目的是為了封裝變化,所有類型都可以作為實(shí)參來(lái)使用Method1,因?yàn)镺bject是基類,當(dāng)然也可以將Object換成其他接口類型,只要實(shí)現(xiàn)了該接口的類型都可以作為Method1的實(shí)參。
雖然Method1和Method2參數(shù)類型不一樣,但是目的是一致的,委托是將方法的聲明和實(shí)現(xiàn)分離。
下面看一個(gè)網(wǎng)上使用廣泛的例子

    示例1
     //定義委托,與任何具有兼容簽名和返回類型的方法相關(guān)聯(lián)
     public delegate void GreetingDelegate(string name);
    class Program
    {
        private static void EnglishGreeting(string name)
        {
            Console.WriteLine("Morning, " + name);
        }
        private static void ChineseGreeting(string name)
        {
            Console.WriteLine("早上好, " + name);
        }
        //將委托類型GreetingDelegate作為形參聲明
        private static void GreetPeople(string name, GreetingDelegate MakeGreeting)
        {
            //這里可以完成其他的業(yè)務(wù)邏輯
            //然后調(diào)用委托
            MakeGreeting(name);
        }
        static void Main(string[] args)
        {
             GreetPeople("hanmeimei", EnglishGreeting);//使用靜態(tài)方法初始化委托
             GreetPeople("韓梅梅", new Program().ChineseGreeting);//使用實(shí)例方法初始化委托
             Console.ReadKey();
        }
    }

委托類型GreetingDelegate作為形參聲明,只要是具有兼容簽名和返回類型的方法都可以作為GreetPeople方法的實(shí)參。這樣一來(lái),GreetPeople方法不僅可以通過(guò)中文和英文問(wèn)好了,所有與委托類型GreetingDelegate相關(guān)聯(lián)的語(yǔ)言(方法)都可以問(wèn)好了,程序更容易擴(kuò)展了。

2. 委托是一種引用類型

既然委托是一種類型,應(yīng)該包含類型的成員,我們將代碼編譯完成后,借助反編譯工具看下,編譯后的樣子:


委托的成員

下面這兩種方式是等同的:

 MakeGreeting(name);
 MakeGreeting.Invoke(name);

而B(niǎo)eginInvoke和EndInvoke是屬于異步調(diào)用的范疇,我們稍后再說(shuō)。

3.Lambda表達(dá)式比匿名方法簡(jiǎn)約

從示例1中可以看到分別顯示調(diào)用了靜態(tài)方法和實(shí)例方法初始化了委托,但是對(duì)于那些只使用一次的方法,就沒(méi)有必要?jiǎng)?chuàng)建具名方法了。C#2.0提出了使用匿名方法代替具名方法的解決方案

  GreetingDelegate MakeGreeting = delegate (string name)
  {
     Console.WriteLine("早上好, " + name);
  };
  GreetPeople("韓梅梅", MakeGreeting);

匿名方法仍然比較繁瑣,C#3.0引入了Lambda表達(dá)式

    //去掉delegate關(guān)鍵字并添加 =>運(yùn)算符
  GreetingDelegate MakeGreeting = (string name) =>
   {
        Console.WriteLine("早上好, " + name);
   };
  GreetPeople("韓梅梅", MakeGreeting);   
  //根據(jù)委托的參數(shù)類型推斷,string類型也去掉了
  GreetingDelegate MakeGreeting = name =>
  {
      Console.WriteLine("早上好, " + name);
  };
  GreetPeople("韓梅梅", MakeGreeting);         
4. 委托也是類型安全的
  GreetingDelegate MakeGreeting1 = (int name) =>
  {
      Console.WriteLine("早上好, " + name);
  };
簽名不同報(bào)錯(cuò)
5. 泛型委托的協(xié)變和逆變

常用泛型委托

  • Action<in T,....>:有參數(shù)無(wú)返回值
  • Func<in T1,...,out TResult>:有參數(shù)有返回值,最后一個(gè)類型參數(shù)為返回值類型

關(guān)于泛型委托的協(xié)變和逆變可以閱讀這篇文章《C#基本功之泛型》
有了常用泛型委托,我們就不需要自己定義GreetingDelegate委托類型了,對(duì)委托的使用進(jìn)一步的簡(jiǎn)化了。

     //用泛型委托聲明形參,F(xiàn)unc于此類似,只不過(guò)有返回值而已
     private static void GreetPeople(string name,Action<string> action)
     {
            //這里可以完成其他的業(yè)務(wù)邏輯
            //然后調(diào)用委托
            action.Invoke(name);
     }
   //調(diào)用
   GreetPeople("韓梅梅", name => Console.WriteLine("早上好, " + name))

到目前為止,我們將方法的變化抽象,并用委托封裝,實(shí)現(xiàn)了委托的簡(jiǎn)單使用。但委托還有很大的用處。
委托是類的成員,我們看一個(gè)作為類的成員使用的例子:現(xiàn)在生活中智能設(shè)備越來(lái)越普及,以前需要自己動(dòng)手拉開(kāi)窗簾、打開(kāi)熱水器等等,現(xiàn)在只需要設(shè)定場(chǎng)景,利用智能設(shè)備就可以完成這些操作。
當(dāng)時(shí)間定格為早上7點(diǎn)的時(shí)候,鬧鐘想起,窗簾自動(dòng)打開(kāi),熱水器開(kāi)始燒水,加濕器關(guān)閉.....

 //先不用管EventArgs參數(shù),object 類型的sender,可以理解為任何類型都可以傳遞
 public delegate void EventHandler(object sender, EventArgs e);
 /// <summary>
 /// 控制中心
 /// </summary>
 public class ControlCore
  {
         public DateTime Time { get; set; } = DateTime.Now;
        /// <summary>
        /// 執(zhí)行任務(wù)
        /// </summary>
        public event EventHandler Task;
 }
 static void Main(string[] args)
 {
         var controlCore= new ControlCore();
         controlCore.Task= new EventHandler(AlarmClock);
         //怎么操作委托?
         //添加、移除
         controlCore.Task+=....;
        controlCore.Task-=....;
        //或者直接覆蓋掉
       controlCore.Task=....;
       controlCore.Task(null, null);
  }
  /// <summary>
  /// 鬧鐘
  /// </summary>
 public static void AlarmClock(object sender, EventArgs e)
  {
      Console.WriteLine("起床了,親");
 }

作為類的成員時(shí),要怎么操作委托?我們都知道委托還可以添加或移除方法,所以我們不僅可以直接調(diào)用委托,還可以添加、移除或者直接覆蓋委托,對(duì)委托的操作沒(méi)有任何限制。如此一來(lái),破壞了類的封裝性。我們希望對(duì)委托有一些限制,就像用屬性去限制字段的輸入輸出一樣;

        //將委托的訪問(wèn)級(jí)別改為private
        private EventHandler Task;
       //為委托添加方法
        public void AddTask(EventHandler handler)
        {
            if (this.Task== null)
                this.Task= new EventHandler(handler);
            else
                this.Task+= new EventHandler(handler);
        }
       //移除方法
        public void RemoveTask(EventHandler handler)
        {
            System.Delegate.Remove(this.Task, handler);
        }
       //因?yàn)槲卸x為private,所以需要提供調(diào)用委托的接口
        public void OnTask(EventArgs e)
        {
            //7點(diǎn)了
            if (Time.Hour == 7)
            {
                if (this.Task!= null)
                {
                    this.Task(this, e);
                }
            }
        }


如果對(duì)委托的使用僅僅是添加或移除方法,然后執(zhí)行委托的調(diào)用列表,我相信用事件會(huì)更簡(jiǎn)單容易;
事件是以委托為基礎(chǔ),可以理解為對(duì)委托的進(jìn)一步封裝。

二、對(duì)事件的理解

1. 事件是將委托封裝,并對(duì)外公布了訂閱和取消訂閱的接口

將委托private EventHandler Task;修改為事件public event EventHandler Task;而我們創(chuàng)建的方法AddTask和RemoveTask也需要去掉了,重新生成代碼,通過(guò)反編譯工具可以看到:

重新生成

事件編譯后生成的兩個(gè)方法,與我們的示例中AddClick和RemoveClick方法類似;同時(shí)可以看到EventHandler委托,已經(jīng)字段變?yōu)閜rivate的訪問(wèn)級(jí)別了(小鎖表示私有);這樣一來(lái),事件幫我們完成了對(duì)委托的“限制“;
在客戶端訪問(wèn)事件也只能+=(訂閱)或者 -=(取消訂閱)了,如果直接用“=“運(yùn)算符賦值就會(huì)報(bào)錯(cuò)(在Control類內(nèi)容還是可以的);


報(bào)錯(cuò)了
2. 事件使用 發(fā)布-訂閱(publisher-subscriber) 模型

發(fā)布者:包含事件的類用于觸發(fā)事件,而這個(gè)類稱為事件的“發(fā)布者”。
通過(guò)聲明委托類型的事件,將委托與事件關(guān)聯(lián)。發(fā)布者對(duì)象調(diào)用這個(gè)事件,并通知訂閱者對(duì)象
訂閱者:其他注冊(cè)該事件的類稱為“訂閱者”。
訂閱者注冊(cè)事件并提供事件處理程序(鬧鐘、打開(kāi)窗簾、熱水器燒水等),在發(fā)布者類中通過(guò)委托調(diào)用訂閱者的事件處理程序。

   public delegate void EventHandler(object sender, EventArgs e);
  /// <summary>
  /// 控制中心
  /// </summary>
  public class ControlCore
  {
      public DateTime Time { get; set; } = DateTime.Now;
      /// <summary>
      /// 執(zhí)行任務(wù)
      /// </summary>
      public event EventHandler Task;
      /// <summary>
      /// 觸發(fā)事件
      /// </summary>
      /// <param name="e"></param>
      public void OnTask(EventArgs e)
      {
          //7點(diǎn)了
          if (Time.Hour == 7)
          {
              if (this.Task != null)
              {
                  this.Task(this, e);
              }
          }
      }
  }
      

訂閱者類中的事件處理程序

        //下面的方法分別屬于訂閱者類中的方法,篇幅有限,沒(méi)有單獨(dú)聲明每一個(gè)訂閱者類
        /// <summary>
        /// 鬧鐘響起
        /// </summary>
        public static void AlarmClock(object sender, EventArgs e)
        {
            var core = (ControlCore)sender;
            Console.WriteLine(core.Time.Hour+"點(diǎn)了,起床了,親");
        }
        /// <summary>
        /// 打開(kāi)窗簾
        /// </summary>
        public static void OpenWindow(object sender, EventArgs e)
        {
            var core = (ControlCore)sender;
            Console.WriteLine(core.Time.Hour + "點(diǎn)了,打開(kāi)窗簾");
        }
        /// <summary>
        /// 熱水器燒水
        /// </summary>
        public static void BoilWater(object sender, EventArgs e)
        {
            var core = (ControlCore)sender;
            Console.WriteLine(core.Time.Hour + "點(diǎn)了,熱水器開(kāi)始燒水");
        }

客戶端代碼

        static void Main(string[] args)
        {
            var controlCore = new ControlCore();
            controlCore.Task += new EventHandler(AlarmClock);
            controlCore.Task += OpenWindow;
            controlCore.Task += BoilWater;
            while (true)
            {
                controlCore.OnTask(null);
                Console.ReadKey();
            }
        }

執(zhí)行結(jié)果

7點(diǎn)了,起床了,親
7點(diǎn)了,打開(kāi)窗簾
7點(diǎn)了,熱水器開(kāi)始燒水
3. 關(guān)于sender和EventArgs

在示例代碼中,可以看到將ControlCore本身作為參數(shù)傳遞給訂閱者,既然訂閱了發(fā)布者的動(dòng)態(tài),那么關(guān)于發(fā)布者的某些信息或許感興趣(比如ControlCore中的Time)。
而EventArgs是作為發(fā)布者信息之外的信息傳遞

   //自定義參數(shù)類型
   public class CustomEventArgs: EventArgs
    {
        public string Arg1 { get; set; }
        public string Arg2 { get; set; }
    }

用CustomEventArgs替換委托中的參數(shù)EventArgs。public delegate void EventHandler(object sender, CustomEventArgs e)
那么在客戶端調(diào)用時(shí)傳入更多的信息

  controlCore.OnTask(new ButtonEventArgs()
 {
            Arg1="",
            Arg2=""
   });

總結(jié):一直想寫(xiě)一篇關(guān)于委托和事件的文章,但是網(wǎng)上已經(jīng)有很多這類優(yōu)秀的文章了,不乏一些佼佼者,由淺入深,從無(wú)到有的風(fēng)格將知識(shí)點(diǎn)講的很透徹。如果我再按照這個(gè)類型去寫(xiě),實(shí)在沒(méi)有意思。
所以我想,我們可不可以從已知到未知這條路徑來(lái)將知識(shí)點(diǎn)講明白,比如,我們知道將具有相同屬性和行為的對(duì)象抽象為類型。那么我是不是可以將具有相同簽名和返回類型的方法抽象為委托?再比如,我們知道屬性封裝了字段,并對(duì)字段的輸入輸出進(jìn)行了限制。那么我是不是可以將委托封裝,控制委托的注冊(cè)或取消,這樣就引出了事件。
希望可以幫助到朋友們

:.NET關(guān)于委托和事件的編碼規(guī)范

  • 委托類型的名稱都應(yīng)該以EventHandler結(jié)束。
  • 事件的命名為 委托去掉 EventHandler之后剩余的部分。
  • 委托的原型定義:有一個(gè)void返回值,并接受兩個(gè)輸入?yún)?shù):一個(gè)Object 類型,一個(gè) EventArgs類型(或繼承自EventArgs)。
  • 繼承自EventArgs的類型應(yīng)該以EventArgs結(jié)尾。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本來(lái)應(yīng)該學(xué)習(xí)泛型與委托的,但是發(fā)現(xiàn)C#這里還沒(méi)有系統(tǒng)的記錄過(guò)委托與事件,所以先打算把委托與事件補(bǔ)上再繼續(xù)泛型與委托...
    一個(gè)有味道的名字閱讀 1,557評(píng)論 1 5
  • 網(wǎng)上講C#委托和事件的博文已經(jīng)非常多了,其中也不乏一些深入淺出、條理清晰的文章。我之所以還是繼續(xù)寫(xiě),主要是借機(jī)整理...
    丑小丫大笨蛋閱讀 1,191評(píng)論 0 5
  • 一、理解事件事件采用發(fā)布/訂閱模型,其中發(fā)行者決定在什么情況下引發(fā)事件,而訂戶決定為響應(yīng)事件而執(zhí)行的操作。事件可以...
    CarlDonitz閱讀 347評(píng)論 0 0
  • 事件 事件含義 事件由對(duì)象引發(fā),通過(guò)我們提供的代碼來(lái)處理。一個(gè)事件我們必須訂閱(Subscribe)他們,訂閱一個(gè)...
    天堂邁舞閱讀 3,148評(píng)論 1 7
  • 虧啦餓了
    抉擇_2562閱讀 200評(píng)論 0 0

友情鏈接更多精彩內(nèi)容