侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

C#基础加强(9)之委托和事件

zze
zze
2019-08-11 / 0 评论 / 0 点赞 / 435 阅读 / 24386 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

委托

简介

委托是一种可以声明出指向方法的变量的数据类型。

声明委托的方式

格式: delegate <返回值类型> 委托类型名(参数) ,例如:

delegate void MyDel(string str) 
// 注意:这里除了前面的 delegate 关键字,剩下部分和声明一个函数相同,但是 MyDel 不是函数名,而是委托类型名。

创建委托类型变量

声明委托变量的方式与声明变量相同,都是通过 new 关键字,例:

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 张三
             */
        }
    }
}

委托变量之间可以互相赋值,其实就是一个传递方法指针的过程,如:

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 方法
            /*
             * 张三
             */
        }
    }
}

案例一:获取最大值

先从一个简单的需求开始,如果我们需要编写一个获取 int 数组中最大值的方法,很简单如下:

int GetMaxNum(int[] nums)
{
    int max = nums[0];
    for (var i = 1; i < nums.Length; i++)
    {
        if (nums[0] > max) max = nums[0];
    }
    return max;
}

假如又有一个要求,我们定义一个获取 string 数组中最大值(每个 string 变量都可转型为 int 变量)的方法,显示上述方法就不适用了。那有没有什么方法能够让其通用起来呢?

如果我们要获取 string 数组中的最大值,显然我们需要先将其中每个元素转换到 int 类型,然后再进行比较,即重点就是我们要如何定义它的比较规则?

上述代码的比较规则是在第 6 行的 if 块中,我们要做的就是让那个这个 if 块中的内容动态起来,此时委托就派上用场了,看如下代码:

/**
 * 获取数组中的最大值
 */
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);
        }
    }
}

我们需要定义一个方法能够返回该实体对象数组中年龄最大的对象,很简单,我们只需要单独为 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);
    }
}

内置的泛型委托

.Net 中内置两个泛型委托 FuncAction ,日常开发中基本不用自定义委托类型了。 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
             */
        }
    }
}

而上述的案例也可以修改为如下:

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

匿名方法

匿名方法,就是没有名字的方法。使用委托的很多时候没必要定义一个普通方法,因为这个方法只有这个委托会用,并且只用一次,这个时候使用匿名方法最为合适。以将 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;
        }
    }
}

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”。

练习

1、将下面代码尽可能简化:

Action<string, bool > a1 = delegate(string s, bool b)
{
    if (b) { Console.WriteLine("true" + s); }
    else { Console.WriteLine("false" + s); }
};

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

2、把下面的代码还原成匿名方法形式:

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

3、写出下面一个 lambda 表达式的委托类型及非匿名函数形式:

n => n > 0;
// result:
// 委托类型为 Func<int, bool>
// 非匿名函数形式:
public bool IsGtZero(int n)
{
    return n > 0;
}

4、使用 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;
        }
    }
}

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

通过上面学习到的内容,我们可以为集合做一个扩展方法,这个方法的功能是可以根据我们传入的 lambda 表达式过滤出我们需要的元素集合。

1、编写扩展方法:

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

2、使用:

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
 */

委托的组合

委托是可以使用 + 号来进行组合的,组合后会生成一个新的委托对象,调用这个新的委托对象时,会按顺序将组合进来的委托依次执行。看如下示例:

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

还可以通过 - 号从委托对象中将已组合进来的委托对象移除,如下:

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

委托如果有返回值则有一些特殊,不过委托的组合一般是给事件使用,普通情况很少使用,这里就不再深究。

事件

定义

给委托添加上 event 关键字它就是一个事件,格式如下:

[访问修饰符] event <委托类型> <事件名称>;

案例三:本命年事件

1、修改 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; // 定义一个本命年触发的事件
    }
}

2、使用:

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岁了,本命年到了,大鱼大肉
 */

注册事件触发的方法时方法的参数及返回值要与事件的委托一致。

事件原理

事件的注册和移除实际上是通过事件的 addremove 方法完成,在这两个方法中维护了同一个委托对象,且事件不可为 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; }
        }
    }
}

委托与事件总结

委托的作用

占位,在不知道将来要执行方法的具体逻辑时,可以先用一个委托变量来代替方法调用(委托的返回值,参数列表要确定)。在实际调用之前,需要为委托赋值。

事件的作用

事件的作用域委托变量一样,只是在功能上相对委托变量有更多的限制,比如:

  • 只能通过 +=-= 来绑定方法(事件处理程序)。
  • 只能在类的内部调用(触发)事件。

事件和委托的关系

反编译会发现,事件是由一个私有的委托变量、 add_*remove_* 方法组成,而事件的非简化写法就是声明一个私有的委托变量和 addremove 方法。

相关面试题

1、说一下事件和委托的关系?

网上有很多答案说事件就是委托,这个肯定是错误的。只能说事件的实现依赖于委托,因为事件是由一个私有的委托变量、 add_*remove_* 方法组成。

2、接口中可以定义事件吗?那索引器和属性呢?

首先,接口中只可以定义方法的签名,事件、索引器、属性本质上都是方法,所以是可以定义的。

看如下示例:

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

对应反编译文件内容为:

.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>)
    }

}

可以看到接口中定义事件实际上是声明了 add_*remove_* 方法的签名,定义索引器实际上是声明了 set_Itemget_item 方法的签名,而定义属性实际上是声明了 set_*get_* 方法的签名。

0

评论区