C#基礎增強(8)之委託和事件

委託

簡介

委託是一種能夠聲明出指向方法的變量的數據類型面試

聲明委託的方式

格式: delegate <返回值類型> 委託類型名(參數) ,例如:數組

delegate void MyDel(string str) 。
// 注意:這裏除了前面的 delegate 關鍵字,剩下部分和聲明一個函數相同,可是 MyDel 不是函數名,而是委託類型名。

建立委託類型變量

聲明委託變量的方式與聲明變量相同,都是經過 new 關鍵字,例:ide

MyDel sayHello = new MyDel(SayHello);
/*
 * SayHello 是一個方法句柄,而且它的返回值須要與 MyDel 的參數返回值相同;
 * sayHello 這個委託變量就指向 SayHello 方法
 */

還有一種簡化的寫法:函數

MyDel sayHello = SayHello;
/*
 * 反編譯查看以下:
 *     MyDel sayHello = new MyDel(SayHello);
 * 即其實與原始寫法相同
 */

委託的使用

要使用委託能夠直接使用 <委託變量名>() 的方式調用委託指向的方法,若是有參數就傳遞參數,例:學習

using System;
using NUnit.Framework;

namespace MyTests
{
    [TestFixture]
    public class Tests
    {
        delegate void MyDel(string str);

        void SayHello(string name)
        {
            Console.WriteLine("hello " + name);
        }

        [Test]
        public void Test()
        {
            MyDel sayHello = SayHello;
            sayHello("張三");
            /*
             * hello 張三
             */
        }
    }
}
MyTests.Tests

委託變量之間能夠互相賦值,其實就是一個傳遞方法指針的過程,如:this

using System;
using NUnit.Framework;

namespace MyTests
{
    [TestFixture]
    public class Tests
    {
        delegate void MyDel(string str);

        void SayHello(string name)
        {
            Console.WriteLine("hello " + name);
        }

        void SayName(string name)
        {
            Console.WriteLine(name);
        }

        [Test]
        public void Test()
        {
            MyDel sayHello = SayHello;
            sayHello = SayName; // sayHello 原本指向 SayHello 方法,這一行讓其指向了 SayName 方法
            sayHello("張三"); // 因此實際執行的是 SayName 方法
            /*
             * 張三
             */
        }
    }
}
MyTests.Tests

案例一:獲取最大值

先從一個簡單的需求開始,若是咱們須要編寫一個獲取 int 數組中最大值的方法,很簡單以下:spa

1 int GetMaxNum(int[] nums)
2 {
3     int max = nums[0];
4     for (var i = 1; i < nums.Length; i++)
5     {
6         if (nums[0] > max) max = nums[0];
7     }
8     return max;
9 }

假如又有一個要求,咱們定義一個獲取 string 數組中最大值(每一個 string 變量均可轉型爲 int 變量)的方法,顯示上述方法就不適用了。那有沒有什麼方法可以讓其通用起來呢?3d

若是咱們要獲取 string 數組中的最大值,顯然咱們須要先將其中每一個元素轉換到 int 類型,而後再進行比較,即重點就是咱們要如何定義它的比較規則?指針

上述代碼的比較規則是在第 6 行的 if 塊中,咱們要作的就是讓那個這個 if 塊中的內容動態起來,此時委託就派上用場了,看以下代碼:code

/**
 * 獲取數組中的最大值
 */
object GetMax(object[] nums,CompareFunc compareFunc)
{
    object max = nums[0];
    for (var i = 1; i < nums.Length; i++)
    {
        if (compareFunc(nums[i],max))
            max = nums[i];
    }
    return max;
}

/**
 * 若是 obj1 比 obj2 大,則返回 true,不然返回 false
 */
delegate bool CompareFunc(object obj1, object obj2);

上述咱們新定義了一個 GetMax 方法,它的返回值爲 object 類型,第一個參數爲 object 數組類型,第二個參數則是 CompareFunc 委託變量。而 CompareFunc 委託的做用就是對比較規則一個定義,即咱們要作的就是傳入對應數組參數的同時也一塊兒傳入響應的比較規則的實現。

定義比較規則:

/**
 * 比較規則
 */
bool CompareInt(object num1, object num2)
{
    return Convert.ToInt32(num1) > Convert.ToInt32(num2);
}

再看此時咱們如何獲取 int 數組的最大值:

[Test]
public void Test()
{
    object[] numArr = {32, 445, 65, 321, 4};
    var max = GetMax(numArr, CompareInt);
    Console.WriteLine(max);
    /*
     * 445
     */
}

而咱們若是要獲取一個 string 數組中的最大值,不用作修改,直接傳入 string 類型數組便可:

[Test]
public void Test()
{
    object[] numArr = {"32", "445", "65", "321", "4"};
    var max = GetMax(numArr, CompareInt);
    Console.WriteLine(max);
    /*
     * 445
     */
}

此時來了一個新需求,有以下實體類:

namespace MyTests.Entities
{
    public class User
    {
        public User()
        {
        }

        public User(int id, string name, int age)
        {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        private int id;
        private string name;
        private int age;

        public int Id
        {
            get { return id; }
            set { id = value; }
        }

        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        public int Age
        {
            get { return age; }
            set { age = value; }
        }

        public override string ToString()
        {
            return string.Format("Id: {0}, Name: {1}, Age: {2}", id, name, age);
        }
    }
}
MyTests.Entities.User

咱們須要定義一個方法可以返回該實體對象數組中年齡最大的對象,很簡單,咱們只須要單獨爲 User 的實例定義一個它的比較規則便可,以下:

[Test]
public void Test()
{
    object[] userArr =
    {
        new User(1, "張三", 34),
        new User(2, "李四", 23),
        new User(3, "王五", 34)
    };
    var max = GetMax(userArr, CompareUser);
    Console.WriteLine(max);
    /*
     * Id: 1, Name: 張三, Age: 34
     */
}

bool CompareUser(object user1, object user2)
{
    return (user1 as User).Age > (user2 as User).Age;
}

委託最大的價值在於可讓咱們在編寫代碼時不用考慮委託變量指向哪個方法,只須要按照聲明委託時的約定傳入參數便可。其實有點相似於接口的做用,咱們不須要了解它的具體實現就能夠直接使用它。

泛型委託

自定義泛型委託

泛型委託的定義其實與泛型方法的定義類似,格式以下:

delegate <返回值類型> <方法名><泛型名稱1, 泛型名稱2, ...>(參數1, 參數2,...);

經過泛型委託上述案例能夠修改以下:

using System;
using MyTests.Entities;
using NUnit.Framework;

namespace MyTests
{
    [TestFixture]
    public class GetMaxNumTest
    {
        [Test]
        public void Test()
        {
            User[] userArr =
            {
                new User(1, "張三", 34),
                new User(2, "李四", 23),
                new User(3, "王五", 34)
            };
            
            var max = GetMax<User>(userArr, CompareUser);
            Console.WriteLine(max);
            /*
             * Id: 1, Name: 張三, Age: 34
             */
        }

        /**
         * 比較規則
         */
        bool CompareUser(User user1, User user2)
        {
            return user1.Age > user2.Age;
        }

        /**
         * 泛型方法
         */
        T GetMax<T>(T[] nums, CompareFunc<T> compareFunc)
        {
            T max = nums[0];
            for (var i = 1; i < nums.Length; i++)
            {
                if (compareFunc(nums[i], max))
                    max = nums[i];
            }

            return max;
        }

        /**
         * 泛型委託
         */
        delegate bool CompareFunc<T>(T obj1, T obj2);
    }
}
MyTests.GetMaxNumTest

內置的泛型委託

.Net 中內置兩個泛型委託 Func 和 Action ,平常開發中基本不用自定義委託類型了。 Func 是有返回值的委託,而 Action 是沒有返回值的委託。使用以下:

using System;
using MyTests.Entities;
using NUnit.Framework;

namespace MyTests
{
    [TestFixture]
    public class Test1
    {
        void SayHello(string name)
        {
            Console.WriteLine("hello " + name);
        }
        
        bool CompareUser(User user1, User user2)
        {
            return user1.Age > user2.Age;
        }

        [Test]
        public void Test()
        {
            // 無返回值的委託
            Action<string> sayHello = SayHello;
            sayHello("張三");
            // 有返回值的委託
            Func<User, User, bool> compareUser = CompareUser;
            // 若是是有返回值的委託,那麼最後一個泛型參數爲返回值類型
            var isGt = compareUser(new User(1, "張三", 32), new User(2, "李四", 43));
            Console.WriteLine(isGt);
            /*
             hello 張三
             False
             */
        }
    }
}
MyTests.Test

而上述的案例也能夠修改成以下:

using System;
using MyTests.Entities;
using NUnit.Framework;

namespace MyTests
{
    [TestFixture]
    public class GetMaxNumTest2
    {
        [Test]
        public void Test()
        {
            User[] userArr =
            {
                new User(1, "張三", 34),
                new User(2, "李四", 23),
                new User(3, "王五", 34)
            };

            var max = GetMax<User>(userArr, CompareUser);
            Console.WriteLine(max);
            /*
             * Id: 1, Name: 張三, Age: 34
             */
        }

        /**
         * 比較規則
         */
        bool CompareUser(User user1, User user2)
        {
            return user1.Age > user2.Age;
        }

        /**
         * 泛型方法 使用內置泛型委託
         */
        T GetMax<T>(T[] nums, Func<T, T, bool> compareFunc)
        {
            T max = nums[0];
            for (var i = 1; i < nums.Length; i++)
            {
                if (compareFunc(nums[i], max))
                    max = nums[i];
            }

            return max;
        }
    }
}
MyTests.GetMaxNumTest

匿名方法

匿名方法,就是沒有名字的方法。使用委託的不少時候不必定義一個普通方法,由於這個方法只有這個委託會用,而且只用一次,這個時候使用匿名方法最爲合適。以將 SayHello 方法的指針賦給委託爲例:

using System;
using NUnit.Framework;

namespace MyTests
{
    [TestFixture]
    public class Tests
    {
        #region 普通方法方式

        void SayHello(string name)
        {
            Console.WriteLine("hello " + name);
        }

        public void TestOld()
        {
            Action<string> sayHello = SayHello;
        }

        #endregion

        #region 匿名方法方式

        [Test]
        public void TestNew()
        {
            Action<string> sayHello = delegate(string name) { Console.WriteLine("hello " + name); };
        }

        #endregion
    }
}

將最大值哪一個案例使用匿名方法重構後以下:

using System;
using MyTests.Entities;
using NUnit.Framework;

namespace MyTests
{
    [TestFixture]
    public class GetMaxNumTest2
    {
        [Test]
        public void Test()
        {
            User[] userArr =
            {
                new User(1, "張三", 34),
                new User(2, "李四", 23),
                new User(3, "王五", 34)
            };
            
            // 使用匿名方法
            var max = GetMax<User>(userArr, delegate(User user1, User user2) { return user1.Age > user2.Age; });
            Console.WriteLine(max);
            /*
             * Id: 1, Name: 張三, Age: 34
             */
        }

        /**
         * 泛型方法 使用內置泛型委託
         */
        T GetMax<T>(T[] nums, Func<T, T, bool> compareFunc)
        {
            T max = nums[0];
            for (var i = 1; i < nums.Length; i++)
            {
                if (compareFunc(nums[i], max))
                    max = nums[i];
            }

            return max;
        }
    }
}
MyTests.GetMaxNumTest

lambda表達式

使用

lambda 表達式實際上是對匿名方法使用的一個再度簡化,看以下示例:

Action<string> a1 = delegate(string s) { Console.WriteLine(s); };

上面是將一個匿名方法指針賦值給一個委託變量,經過 lambda 表達式可簡化以下:

Action<string> a2 = (string s) => { Console.WriteLine(s); };

還能夠省略參數類型,編譯器會自動根據委託類型推斷:

Action<string> a3 = (s) => { Console.WriteLine(s); };

若是隻有一個參數,還能夠省略小括號:

Action<string> a3 =  s => { Console.WriteLine(s); };

若是委託有返回值,而且方法體只有一行代碼,這一行代碼仍是返回值,那麼就能夠連方法的大括號和 return 都省略:

Func<int,int,int> a4 = (i, j) => i + j;

