天天看點

C#基礎篇 - 了解委托和事件

委托

委托類似于C++中的函數指針(一個指向記憶體位置的指針)。委托是C#中類型安全的,可以訂閱一個或多個具有相同簽名方法的函數指針。簡單了解,委托是一種可以把函數當做參數傳遞的類型。很多情況下,某個函數需要動态地去調用某一類函數,這時候我們就在參數清單放一個委托當做函數的占位符。在某些場景下,使用委托來調用方法能達到減少代碼量,實作某種功能的用途。

自定義委托

聲明和執行一個自定義委托,大緻可以通過如下步驟完成:

  1. 利用關鍵字delegate聲明一個委托類型,它必須具有和你想要傳遞的方法具有相同的參數和傳回值類型;
  2. 建立委托對象,并且将你想要傳遞的方法作為參數傳遞給委托對象; 
  3. 通過上面建立的委托對象來實作該委托綁定方法的調用。 

下面一段代碼,完成了一次應用委托的示範:

//step01:使用delegate關鍵字聲明委托
    public delegate int CalcSumDelegate(int a, int b);

class Program
    {
        static void Main(string[] args)
        {
            //step03:執行個體化這個委托,并引用方法
            CalcSumDelegate del = new CalcSumDelegate(CalcSum);
            //step04:調用委托
            int result = del(5, 5);
            Console.WriteLine("5+5=" + result);
        }
//step02:聲明一個方法和委托類型對應
        public static int CalcSum(int a, int b)
        {
            return a + b;
        }
    }      

通過上面4個步驟,完成了委托從聲明到調用的過程。接着,咱也學着大神用ILSpy反編譯上面的代碼生成的程式集。截圖如下:

C#基礎篇 - 了解委托和事件
  1. 自定義委托繼承關系是:System.MulticastDelegate —> System.Delegate —>System.Object。
  2. 委托類型自動生成3個方法:BeginInvoke、EndInvoke、Invoke。查資料得知,委托正是通過這3個方法在内部實作調用的。Invoke 方法,允許委托同步調用。上面調用委托的代碼del(5, 5)執行時,編譯器會自動調用 del.Invoke(5,5);BeginInvoke 方法,允許委托的異步調用。假如上述委托以異步的方式執行,則要顯示調用dal.BeginInvoke(5,5)。
 注意:BeginInvoke 和 EndInvoke 是.Net中使用異步方式調用同步方法的兩個重要方法,具體用法詳見微軟 官方示例

多點傳播委托

一個委托可以引用多個方法,包含多個方法的委托就叫多點傳播委托。下面通過一個示例來了解什麼是多點傳播委托:

//step01:聲明委托類型
    public delegate void PrintDelegate();
    public class Program
    {
        public static void Main(string[] args)
        {
            //step03:執行個體化委托,并綁定第1個方法
            PrintDelegate del = Func1;
            //綁定第2個方法
            del += Func2;
            //綁定第3個方法
            del += Func3;
            //step04:調用委托
            del();
//控制台輸出結果:
            //調用第1個方法!
            //調用第2個方法! 
            //調用第3個方法!
        }
        //step02:聲明和委托對應簽名的3個方法
        public static void Func1()
        {
            Console.WriteLine("調用第1個方法!");
        }
        public static void Func2()
        {
            Console.WriteLine("調用第2個方法!");
        }
        public static void Func3()
        {
            Console.WriteLine("調用第3個方法!");
        }
    }      

可以看出,多點傳播委托的聲明過程是和自定義委托一樣的,可以了解為,多點傳播委托就是自定義委托在執行個體化時通過 “+=” 符号多綁定了兩個方法。

Q:為什麼能給委托綁定多個方法呢?

自定義委托的基類就是多點傳播委托MulticastDelegate ,這就要看看微軟是如何對System.MulticastDelegate定義的:

MulticastDelegate擁有一個帶有連結的委托清單,該清單稱為調用清單,它包含一個或多個元素。在調用多路廣播委托時,将按照調用清單中的委托出現的順序來同步調用這些委托。如果在該清單的執行過程中發生錯誤,則會引發異常。(--摘自MSDN)

Q:為什麼使用“+=”号就能實作綁定呢?

先來看上述程式集反編譯後的調用委托的代碼:

C#基礎篇 - 了解委托和事件

“+=”的本質是調用了Delegate.Combine方法,該方法将兩個委托連接配接在一起,并傳回合并後的委托對象。

