C# 系列 -- 委托和事件

796 阅读7分钟

委托

如果我们想 传递一个函数作为参数 怎么办? C# 如何处理回调函数或事件处理程序? 答案是——委托

委托的核心作用是:把函数作为参数传递给委托,可通过委托实例加和累计多个函数,按流程帮我处理

委托是定义方法签名的引用类型数据类型。 您可以定义委托的变量,就像其他数据类型一样,可以引用与委托具有相同签名的任何方法。

我们使用委托时会涉及到三个步骤:

  • 声明一个委托 Delegate
  • 设置目标方法
  • 调用委托 Delegate

声明委托

可以使用 delegate 关键字后跟函数签名来声明委托,如下所示。

[访问修饰符] delegate [返回类型] [委托名称]([参数...])

下面声明了一个名为 MyDelegate 的委托。

public delegate void MyDelegate(string msg);

上面,我们已经声明了一个具有 void 返回类型和字符串参数的委托 MyDelegate。 委托可以在类外类内声明。 推荐:声明在类之外

声明委托后,我们需要设置目标方法或 lambda 表达式。 我们可以通过使用 new 关键字创建委托对象并传递签名与委托签名匹配的方法来实现。

委托的使用

using System;

namespace ConsoleApp1
{
    // 1. 声明一个委托
    public delegate void MyDelegate(string msg);
    class Program
    {
        // 2. 设置跟 MyDelegate 有相同参数和返回值的方法
        static void MethodA(string message)
        {
            Console.WriteLine(message);
        }

        static void Main(string[] args)
        {
            // a. 把函数传给委托
            MyDelegate del1 = new MyDelegate(MethodA);
            // b. 直接赋值给委托
            MyDelegate del2 = MethodA;
            // c. 使用 lambda 表达式
            MyDelegate del3 = (string msg) => Console.WriteLine(msg);

            // 3. 调用委托
            del1.Invoke("1"); // a. 使用 Invoke() 方法调用委托
            del2("2"); // b. 使用 () 运算符调用委托
            del3("3");

            Console.ReadKey();
        }
    }
}

运行结果:

1
2
3

多播委托

委托可以 引用多个方法。当调用多播委托时,它将依次调用所有关联的方法。要将多个方法分配给同一个委托实例,可以使用 += 运算符

using System;

delegate int NumberChanger(int n);
namespace DelegateAppl
{
   class TestDelegate
   {
      static int num = 10;
      public static int AddNum(int p)
      {
         num += p;
         return num;
      }

      public static int MultNum(int q)
      {
         num *= q;
         return num;
      }
      public static int getNum()
      {
         return num;
      }

      static void Main(string[] args)
      {
         // 创建委托实例
         NumberChanger nc;
         NumberChanger nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         nc = nc1;
         nc += nc2;
         // 调用多播
         nc(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}

运行结果:

Value of Num: 75

将委托(函数)作为参数传递

在c#里面,有的时候一些代码的实现必须用到委托,比如:

  • 线程里面修改某个textBox的值,如果直接在线程里面写修改的代码,执行时候,编译器会报错,因为c#不允许这样写。
  • 还有在单独写的类里面,修改某个form里面某个控件的属性值等等,也是不被允许的。

这时候,就需要使用到委托(delegate)。委托其实是这样的,为某些实现写一个函数,并将其赋值给委托(相当于函数指针),在使用的时候直接通过 委托名 来调用。
如果我们要把方法当做参数来进行传递的话,就要用到委托。 简单来说,委托是一个类型,这个类型可以赋值一个方法的引用。这是委托的主要用途

委托的理解是:委托别人帮我做事,那别人就得提前想(写)好方法才能帮我干活

using System;

class Program
{
    // 1. 定义委托
    public delegate void GreetDelegate(string name);
    // 2. 设置跟 GreetDelegate 有相同参数和返回值的方法
    private static void EnglishGreeting(string name)
    {
        Console.WriteLine("Morning, " + name);
    }
    private static void ChineseGreeting(string name)
    {
        Console.WriteLine("早上好," + name);
    }
    // 3. 定义一个接收【name + GreetingDelegate】类型的方法
    private static void GreetPeople(string name, GreetDelegate MakeGreeting)
    {
        MakeGreeting(name);
    }
    static void Main(string[] args)
    {
        // 4. 将方法作为参数传递,并同时传递方法执行需要的参数
        GreetPeople("bro", EnglishGreeting);
        GreetPeople("兄弟", ChineseGreeting);
        Console.ReadKey();
    }
}

运行结果:

Morning, bro
早上好,兄弟

事件

事件(Event)基本上说是某个【用户操作】,如按键、点击、鼠标移动等等,或者是一些【提示通知】,如系统生成的通知。应用程序需要在事件发生时响应事件

C# 中使用事件机制实现 线程间的通信

通过事件使用委托

事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。包含事件的类用于发布事件。这被称为 发布器  类。其他接受该事件的类被称为 订阅器)  类。事件使用 发布-订阅  模型。

发布器  是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象调用这个事件,并通知其他的对象。

订阅器  是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)。

声明事件

在类的内部声明事件,首先必须声明该事件的委托类型。例如:

public delegate void BoilerLogHandler(string status);

然后,声明事件本身,使用 event 关键字:

// 基于上面的委托定义事件
public event BoilerLogHandler BoilerEventLog;

上面的代码定义了一个名为 BoilerLogHandler 的委托和一个名为 BoilerEventLog 的事件,该事件在生成的时候会调用委托。

实例

using System;
namespace SimpleEvent
{
  using System;
  /***********发布器类***********/
  public class EventTest
  {
    private int value;
    public delegate void NumManipulationHandler();
    public event NumManipulationHandler ChangeNum;
    protected virtual void OnNumChanged()
    {
      if ( ChangeNum != null )
      {
        ChangeNum(); /* 事件被触发 */
      } else {
        Console.WriteLine( "event not fire" );
        Console.ReadKey(); /* 回车继续 */
      }
    }

    public EventTest()
    {
      int n = 5;
      SetValue( n );
    }

    public void SetValue( int n )
    {
      if ( value != n )
      {
        value = n;
        OnNumChanged();
      }
    }
  }

  /***********订阅器类***********/

  public class subscribEvent
  {
    public void printf()
    {
      Console.WriteLine( "event fire" );
      Console.ReadKey(); /* 回车继续 */
    }
  }

  /***********触发***********/
  public class MainClass
  {
    public static void Main()
    {
      EventTest e = new EventTest(); /* 实例化对象,第一次没有触发事件 */
      subscribEvent v = new subscribEvent(); /* 实例化对象 */
      e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 注册 */
      e.SetValue( 7 );
      e.SetValue( 11 );
    }
  }
}

委托和事件的区别

委托和事件有一定的相似性,这里先说一下委托和事件的定义

委托:delegate 是一种可用于封装命名或匿名方法的引用类型。 委托类似于 C++ 中的 函数指针;但是,委托是类型安全和可靠的。

事件:事件是特殊类型的多路广播委托,仅可从声明它们的类或结构里面调用。 如果其他类或结构订阅了该事件,则当发行者类引发该事件时,会调用其事件处理程序方法。

委托和事件的区别如下:

区别委托事件
1是否是一个类型否,事件修饰的是一个对象
2是否可以在类外部进行调用
3是否可以使用=来赋值

区别一:是否是一个类型

class Class3
{
    static void Main(string[] args)
    {
        //委托正确使用
        Class1.NumberChanger n1 = Class2.ClassA_Test;
        //事件使用 编译器报错
        Class1.changer handle2 = Class2.ClassA_Test;
    }
}

错误:Class1.changer 是“字段”,但此处被当做“类型”来使用

区别二:委托可以在声明它的类外部进行调用,而事件只能在类的内部进行调用。

(1)在类外部调用委托

在这里插入图片描述

(2)在类外部调用事件

在这里插入图片描述

事件“ClassC.Say_EventHandler”只能出现在 += 或 -= 的左边(从类型“ClassC”中使用时除外)

区别三:委托可以在外部类使用 = 来赋值,事件只能在内部类用 = 赋值,外部类不可以

在这里插入图片描述

从编译器提示的错误,我们可以了解到,事件只能在声明它的类内部被调用。从事件本身来讲,事件一般用于类自身的属性变化时,用来通知外界自身的变化的。我们将对ClassC内部的一个属性赋值,然后调用事件,模拟对外通知。代码如下所示

在这里插入图片描述

总结:事件与委托最主要的区别应该是不能在外部调用,但可以通过+=或-=进行注册,但如果委托变量为私有,则外部不能注册;如果为公有,则外部有可以调用,破坏了封装,所以没办法,在这种情况就定义一个event就好了