 => 可讀做「goes to」。

練習

一、將下面代碼儘量簡化:

Action<string, bool > a1 = delegate(string s, bool b)
{
    if (b) { Console.WriteLine("true" + s); }
    else { Console.WriteLine("false" + s); }
};
Action<string, bool> a1 = (s, b) =>
{
    if (b) Console.WriteLine("true" + s);
    else Console.WriteLine("false" + s);
};
result
Func<string, int> f1 = delegate(string str) { return Convert.ToInt32(str);};
Func<string, int> f1 = str => Convert.ToInt32(str);
result

二、把下面的代碼還原成匿名方法形式:

Action<string, int> a1 = (s, i) => { Console.WriteLine("s=" + s + ",i=" + i); };
Action<string, int> a1 = delegate(string s, int i) { 
    Console.WriteLine("s=" + s + ",i=" + i); 
 };
result
Func<int, string> f2 = n => (n + 1).ToString();
Func<int, string> f2 = delegate(int n) {
    return (n + 1).ToString();
 };
result
Func<int, int> f3 = n => n * 2;
Func<int, int> f3 = delegate(int n) { return n * 2; };
result

三、寫出下面一個 lambda 表達式的委託類型及非匿名函數形式:

n => n > 0;
委託類型爲 Func<int, bool>
非匿名函數形式:
public bool IsGtZero(int n)
{
    return n > 0;
}
result

四、使用 lambda 表達式修改獲取最大值案例:

using System;
using MyTests.Entities;
using NUnit.Framework;

namespace MyTests
{
    [TestFixture]
    public class GetMaxNumTest2
    {
        [Test]
        public void Test()
        {
            User[] userArr =
            {
                new User(1, "張三", 34),
                new User(2, "李四", 23),
                new User(3, "王五", 34)
            };

            // 使用匿名方法
            var max = GetMax<User>(userArr, (User user1, User user2) => user1.Age > user2.Age);
            Console.WriteLine(max);
            /*
             * Id: 1, Name: 張三, Age: 34
             */
        }

        /**
         * 泛型方法 使用內置泛型委託
         */
        T GetMax<T>(T[] nums, Func<T, T, bool> compareFunc)
        {
            T max = nums[0];
            for (var i = 1; i < nums.Length; i++)
            {
                if (compareFunc(nums[i], max))
                    max = nums[i];
            }
            return max;
        }
    }
}
result

案例二:擴展集合的Where方法

經過上面學習到的內容,咱們能夠爲集合作一個擴展方法,這個方法的功能是能夠根據咱們傳入的 lambda 表達式過濾出咱們須要的元素集合。

一、編寫擴展方法:

using System;
using System.Collections.Generic;

namespace MyTests.Ext
{
    /**
     * 集合擴展方法類
     */
    public static class EnumerableExt
    {
        public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> data, Func<T, bool> filter)
        {
            var list = new List<T>();
            foreach (var obj in data)
            {
                if (filter(obj))
                {
                    list.Add(obj);
                }
            }

            return list;
        }
    }
}
MyTests.Ext.EnumerableExt

二、使用:

User[] userArr =
{
    new User(1, "張三", 34),
    new User(2, "李四", 23),
    new User(3, "王五", 34)
};
// 獲取 name 中包含 "張" 的元素集合
var list = userArr.MyWhere(p => p.Name.Contains(""));
foreach (var user in list)
    Console.WriteLine(user);
