【C#】委托和事件的基本使用

委托

当你需要将方法传递给其他方法时,就可以使用委托。用委托类型形参来调用传递的方法。

声明委托

下面是一个委托声明的例子:

public delegate string DoString(int i)

其中

delegate 是委托的关键字,当你需要声明一个委托时,需要给声明添加delegate关键字。

string 是当前委托的返回值类型。

DoString 是当前委托的委托名(可以理解为类名)

int 是当前委托的参数类型

声明委托可以理解为创建了一个类,委托名对应于类名,所以可以通过New的方法来创建一个委托。

使用委托

委托的类型安全性非常高,使用委托的方法时需要和委托类型有一样的签名(返回值,参数值等),委托才能够成功。 下面是使用委托的例子:

csharp 复制代码
//声明委托
    public delegate string DoString(int i);

    //使用委托的方法,签名与委托一致
    public static string MyTranslation(int num)
    {
        Console.WriteLine(num.ToString());
        return num.ToString();
    }

    public static void Main(string[] args)
    {
        //创建委托,并将委托方法赋值
        //可以使用下面两种方式
        //第一种通过构造传值
        //第二种是委托推断的方法,编译器会自行判断签名是否一致
        //委托赋值时绑定的方法不需要加()
        DoString doString = new DoString(MyTranslation);
        //DoString doString = MyTranslation;
        //调用委托方法传入参数值,使用委托的方法同时被调用并接收到传入的参数值
        //调用委托方法时需要添加()
        doString(87);
    }

在控制台输出的结果为

87

Action<T>Func<T>委托

Action<T>Func<T>委托是C#中直接定义好的两种委托类型,通常用于给需要将方法做为参数传递的方法,定义参数方法的类型。也可以直接声明Action<T>Func<T>委托进行委托赋值和使用。

Action<T>委托

表示一个返回值为void类型,并可接收参数的委托类型。当前委托存在不同变体,可以没有参数值,或最多可以定义8个参数值。 例如:Action<T1,T2,T3> action 则是一个含有三个参数的委托,参数类型分别为T1,T2,T3。

Action<T>委托还原为委托原型为 public delegate void Action<T>(T t)

Action<T>委托的使用方法

csharp 复制代码
    //使用Action委托的方法
    public static void Add(int num1,int num2)
    {
        var value = num1+num2;
        Console.WriteLine($"{num1}与{num2}相加的值为{value}");
    }
    //使用Action委托的方法
    public static void Sub(int num1,int num2)
    {
        var value = num1-num2;
        Console.WriteLine($"{num1}与{num2}相减的值为{value}");
    }
    
    //将Action委托做为方法的参数
    public static void Calculate(Action<T1,T2> action,int num1,int num2){
        action(num1,num2);
    }
    public static void Main(string[] args)
    {
        //声明一个Action委托的数组,将Add和Sub方法加入
          Action<int,int>[] actions{
              Add,
              Sub
          }
          //遍历数组,调用计算方法,则可以得出不同计算情况下的值
          foreach(var action in actions){
              Calculation(action,5,8);
          }
    }

在控制台输出的结果为

5与8相加的值为13

5与8相减的值为-3

Func<in T,out TResult>委托

表示有一个返回值,并可以接收参数的委托类型。当前委托存在变体,可以没有参数值,或最多可以定义16个参数值,但使返回值类型需要放在泛型参数的最后。

例如Func<in T1,in T2,in T3,out TResult>则是一个拥有三个参数T1,T2,T3和一个返回值TResult的委托类型。in代表参数,out代表返回值,如果委托只有参数,可以省略in。

Func<in T1,out TResult>还原为委托原型为 public delegate TResult Func<in T,out TResult>(T t)

Func<in T1,out TResult>委托的使用方法,对前面Action方法进行修改,可比较差异

csharp 复制代码
    //使用Func委托的方法
    public static int Add(int num1,int num2)
    {
        return num1+num2;
    }
    //使用Func委托的方法
    public static int Sub(int num1,int num2)
    {
        return num1-num2;
    }
    
    //将Func委托做为方法的参数
    public static void Calculate(Func<T1,T2> func,int num1,int num2){
        var value = func(num1,num2);
        Console.WriteLine($"{num1}与{num2} {func.name}的值为{value}");
    }
    public static void Main(string[] args)
    {
        //声明一个Func委托的数组,将Add和Sub方法加入
          Func<int,int>[] funcs{
              Add,
              Sub
          }
          //遍历数组,调用计算方法,则可以得出不同计算情况下的值
          foreach(var func in funcs){
              Calculation(func,5,8);
          }
    }

在控制台输出的结果为

5与8 Add的值为13

5与8 Sub的值为-3

多播委托

委托可以包含多个方法,这种委托方式称为多播委托。多播委托可以识别运算符"+"和"+="进行多播委托的绑定,也可以识别"-"和"-="进行多播委托的解绑。

多播委托调用时,绑定方法执行的顺序不是固定的,所以如果希望自行控制委托方法的调用,可以获取委托中绑定的所有方法的数组,并遍历数组进行调用。如果多播委托中的其中一个方法有异常,当抛出异常时,后续方法将不会调用。

csharp 复制代码
     
    public static void One()
    {
        Console.WriteLine("我是方法一");
    }

    public static void Two()
    {
        Console.WriteLine("我是方法二");
        throw new Exception("方法二的异常");
    }

    public static void Three()
    {
        Console.WriteLine("我是方法三");
    }

    public static void Main(string[] args)
    {
       //第一个绑定方法需要用"="赋值
        Action action = One;
        action += Two;
        action += Three;
        //获取action委托绑定的所有方法的数组
        Delegate[] delegates = action.GetInvocationList();
        foreach (Delegate myDelegate in delegates)
        {
            try
            {
                myDelegate();
            }
            catch (Exception)
            {
            //捕获异常,并打印。如果没有进行异常捕获,后续方法不会被调用
                Console.WriteLine("Catch Execption");
            }
        }
        action -= Two;
        action();
    }

在控制台输出的结果为

我是方法一

我是方法二

Catch Execption

我是方法三

我是方法一

我是方法三

匿名方法和lambda表达式

匿名方法

匿名方法其实就是定义一个临时方法,不为当前方法署名。优点是减少代码量,但是如果匿名方法多次编写同一种功能代码相同,还是定义一个方法相对合适。且进行委托绑定时,会自动推断出匿名方法的签名与委托是否一致。

csharp 复制代码
public static void Main(string[] args)
    {
        //匿名方法与func签名一致
        //有参数
        Func<int, string> func = delegate(int num)
        {
            string str = num.ToString();
            Console.WriteLine($"{str}在匿名方法中被打印");
            //有返回
            return str;
        };

        string str = func(1);
        Console.WriteLine($"{str}在Main方法中被打印");
    }

在控制台输出的结果为

1在匿名方法中被打印

1在Main方法中被打印

lambda表达式

从C# 3.0版本开始引入了Lambda表达式,提供了与匿名方法相同的功能及其他更多的功能,所以可以直接使用Lambda表达式替代匿名方法。

csharp 复制代码
public static void Main(string[] args)
    {
        
        //num表示参数值
        Func<int, string> func = num =>
        {
            string str = num.ToString();
            Console.WriteLine($"{str}在Lambda表达式中被打印");
            //有返回
            return str;
        };

        string str = func(1);
        Console.WriteLine($"{str}在Main方法中被打印");
    }

在控制台输出的结果为

1在Lambda表达式中被打印

1在Main方法中被打印

Lambda表达式的左边列出需要的参数,而其右边定义了赋予Lambda变量的方法的实现代码。 Func<int, string> func = ()=> ConsoleWriteLine("测试") 没有参数可以使用空心括号表示参数。

Func<int, string> func = (num1,num2)=> num1+num2 如果实现代码只有一行可以省略return和花括号,但是有多行一定要添加return和花括号。

闭包

通过Lambda表达式可以访问Lambda块外部的变量,称为闭包。这是Lambda表达式中一个很重要的概念。当访问外部变量时,编译器会给Lambda实现代码创建一个匿名类,在匿名类中创建一个外部变量类型的引用。

ini 复制代码
int value=5;
Func <int,int> func = x=>x + value;
value=7;
Console.WriteLine(func(5));

在控制台输出的结果为 12

因为实际上编译器为当前Lambda函数创建了匿名类,AnonymousClass和AnonymousMethod并不是匿名类的真实类名和方法名,实际类名和方法名只有编译器知道。

当委托方法被调用时,外部的变量才会通过构造函数进行赋值。

arduino 复制代码
public class AnonymousClass
{
    private int value;
    public AnonymousClass(int value){
        this.value=value;
    }
    public int AnonymousMethod(int x)=>x + value;
}

事件

事件是基于委托提供的一种发布订阅机制。在Unity中按钮点击触发都是使用事件的机制。

定义事件的语法为 public event EventHandler<EventArgs> myEvent

event关键字表明当前声明的是一个事件类型的委托,且委托类型是EventHandler

事件中的参数有两个,第一个是调用事件的对象,也就是事件本身,为Object类型;第二个则是事件参数,但这个参数一定要继承EventArgs类。泛型中只需要声明参数类型。

事件还原为的委托原型为public delegate void EventHandler<EventArgs>(object sender,TEventArgs e) where TEventArgs : EventArgs

事件的添加和删除也是和多播委托一样使用"+="进行添加,"-="进行删除。

事件的使用

csharp 复制代码
public static void Main(string[] args)
    {
        var finance = new Finance();
        var hr = new HR();
        var programmer = new Programmer();

        finance.salaryEvent += hr.GetFinanceMessage;
        finance.salaryEvent += programmer.GetFinanceMessage;

        finance.Notify();
    }

    public class HR
    {
        //用于绑定事件的方法
        public void GetFinanceMessage(object sender, SalaryEventArgs e)
        {
            Console.WriteLine($"HR 接收到 Finance 发送的消息 {e.message}");
        }
    }

    public class Programmer
    {
        //用于绑定事件的方法
        public void GetFinanceMessage(object sender, SalaryEventArgs e)
        {
            Console.WriteLine($"Programmer 接收到 Finance 发送的消息 {e.message}");
        }
    }

    public class Finance
    {
        //定义一个事件,参数类型是SalaryEventArgs
        public event EventHandler<SalaryEventArgs> salaryEvent;
        
        //调用事件的方法
        public void Notify()
        {
            var salaryEventArgs = new SalaryEventArgs();
            //?.是对当前对象引用判空操作,非空则进行方法调用
            salaryEvent?.Invoke(this, salaryEventArgs);
        }
    }
    //事件参数类型,继承EventArgs
    public class SalaryEventArgs : EventArgs
    {
        public string message = "今天下午发工资";
    }

在控制台输出的结果为

HR 接收到 Finance 发送的消息 今天下午发工资

Programmer 接收到 Finance 发送的消息 今天下午发工资

总结

在设计大型应用程序时,使用委托和事件可以减少依赖性和各层的耦合,并能开发出具有更高重用性的组件。

相关推荐
坐井观老天5 小时前
在C#中使用资源保存图像和文本和其他数据并在运行时加载
开发语言·c#
pchmi7 小时前
C# OpenCV机器视觉:模板匹配
opencv·c#·机器视觉
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭9 小时前
C#都可以找哪些工作?
开发语言·c#
boligongzhu10 小时前
Dalsa线阵CCD相机使用开发手册
c#
向宇it1 天前
【从零开始入门unity游戏开发之——C#篇23】C#面向对象继承——`as`类型转化和`is`类型检查、向上转型和向下转型、里氏替换原则(LSP)
java·开发语言·unity·c#·游戏引擎·里氏替换原则
sukalot1 天前
windows C#-命名实参和可选实参(下)
windows·c#
小码编匠1 天前
.NET 下 RabbitMQ 队列、死信队列、延时队列及小应用
后端·c#·.net
我不是程序猿儿1 天前
【C#】Debug和Release的区别和使用
开发语言·c#
lzhdim1 天前
2、C#基于.net framework的应用开发实战编程 - 设计(二、二) - 编程手把手系列文章...
开发语言·c#·.net