Q:多點傳播委托能引用多個具有傳回值的方法嗎?

答案是,當然能。委托的方法可以是無傳回值的,也可以是有傳回值的。不過,對于有傳回值的方法需要我們從委托清單上手動調用。否則,就隻能得到委托調用的最後一個方法的結果。下面通過兩段代碼驗證下:

C#基礎篇 - 了解委托和事件
C#基礎篇 - 了解委托和事件
public delegate string GetStrDelegate();
    public class Program
    {
        public static void Main(string[] args)
        {
            GetStrDelegate del = Func1;
            del += Func2;
            del += Func3;
            string result = del();
            Console.WriteLine(result);
            
            //控制台輸出結果:
        //You called me from Func3
        }
        public static string Func1()
        {
            return "You called me from Func1!";
        }
        public static string Func2()
        {
            return "You called me from Func2!";
        }
        public static string Func3()
        {
            return "You called me from Func3!";
        }
    }      

直接執行

正确做法:利用GetInvocationList獲得委托清單上所有方法,循環依次執行委托,并處理委托傳回值。 

C#基礎篇 - 了解委托和事件
C#基礎篇 - 了解委托和事件
public delegate string GetStrDelegate();
    public class Program
    {
        public static void Main(string[] args)
        {
            GetStrDelegate del = Func1;
            del += Func2;
            del += Func3;
            //擷取委托鍊上所有方法
            Delegate[] delList = del.GetInvocationList();
            //周遊,分别處理每個方法的傳回值
            foreach (GetStrDelegate item in delList)
            {
                //執行目前委托
                string result = item();
                Console.WriteLine(result);
                //控制台輸出結果:
                //You called me from Func1
                //You called me from Func2
                //You called me from Func3
            }
            Console.ReadKey();
        }
        public static string Func1()
        {
            return "You called me from Func1";
        }
        public static string Func2()
        {
            return "You called me from Func2";
        }
        public static string Func3()
        {
            return "You called me from Func3";
        }
    }      

周遊執行

匿名方法

匿名方法是C#2.0版本引入的一個新特性,用來簡化委托的聲明。假如委托引用的方法隻使用一次,那麼就沒有必要聲明這個方法,這時用匿名方法表示即可。

//step01:定義委托類型
    public delegate string ProStrDelegate(string str);
    public class Program
    {
        public static void Main(string[] args)
        {
            //step02:将匿名方法指定給委托對象
            ProStrDelegate del = delegate(string str) { return str.ToUpper(); };
            string result = del("KaSlFkaDhkjHe");
            Console.WriteLine(result);
            Console.ReadKey();
            //輸出:KASLFKAFHKJHE
        }
    }      

匿名方法隻是C#提供的一個文法糖,友善開發人員使用。在性能上與命名方法幾乎無異。

匿名方法通常在下面情況下使用:

  1. 委托需要指定一個臨時方法,該方法使用次數極少;
  2. 這個方法的代碼很短,甚至可能比方法聲明都短的情況下使用。

Lambda表達式

Lambda表達式是C#3.0版本引入的一個新特性,它提供了完成和匿名方法相同目标的更加簡潔的格式。下面示例用Lambda表達式簡化上述匿名方法的例子:

public delegate string ProStrDelegate(string str);
    public class Program
    {
        public static void Main(string[] args)
        {
            //匿名委托
            //ProStrDelegate del = delegate(string str) { return str.ToUpper(); };
            //簡化1
            //ProStrDelegate del1 = (string str) =>{ return str.ToUpper(); };
            //簡化2
            //ProStrDelegate del2 = (str) =>{ return str.ToUpper(); };
            //簡化3
            ProStrDelegate del3 = str => str.ToUpper();
            string result = del3("KaSlFkaDhkjHe");
            Console.WriteLine(result);
            Console.ReadKey();
            //輸出:KASLFKAFHKJHE
        }
    }      
  • 簡化1:去掉delegate關鍵字,用"=>"符号表示參數清單和方法體之間的關系;
  • 簡化2:去掉方法的參數類型;假如隻有一個參數,參數清單小括号()也可省略;
  • 簡化3:如果方法體中的代碼塊隻有一行,可以去掉 return,去掉方法體的大括号{}。

内置委托

上述幾種委托的使用,都沒能離開定義委托類型這一步驟。微軟幹脆直接把定義委托這一步驟封裝好,形成三個泛型類:Action<T>、Func<T>和Predicate<T>,這樣就省去了定義的步驟,推薦使用。

public class Program
    {
        public static void Main(string[] args)
        {
            //Action
            Action<string> action = delegate(string str) { Console.WriteLine("你好!" + str); };
            action("GG");

//Func
            Func<int, int, int> func = delegate(int x, int y) { return x + y; };
            Console.WriteLine("計算結果:" + func(5, 6));

//Predicate
            Predicate<bool> per = delegate(bool isTrue) { return isTrue == true; };
            Console.WriteLine(per(true));
        }
    }      

它們的差別如下:

  1. Action<T>委托:允許封裝的方法有多個參數,不能有傳回值;
  2. Func<T>委托:允許封裝的方法有多個參數,必須有傳回值;
  3. Predicate<T>委托:允許封裝的方法有一個參數,傳回值必須為bool類型。

事件

委托是一種類型,事件依賴于委托,故事件可以了解為是委托的一種特殊執行個體。它和普通的委托執行個體有什麼差別呢?委托可以在任意位置定義和調用,但是事件隻能定義在類的内部,隻允許在目前類中調用。是以說,事件是一種類型安全的委托。

定義事件

通過一個簡單的場景來示範下事件的使用:

/// <summary>
    /// 音樂播放器
    /// </summary>
    public class MusicPlayer
    {
        //step01:定義 音樂播放結束 事件
        public event EventHandler<EventArgs> PlayOverEvent;
        public string Name { get; set; }
        public MusicPlayer(string name)
        {
            this.Name = name;
        }
        //step02:定義一個觸發事件的方法
        public void PlaySong()
        {
            //模拟播放
            Console.WriteLine("正在播放歌曲:" + this.Name);
            for (int i = 0; i < 20; i++)
            {
                Console.Write(".");
                Thread.Sleep(100);
            }
            //播放結束,則觸發PlayOverEvent事件
            if (PlayOverEvent != null)
            {
                PlayOverEvent(this, null);
            }
        }
    }
    public class Program
    {
        static void Main(string[] args)
        {
            //建立音樂播放器對象
            MusicPlayer player = new MusicPlayer("自由飛翔");
            //step03:注冊事件
            player.PlayOverEvent += player_PlayOverEvent;
            //播放歌曲,結束後觸發事件
            player.PlaySong();
            Console.ReadKey();
        }
        static void player_PlayOverEvent(object sender,EventArgs e)
        {
            MusicPlayer player = sender as MusicPlayer;
            Console.WriteLine("\r\n{0}播完了!", player.Name);
        }
    }      

程式運作結果:

C#基礎篇 - 了解委托和事件

總結上面事件使用的幾個步驟:

  1. 用event關鍵字定義事件,事件必須要依賴一個委托類型;
  2. 在類内部定義觸發事件的方法;
  3. 在類外部注冊事件并引發事件。

public event EventHandler<EventArgs> PlayOverEvent

這句代碼在MusicPlayer類定義了一個事件成員PlayOverEvent,我們說事件依賴于委托、是委托的特殊執行個體,是以EventHandler<EventArgs>肯定是一個委托類型。下面我們來驗證一下:

C#基礎篇 - 了解委托和事件

EventHandler是微軟封裝好的事件委托,該委托沒有傳回值類型,兩個參數:sender事件源一般指的是事件所在類的執行個體;TEventArgs事件參數,如果有需要建立,要顯示繼承System.EventArgs。

事件的本質

  MusicPlayer player = new MusicPlayer("自由飛翔");
  //注冊事件
  player.PlayOverEvent += player_PlayOverEvent;
  player.PlaySong();      

從上面代碼我們觀察到,事件要通過"+="符号來注冊。我們猜想,事件是不是像多點傳播委托一樣通過Delegate.Combine方法可以綁定多個方法?還是通過反編譯工具檢視下。

C#基礎篇 - 了解委托和事件

我們看到PlayOverEvent事件内部生成了兩個方法:add_ PlayOverEvent和remove_ PlayOverEvent。add方法内部調用Delegate.Combine把事件處理方法綁定到委托清單;remove方法内部調用Delegate.Remove從委托清單上移除指定方法。其實,事件本質上就是一個多點傳播委托。

參考文章

[1] Edison Chou,

http://www.cnblogs.com/edisonchou/p/4827578.html

[2] jackson0714,

http://www.cnblogs.com/jackson0714/p/5111347.html

[3] Sam Xiao, 

http://www.cnblogs.com/xcj26/p/3536082.html