/*
 Id: 1, Name: 張三, Age: 34
 */
test

委託的組合

委託是可使用「+」號來進行組合的,組合後會生成一個新的委託對象,調用這個新的委託對象時,會按順序將組合進來的委託依次執行。看以下示例:

using System;
using NUnit.Framework;

namespace 委託的組合
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void Test1()
        {
            Action<string> a1 = SayHello1;
            Action<string> a2 = SayHello2;
            Action<string> a3 = SayHello3;
            // 組合
            Action<string> a4 = a1 + a2 + a3;
            
            a4("張三");
            /*
            hello 張三 from SayHello1
            hello 張三 from SayHello2
            hello 張三 from SayHello3
             */
        }

        public void SayHello1(string name)
        {
            Console.WriteLine("hello " + name + " from SayHello1");
        }

        public void SayHello2(string name)
        {
            Console.WriteLine("hello " + name + " from SayHello2");
        }

        public void SayHello3(string name)
        {
            Console.WriteLine("hello " + name + " from SayHello3");
        }
    }
}
委託的組合.Tests

還能夠經過「-」號從委託對象中將已組合進來的委託對象移除,以下:

using System;
using NUnit.Framework;

namespace 委託的組合
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void Test1()
        {
            Action<string> a1 = SayHello1;
            Action<string> a2 = SayHello2;
            Action<string> a3 = SayHello3;
            // 組合
            Action<string> a4 = a1 + a2 + a3;

            // 移除 a2 委託對象
            a4 = a4 - a2;
            
            a4("張三");
            /*
            hello 張三 from SayHello1
            hello 張三 from SayHello3
             */
        }

        public void SayHello1(string name)
        {
            Console.WriteLine("hello " + name + " from SayHello1");
        }

        public void SayHello2(string name)
        {
            Console.WriteLine("hello " + name + " from SayHello2");
        }

        public void SayHello3(string name)
        {
            Console.WriteLine("hello " + name + " from SayHello3");
        }
    }
}
委託的組合.Tests

委託若是有返回值則有一些特殊,不過委託的組合通常是給事件使用,普通狀況不多使用,這裏就再也不深究。

事件

定義

給委託添加上 event 關鍵字它就是一個事件,格式以下:

[訪問修飾符] event <委託類型> <事件名稱>;

案例三:本命年事件

一、修改 User 實體類,定義一個事件,讓其在本命年時觸發:

using System;

namespace MyTests.Entities
{
    public class User
    {
        public User()
        {
        }

        public User(int id, string name, int age)
        {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        private int id;
        private string name;
        private int age;

        public int Id
        {
            get { return id; }
            set { id = value; }
        }

        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        public int Age
        {
            get { return age; }
            set
            {
                if (age % 12 == 0) OnBirthYear(this.name);
                age = value;
            }
        }

        public override string ToString()
        {
            return string.Format("Id: {0}, Name: {1}, Age: {2}", id, name, age);
        }

        public event Action<string> OnBirthYear; // 定義一個本命年觸發的事件
    }
}
MyTests.Entities.User

二、使用:

var user1 = new User();
var user2 = new User();
var user3 = new User();
// 給每個 User 對象經過 += 註冊事件
user1.OnBirthYear += (name, age) => { Console.WriteLine(name + age + "歲了,本命年到了,喝稀飯"); };
user2.OnBirthYear += (name, age) => { Console.WriteLine(name + age + "歲了,本命年到了,吃饅頭"); };
user3.OnBirthYear += (name, age) => { Console.WriteLine(name + age + "歲了,本命年到了,大魚大肉"); };

user1.Id = 1;
user1.Name = "張三";
user1.Age = 23;            

user2.Id = 2;
user2.Name = "李四";
user2.Age = 24;

user3.Id = 3;
user3.Name = "王五";
user3.Age = 36;
/*
 
李四24歲了,本命年到了,吃饅頭
王五36歲了,本命年到了,大魚大肉
 */

註冊事件觸發的方法時方法的參數及返回值要與事件的委託一致。

事件原理

事件的註冊和移除其實是經過事件的 add 和 remove 方法完成,在這兩個方法中維護了同一個委託對象,且事件不可爲 null。上述實體類也可修改以下:

using System;

namespace MyTests.Entities
{
    public class User
    {
        public User()
        {
        }

