C#基本功之委托和事件


定義:委托是一種引用類型,表示對具有特定參數列表和返回類型的方法的引用。 在實例化委托時,你可以將其實例與任何具有兼容簽名和返回類型的方法相關聯
目的:方法聲明和方法實現的分離,使得程序更容易擴展

一、對委托的理解

1. 為什么將方法作為另一個方法的參數

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

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

這是隨便寫的一個方法,沒有實際意義,但是,根據我們已掌握的關於類型的基礎知識,應該明白,這里的obj(引用類型)作為形參,存放的是對象的引用,既然獲取到了對象的引用,那么我們可以在run方法內部對obj的成員進行訪問(一段廢話)。好了,現在我要問一個問題:為什么要將obj作為參數?

問題先慢慢想着,我們再次看一下委托的定義,"是引用類型,對方法的引用",看下面的代碼

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

delegate 作為一種引用類型,引用的是個方法,我們能對方法做什么,只能執行方法。
下面我們回答剛才的問題,obj作為參數(類型聲明),將類型的聲明和類型的實例分離。當然這樣做的目的是為了封裝變化,所有類型都可以作為實參來使用Method1,因為Object是基類,當然也可以將Object換成其他接口類型,只要實現了該接口的類型都可以作為Method1的實參。
雖然Method1和Method2參數類型不一樣,但是目的是一致的,委托是將方法的聲明和實現分離。
下面看一個網上使用廣泛的例子

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

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

2. 委托是一種引用類型

既然委托是一種類型,應該包含類型的成員,我們將代碼編譯完成后,借助反編譯工具看下,編譯后的樣子:
委托的成員
下面這兩種方式是等同的:

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

而BeginInvoke和EndInvoke是屬於異步調用的范疇,我們稍后再說。

3.Lambda表達式比匿名方法簡約

從示例1中可以看到分別顯示調用了靜態方法和實例方法初始化了委托,但是對於那些只使用一次的方法,就沒有必要創建具名方法了。C#2.0提出了使用匿名方法代替具名方法的解決方案

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

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

    //去掉delegate關鍵字並添加 =>運算符
  GreetingDelegate MakeGreeting = (string name) =>
   {
        Console.WriteLine("早上好, " + name);
   };
  GreetPeople("韓梅梅", MakeGreeting);   
  //根據委托的參數類型推斷,string類型也去掉了
  GreetingDelegate MakeGreeting = name =>
  {
      Console.WriteLine("早上好, " + name);
  };
  GreetPeople("韓梅梅", MakeGreeting);         
4. 委托也是類型安全的
  GreetingDelegate MakeGreeting1 = (int name) =>
  {
      Console.WriteLine("早上好, " + name);
  };

簽名不同報錯

5. 泛型委托的協變和逆變

常用泛型委托

  • Action<in T,....>:有參數無返回值
  • Func<in T1,...,out TResult>:有參數有返回值,最后一個類型參數為返回值類型

關於泛型委托的協變和逆變可以閱讀這篇文章《C#基本功之泛型》
有了常用泛型委托,我們就不需要自己定義GreetingDelegate委托類型了,對委托的使用進一步的簡化了。

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

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

 //先不用管EventArgs參數,object 類型的sender,可以理解為任何類型都可以傳遞
 public delegate void EventHandler(object sender, EventArgs e);
 /// <summary>
 /// 控制中心
 /// </summary>
 public class ControlCore
  {
         public DateTime Time { get; set; } = DateTime.Now;
        /// <summary>
        /// 執行任務
        /// </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("起床了,親");
 }

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

        //將委托的訪問級別改為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);
        }
       //因為委托定義為private,所以需要提供調用委托的接口
        public void OnTask(EventArgs e)
        {
            //7點了
            if (Time.Hour == 7)
            {
                if (this.Task!= null)
                {
                    this.Task(this, e);
                }
            }
        }


如果對委托的使用僅僅是添加或移除方法,然后執行委托的調用列表,我相信用事件會更簡單容易;
事件是以委托為基礎,可以理解為對委托的進一步封裝。

二、對事件的理解

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

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

重新生成

事件編譯后生成的兩個方法,與我們的示例中AddClick和RemoveClick方法類似;同時可以看到EventHandler委托,已經字段變為private的訪問級別了(小鎖表示私有);這樣一來,事件幫我們完成了對委托的“限制“;
在客戶端訪問事件也只能+=(訂閱)或者 -=(取消訂閱)了,如果直接用“=“運算符賦值就會報錯(在Control類內容還是可以的);
報錯了

2. 事件使用 發布-訂閱(publisher-subscriber) 模型

發布者:包含事件的類用於觸發事件,而這個類稱為事件的“發布者”。
通過聲明委托類型的事件,將委托與事件關聯。發布者對象調用這個事件,並通知訂閱者對象
訂閱者:其他注冊該事件的類稱為“訂閱者”。
訂閱者注冊事件並提供事件處理程序(鬧鍾、打開窗簾、熱水器燒水等),在發布者類中通過委托調用訂閱者的事件處理程序。

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

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


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

客戶端代碼

        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();
            }
        }

執行結果

7點了,起床了,親
7點了,打開窗簾
7點了,熱水器開始燒水
3. 關於sender和EventArgs

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

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

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

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

總結:一直想寫一篇關於委托和事件的文章,但是網上已經有很多這類優秀的文章了,不乏一些佼佼者,由淺入深,從無到有的風格將知識點講的很透徹。如果我再按照這個類型去寫,實在沒有意思。
所以我想,我們可不可以從已知到未知這條路徑來將知識點講明白,比如,我們知道將具有相同屬性和行為的對象抽象為類型。那么我是不是可以將具有相同簽名和返回類型的方法抽象為委托?再比如,我們知道屬性封裝了字段,並對字段的輸入輸出進行了限制。那么我是不是可以將委托封裝,控制委托的注冊或取消,這樣就引出了事件。
希望可以幫助到朋友們

:.NET關於委托和事件的編碼規范

  • 委托類型的名稱都應該以EventHandler結束。
  • 事件的命名為 委托去掉 EventHandler之后剩余的部分。
  • 委托的原型定義:有一個void返回值,並接受兩個輸入參數:一個Object 類型,一個 EventArgs類型(或繼承自EventArgs)。
  • 繼承自EventArgs的類型應該以EventArgs結尾。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM