[TOC]

.NET入门: 委托和事件,就像函数指针那样

Part 0 还记得函数指针吗?🛴

学过C或C++的同学肯定学过函数指针。
也就是将一个函数装进指针里,这样你就能像一般的变量一样,用函数赋给一个参数,之后交给对应的其他函数。

这里给出一段由🤖GPT给出的示例代码,就是个可以通过给函数指针赋不同的函数,就可以实现不同的运算🧮。

#include <stdio.h>

// 定义两个函数
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

// 定义一个函数指针类型,它可以指向接受两个int参数并返回int的函数
typedef int (*FuncPtr)(int, int);

int main() {
    int num1 = 10, num2 = 5, result;

    // 将函数指针指向add函数
    FuncPtr operation = add;
    result = operation(num1, num2); // 通过函数指针调用add函数
    printf("The result of addition is: %d\n", result);

    // 将函数指针指向subtract函数
    operation = subtract;
    result = operation(num1, num2); // 通过函数指针调用subtract函数
    printf("The result of subtraction is: %d\n", result);

    // 使用函数指针数组
    FuncPtr operations[] = {add, subtract};
    int choice = 1; // 假设用户选择第一个操作,即加法

    // 根据用户的选择调用相应的函数
    if(choice >= 1 && choice <= 2) {
        result = operations[choice - 1](num1, num2);
        printf("The result of operation %d is: %d\n", choice, result);
    }

    return 0;
}z#include <stdio.h>

// 定义两个函数
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

// 定义一个函数指针类型,它可以指向接受两个int参数并返回int的函数
typedef int (*FuncPtr)(int, int);

int main() {
    int num1 = 10, num2 = 5, result;

    // 将函数指针指向add函数
    FuncPtr operation = add;
    result = operation(num1, num2); // 通过函数指针调用add函数
    printf("The result of addition is: %d\n", result);

    // 将函数指针指向subtract函数
    operation = subtract;
    result = operation(num1, num2); // 通过函数指针调用subtract函数
    printf("The result of subtraction is: %d\n", result);

    // 使用函数指针数组
    FuncPtr operations[] = {add, subtract};
    int choice = 1; // 假设用户选择第一个操作,即加法

    // 根据用户的选择调用相应的函数
    if(choice >= 1 && choice <= 2) {
        result = operations[choice - 1](num1, num2);
        printf("The result of operation %d is: %d\n", choice, result);
    }

    return 0;
}

学完这个之后你可能会觉得,这个毫无作用。🎐为什么我不直接声明一个函数,💥还需要把函数装进一个指针里,再调用它?
看完本文的事件,你可能会有所收获。

Part 1 委托、事件无处不在 ⛩

如果你使用winfrom,那你一定经常和事件打交道。所有的事件都由VS给你自动生成,自动绑定了。也许你并不关心实现的具体细节。

我们以按钮点击🕹,输出一个hello world来举例🎛。
我们写一个控制台程序来模拟这一过程。
因为按钮按下,操作系统返回一个消息,然后传给窗体这个过程很复杂,我们简化一下,简化成是对isClick🖱这个值的修改操作。

var button_1 = new Button();

class Button
{
    bool isClick { get; set; } = false;
}

这是我们模拟的一个按钮,当它的被按下,也就是isClick被修改时,我们又应该怎么知道呢?

学过封装的同学会说,简单,修改set函数就行了。

var button_1 = new Button();
button_1.IsClick = true;
class Button
{
    bool isClick;
    public bool IsClick
    {
        get => isClick;
        set{
            isClick = value;
            Console.WriteLine("Hello World");

        }
    }
    public Button()
    {
        isClick = false;
    }
}

没什么问题,但是如果我之后又想输出"see you again sunsun"🌞,该怎么办呢?
我们知道点击事件函数是可以由用户决定,并不是被封装好,不可修改的。

这个时候我们来使用委托。

class Button
{
    public delegate void onClick();
    public onClick Click;
    bool isClick;
    public bool IsClick
    {
        get => isClick;
        set{
            isClick = value;
            if (Click != null) {
                Click();
            }

        }
    }
    public Button()
    {
        isClick = false;
    }
}

delegate 就是委托,需要注意的是委托是一个类型,不是一个变量,更不是一个成员。就像枚举和结构体一样。你必须先声明一种委托类型,然后才能定义这种委托变量或者委托成员。

由于这个Click委托成员里可能什么都没有,所以我们运行的时候必须判断是不是空。

那我们应该如何使用呢?很简单,只需要外部给它传一个委托进去就行。

var button_1 = new Button();
button_1.Click += ()=>{ Console.WriteLine("see you again sunsun"); };
button_1.IsClick = true;

这里再一次使用了拉姆达表达式,写了一个匿名函数🌫,当然你也可以写一个函数,然后放进去。


var button_1 = new Button();
button_1.Click += SayHello;
button_1.IsClick = true;

static void SayHello()
{

    Console.WriteLine("see you again sunsun");

}

能放进委托的前提是 🌠 函数的签名和委托声明的类型一致。 🌠有些同学不知道什么叫签名✍,签名就是一个函数的返回值和函数名以及参数列表。当然,对于委托而言,函数的名字并不是那么重要。

有些细心的同学就发现📜了,作为一个属性,为什么委托不是new,也不是赋值“=”,而是 “+=” ?是不是还能 “-=”
对,委托可以 “-=” 。顾名思义,+= 是增加执行的函数, -= 是减少执行的函数。在C#中声明delegate默认都是多播委托,你可以理解为是一个函数指针列表。运行这个委托,会执行一系列的函数。当然你也可以 = 赋值,能用来把一个委托对象赋给另一个委托对象,或者初始化委托。

🔋ps:你不用担心 -= 的对象函数不在委托里,那并不会引起任何错误,委托只会保留原样。🔋

也就是说我们可以随意修改委托里面要执行多少个函数。甚至可以是多个一样的函数,比如:

var button_1 = new Button();
button_1.Click += SayHello;
button_1.Click += SayHello;
button_1.Click += SayHello;
button_1.IsClick = true;

这样就会输出三行向🌞打招呼。

你现在简单认识了什么是委托,那我们再简单讲一下事件。所谓事件就是委托的封装。我们还是举一个例子🍏:

button_1.Click2 += SayHello;
button_1.KeyClick();
class Button
{
    //……类的其他代码
    public event onClick Click2;
    public void KeyClick()
    {
        Click?.Invoke();
    }
}

🍎我们实现了一种新的触发方式,触发方式和之前差不多,只不过由自动触发改成了手动触发,由委托改成了事件。事件的优点在于,类外只能修改执行的函数有哪些,而不能直接执行这个事件。
事件相比与委托而言,封装性更强,安全性更强。具体的使用细节和差别,我们在下一章再展开说明。

Part 2 委托的使用 🚉

在前面的例子中,我们已经知道了如何声明一个委托类型。

访问修饰符 delegate 返回值 委托类型名(参数列表)

就像声明函数那样,但没有方法体。调用也和函数一样,使用

委托名(参数列表)

即可

之前说到委托是多个相同签名的函数放到一起,一起执行。之前的例子🍐中,这些函数都没有返回值,那么如果这些函数都有返回值,会返回什么?

delegate int numberSum(int a,int b);
numberSum delegate_sum = (int a,int b) => { return a + b; };
delegate_sum += (int a,int b) => { return a - b; };
delegate_sum += (int a,int b) => { return a * b; };
delegate_sum += (int a,int b) => { return a / b; };
var returnNum = delegate_sum(1, 2);

你可以猜一猜✨返回值是什么类型,值又是多少。

答案是1/2=0,你猜对了吗?🥝你需要记住的是 委托的返回值是最后一个加入的函数的返回值

此外这一串函数⛓是按照加入顺序,被顺序执行的。🔗所以如果其中一个函数运行出错,会导致后续函数不会被执行。

numberSum delegate_sum = (int a,int b) => { return a + b; };
delegate_sum += (int a,int b) => { return a - b; };
delegate_sum += (int a,int b) => { throw new NotImplementedException(); return a * b; };
delegate_sum += (int a,int b) => { return a / b; };
var returnNum = 99;
try
{
     returnNum = delegate_sum(1, 2);
}
catch { }
returnNum.Dump();

这里给出一段代码🏗,你可以逐行调试,就能明白委托的调用过程,以及发生错误产生中断后委托会发生什么。

Part 3 委托的三个好兄弟 Action、Func、Predicate🏛

委托最常用的传递委托对象其实不是delegate🏫然后写一遍委托的名字,而是使用Action、func、Predicate, 这三个是泛型的委托,也就是无视掉委托的名字,只关注委托的返回值和参数列表。
接下来我们来介绍这三种不同的方式。

第一个来介绍func,func的使用很简单。

func<参数列表,返回值>

参数列表可以为空,比如 func < int > 代表这个委托类型,返回值类型为int。

func的返回值不能为void,也就是说<>里必须至少有一个类型。

使用泛型委托,我们并不需要再声明一个专门的名字给委托,因为其实使用委托,我们只想知道返回值和参数列表,至于委托名还是函数名那都不是很重要。

这次我们以不爱写作业的🌞sunsun同学来举例子,sunsun面对作业📔有两种情况,一种是有一个人💬叫他写;另外一种是截止时间到了,开始写。

SunLuckSun litteFriend = new SunLuckSun();
litteFriend.TimeUP = true;
SunLuckSun.HomeWork callDoubleSun = (string what) => { return $"{what},我一会再写"; };
litteFriend.call(callDoubleSun,"C井");
class SunLuckSun
{
    public delegate string HomeWork(string what);
    public event HomeWork dohomework;
    public HomeWork dododo= (string what)=>{ return "在写了在写了"; };
    private bool timesUp=false;

    public bool TimeUP
    {
        get { return timesUp; }
        set
        {
            timesUp = value;
            Console.WriteLine(dododo?.Invoke("every"));
        }
    }

    public void call(HomeWork hk,String what) {
        Console.WriteLine(hk(what));

    }
}

这是委托使用最多的两种情况。

这里使用了一个新的东西Invoke,有同学就有疑惑了,委托不是像函数一样,直接给参数就能执行吗?对,☎但是如果这个委托的不包含任何函数,直接调用就会抛出空异常☎。还记得我之前给出的例子吗?是不是需要先判断委托是否为空?如果你使用的是invoke,那么即使不绑定任何函数,也不会扔出空异常。如果你知道 '?' 这个本身就是判空运算符,那么就明白这种写法,是一种双保险机制。

在这个案例当中你会发现名为HOmeWork的委托名字其实并不重要,而且我们也可以喊sunsun🌞打游戏🎮,但调用的还是HomeWork这个委托。
所以我们可以直接不声明HomeWork这个委托类型。

SunLuckSun litteFriend = new SunLuckSun();
litteFriend.TimeUP = true;
Func<string, string> callDoubleSun = (string what) => { return $"{what},我一会再写"; };
litteFriend.call(callDoubleSun, "C井");
class SunLuckSun
{
    
    public event Func<string,string> dohomework;
    public Func<string, string> dododo = (string what) => { return "在写了在写了"; };
    private bool timesUp = false;

    public bool TimeUP
    {
        get { return timesUp; }
        set
        {
            timesUp = value;
            Console.WriteLine(dododo?.Invoke("every"));
        }
    }

    public void call(Func<string, string> doubleSunDo, String what)
    {
        Console.WriteLine(doubleSunDo(what));

    }
}

在这改动当中,我们把名为homework的委托类型替换了func,也不再需要声明一个专门的委托类型。这就是通用泛型委托。
当然如果你需要的委托本身就是泛型,需要接受一个类型参数,还是使用delegate自己定制化一个吧,delegate委托类型对签名的定制化还是可以提高可读性的。

func是有返回值的泛型委托。
接下来讲讲Action,和func一样的使用方式,只是Action是无返回值类型的委托。也就是说 < >里面包全是参数列表,并且参数列表可以为空,这和func不同,func的最后一个泛型类型必须有,为返回值类型。

predicate,返回值固定为bool,< >里面包的也是参数列表,参数也可以为空。

Part 4 事件,委托的安全封装 🐟

其实上述的这个🌞写作业就已经用到event事件了,事件相比于delegate 委托变量的好处就在于。在事件声明的类外部,只能对事件进行+=、-=。并不能对事件进行调用重新赋值,这些只能在类内才能执行。

事件使用方式和委托一样,就是一种更加安全的委托。


wang_hun
9 声望1 粉丝