        public User(int id, string name, int age)
        {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        private int id;
        private string name;
        private int age;

        public int Id
        {
            get { return id; }
            set { id = value; }
        }

        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        public int Age
        {
            get { return age; }
            set
            {
                age = value;
                if (age % 12 == 0)
                {
                    if (_OnBirthYear != null)
                    {
                        _OnBirthYear(this.name,this.age);
                    }
                }
            }
        }

        public override string ToString()
        {
            return string.Format("Id: {0}, Name: {1}, Age: {2}", id, name, age);
        }

        private Action<string, int> _OnBirthYear; // 定義一個本命年觸發的事件

        public event Action<string, int> OnBirthYear
        {
            add { _OnBirthYear += value; }
            remove { _OnBirthYear -= value; }
        }
    }
}
MyTests.Entities.User

委託與事件總結

委託的做用

佔位,在不知道未來要執行方法的具體邏輯時,能夠先用一個委託變量來代替方法調用(委託的返回值,參數列表要肯定)。在實際調用以前,須要爲委託賦值。

事件的做用

事件的做用域委託變量同樣,只是在功能上相對委託變量有更多的限制,好比:

  • 只能經過 "+=" 或 "-=" 來綁定方法(事件處理程序)。
  • 只能在類的內部調用(觸發)事件。

事件和委託的關係

反編譯會發現,事件是由一個私有的委託變量、 add_* 和 remove_* 方法組成,而事件的非簡化寫法就是聲明一個私有的委託變量和  add 、 remove 方法。

相關面試題

一、說一下事件和委託的關係?

網上有不少答案說事件就是委託,這個確定是錯誤的。只能說事件的實現依賴於委託,由於事件是由一個私有的委託變量、 add_* 和 remove_* 方法組成。

二、接口中能夠定義事件嗎?那索引器和屬性呢?

首先,接口中只能夠定義方法的簽名,事件、索引器、屬性本質上都是方法,因此是能夠定義的。

看以下示例:

using System;
using System.Collections.Generic;

namespace MyTests
{
    public interface Interface1
    {
        // 屬性
        List<int> list { get; set; }
        // 索引器
        long this[int index] { get; set; }
        // 事件
        event Action<string, int> OnEvent;
    }
}
MyTests.Interface1

對應反編譯文件爲:

.class public interface abstract auto ansi Interface1
{
    .custom instance void [mscorlib]System.Reflection.DefaultMemberAttribute::.ctor(string) = { string('Item') }
    .event [mscorlib]System.Action`2<string, int32> OnEvent
    {
        .addon instance void MyTests.Interface1::add_OnEvent(class [mscorlib]System.Action`2<string, int32>)
        .removeon instance void MyTests.Interface1::remove_OnEvent(class [mscorlib]System.Action`2<string, int32>)
    }


    .property instance int64 Item
    {
        .get instance int64 MyTests.Interface1::get_Item(int32)
        .set instance void MyTests.Interface1::set_Item(int32, int64)
    }

    .property instance class [mscorlib]System.Collections.Generic.List`1<int32> list
    {
        .get instance class [mscorlib]System.Collections.Generic.List`1<int32> MyTests.Interface1::get_list()
        .set instance void MyTests.Interface1::set_list(class [mscorlib]System.Collections.Generic.List`1<int32>)
    }

}

 

 
MyTests.Interface1 反編譯 IL 內容

能夠看到接口中定義事件其實是聲明瞭 add_* 和 remove_* 方法的簽名,定義索引器其實是聲明瞭 set_Item 和 get_item 方法的簽名,而定義屬性其實是聲明瞭 set_* 和 get_* 方法的簽名。

相關文章
相關標籤/搜索