C#基础之委托,事件-1 委托

1.1 简介

C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。
委托(Delegate)特别用于实现事件回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。

1.2 操作使用

1.2.1 声明委托(Delegate)

委托声明决定了可由该委托引用的方法。委托可指向一个与其具有相同标签的方法。
例如,假设有一个委托:public delegate int MyDelegate (string s);
上面的委托可被用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量

声明委托的语法如下:

delegate <return type> <delegate-name> <parameter list>

1.2.2 实例化委托(Delegate)

一旦声明了委托类型,委托对象必须使用 new 关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new 语句的参数就像方法调用一样书写,但是不带有参数。例如:

public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);

下面的实例演示了委托的声明、实例化和使用,该委托可用于引用带有一个整型参数的方法,并返回一个整型值。

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 nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         // 使用委托对象调用方法
         nc1(25);
         Console.WriteLine("Value of Num: {0}", getNum());
         nc2(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}

结果:
Value of Num: 35
Value of Num: 175

注意:调用委托对应方法一般是通过invoke方法,但是从 C# 2.0 开始,委托的调用可以直接使用方法调用语法,而不需要显式调用 Invoke 方法。

1.2.3 直接调用和invoke

虽然委托调用底层实际上是通过 Invoke 方法实现的,但语法上允许直接调用委托,就像调用普通方法一样。换句话说,调用委托和直接调用 Invoke 方法是等效的。

假设我们有一个 Action 类型的委托:

Action action = () => Console.WriteLine("Hello, Delegate!");

直接调用委托:
action(); // 输出: Hello, Delegate!

通过 Invoke 方法调用:
action.Invoke(); // 输出: Hello, Delegate!

两种方式的结果完全一样,因为 () 是对委托对象 Invoke 方法的简化语法糖

为什么允许直接调用?

  • 简洁性:如果每次调用都必须写 .Invoke,代码显得冗长。因此,C# 提供了直接调用语法,增强代码可读性。
  • 语法糖:编译器在编译时会自动将直接调用委托语法转换为 Invoke 方法的调用。
    即:action(); 实际被编译为:action.Invoke();
  • 优先推荐直接调用
    直接调用的方式更加简洁可读,因此在大多数情况下,推荐使用 action() 而不是显式调用 action.Invoke()

为什么保留 Invoke 方法?虽然直接调用语法更方便,但在某些特殊场景下,显式调用 Invoke 方法可能更合适:

  • 反射场景:通过反射调用委托时,需要使用 Invoke 方法。
  • 动态场景:在动态生成代码或动态委托时,Invoke 方法更明确。
using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        Action action = PrintMessage;

        // 使用反射调用 Invoke
        MethodInfo invokeMethod = action.GetType().GetMethod("Invoke");
        invokeMethod.Invoke(action, null);
    }

    static void PrintMessage()
    {
        Console.WriteLine("Hello, Reflection!");
    }
}
输出:
Hello, Reflection!

1.2.4 Invoke 和 BeginInvoke

委托的 InvokeBeginInvoke 方法分别用于同步异步调用委托。它们的主要区别体现在调用方式、线程管理和返回结果的处理上。

Invoke 和 BeginInvoke 的区别

特性 Invoke BeginInvoke
调用类型 同步调用 异步调用
线程阻塞 当前线程会阻塞,直到方法执行完成 当前线程不会阻塞
返回结果 直接返回方法的返回值 返回 IAsyncResult 对象,通过 EndInvoke 获取返回值
异常处理 异常会直接在调用线程中抛出 异常在调用 EndInvoke 时抛出
线程使用 在调用线程上执行方法 在线程池中执行方法
使用场景 方法较快且调用线程不能被中断时 方法较慢且需要异步执行时
  • Invoke:同步调用
    定义:Invoke 是同步调用,当前线程会等待方法执行完毕后再继续执行后续代码。
    特点:
    • 阻塞调用:调用线程会被阻塞,直到被调用的方法完成。
    • 返回结果:直接返回被调用方法的返回值(如果有)。
    • 异常处理:如果被调用的方法抛出异常,异常会在调用线程中传播。
// 定义一个委托
delegate int AddDelegate(int x, int y);

AddDelegate add = (x, y) => x + y;

// 同步调用
int result = add.Invoke(3, 4);
Console.WriteLine($"Result: {result}"); // 输出:Result: 7
  • BeginInvoke:异步调用
    定义:BeginInvoke 是异步调用,立即返回一个 IAsyncResult 对象,并不会阻塞调用线程。
    特点:
    • 非阻塞调用:调用线程可以继续执行其他代码,而被调用的方法在后台线程中执行。
    • 回调机制:可以通过传递回调方法或轮询 IAsyncResult 对象来获取结果。
      需要显式调用 EndInvoke 方法以获取结果或处理异常。
// 定义一个委托
delegate int AddDelegate(int x, int y);
AddDelegate add = (x, y) =>
{
    Console.WriteLine("Adding...");
    System.Threading.Thread.Sleep(2000); // 模拟耗时操作
    return x + y;
};

// 异步调用
IAsyncResult asyncResult = add.BeginInvoke(3, 4, null, null);
// 主线程继续执行其他任务
Console.WriteLine("Doing other work...");
// 获取异步调用结果
int result = add.EndInvoke(asyncResult);
Console.WriteLine($"Result: {result}"); // 输出:Result: 7

BeginInvoke 的回调,可以通过回调函数在异步操作完成后处理结果:

void CallbackMethod(IAsyncResult ar)
{
    // 获取委托实例
    AddDelegate add = (AddDelegate)ar.AsyncState;
    // 获取结果
    int result = add.EndInvoke(ar);
    Console.WriteLine($"Result in Callback: {result}");
}

AddDelegate add = (x, y) =>
{
    Console.WriteLine("Adding...");
    System.Threading.Thread.Sleep(2000);
    return x + y;
};

// 异步调用并指定回调函数
add.BeginInvoke(5, 7, CallbackMethod, add);
// 主线程继续工作
Console.WriteLine("Doing other work...");

注意事项:

  • BeginInvoke 使用线程池中的线程来执行方法,因此需要注意线程池的资源消耗。
    必须调用 EndInvoke:
  • 调用 BeginInvoke 后,无论是否需要结果,都必须调用 EndInvoke,否则可能会导致资源泄漏。
  • 推荐使用 Task 和 async/await:
  • 在现代 C# 中,推荐使用 Task 和 async/await 替代 BeginInvokeEndInvoke,因为它们更易读且不易出错。

1.3 委托的多播

委托对象可使用 + 运算符进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。- 运算符可用于从合并的委托中移除组件委托。
使用委托的这个有用的特点,可以创建一个委托被调用时要调用的方法的调用列表。这被称为委托的 多播(multicasting),也叫组播
+- 运算符确实可以直接用于委托对象的合并和移除,但这和 +=-= 的用法有所不同。它们的区别主要在于运算场景赋值方式。具体来说

  • +- 运算符:用于直接创建新的委托对象,不影响原始委托。它们不会修改原始委托,而是生成一个新的多播委托对象。
    • 使用 + 合并两个委托对象,生成一个新的多播委托。
    • 使用 - 从多播委托中移除一个委托,生成一个新的委托对象。
  • +=-= 运算符:用于修改已有的委托实例,直接在原始变量上添加或移除委托。
    • += 将一个委托添加到现有委托链上,结果赋给原变量。
    • -= 从现有委托链中移除一个委托,结果赋给原变量。

下面的程序演示了委托的多播:

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#中,当使用+=操作符向委托添加方法时,有两种方式是等效的:

  • 显式地创建一个新的委托实例并将其添加到现有的委托链中
    myDelegate += new NumberChanger(AddNum);
  • 省略new 部分,直接添加方法。C#编译器会自动为您处理委托的实例化(如果必要的话):myDelegate += AddNum
    这两种方式在功能上是完全相同的。从C# 2.0开始,第二种方式(省略new关键字和委托类型)变得更加流行,因为它更简洁,并且减少了不必要的代码。

1.4 委托的匿名和lambda

二者比较:

特性 匿名方法 (delegate) Lambda 表达式
语法简洁性 较繁琐,需要显式写出 delegate 关键字和参数列表 更简洁,直接用 (参数) => {} 表达逻辑
表达式形式支持 不支持表达式形式,必须用 {} 包裹逻辑块 支持表达式形式,单行逻辑可以省略 {} 和 return
捕获外部变量(闭包) 支持 支持
语法风格 更接近传统 C# 方法声明 更现代、函数式编程风格
语义清晰性 delegate 明确表明它是匿名方法 使用 => 运算符,强调简洁和函数式思想

1.4.1 匿名方法

匿名方法是通过使用 delegate 关键字创建委托实例来声明的。
语法

delegate(parameters) { statement; }

例如:

delegate void NumberChanger(int n);
...
NumberChanger nc = delegate(int x)
{
    Console.WriteLine("Anonymous Method: {0}", x);
};

代码块 Console.WriteLine("Anonymous Method: {0}", x); 是匿名方法的主体。

委托可以通过匿名方法调用,也可以通过命名方法调用,即,通过向委托对象传递方法参数。

using System;

delegate void NumberChanger(int n);
namespace DelegateAppl
{
    class TestDelegate
    {
        static int num = 10;
        public static void AddNum(int p)
        {
            num += p;
            Console.WriteLine("Named Method: {0}", num);
        }

        public static void MultNum(int q)
        {
            num *= q;
            Console.WriteLine("Named Method: {0}", num);
        }

        static void Main(string[] args)
        {
            // 使用匿名方法创建委托实例
            NumberChanger nc = delegate(int x)
            {
               Console.WriteLine("Anonymous Method: {0}", x);
            };
            
            // 使用匿名方法调用委托
            nc(10);

            // 使用命名方法实例化委托
            nc =  new NumberChanger(AddNum);
            
            // 使用命名方法调用委托
            nc(5);

            // 使用另一个命名方法实例化委托
            nc =  new NumberChanger(MultNum);
            
            // 使用命名方法调用委托
            nc(2);
            Console.ReadKey();
        }
    }
}

1.4.2 lambda 表达式

在 C# 2.0 及更高版本中,引入了 lambda 表达式,它是一种更简洁的语法形式,用于编写匿名方法。并且 从 C# 2.0 开始对委托的实例化做了简化,委托类型的实例化在某些情况下可以省略显式使用 new 关键字
使用 lambda 表达式:

using System;

delegate void NumberChanger(int n);

namespace DelegateAppl
{
    class TestDelegate
    {
        static int num = 10;
        public static void AddNum(int p)
        {
            num += p;
            Console.WriteLine("Named Method: {0}", num);
        }

        public static void MultNum(int q)
        {
            num *= q;
            Console.WriteLine("Named Method: {0}", num);
        }

        static void Main(string[] args)
        {
            // 使用 lambda 表达式创建委托实例
            NumberChanger nc = x => Console.WriteLine($"Lambda Expression: {x}");

            // 使用 lambda 表达式调用委托
            nc(10);

            // 使用命名方法实例化委托
            nc = new NumberChanger(AddNum);

            // 使用命名方法调用委托
            nc(5);

            // 使用另一个命名方法实例化委托
            nc = new NumberChanger(MultNum);

            // 使用命名方法调用委托
            nc(2);

            Console.ReadKey();
        }
    }
}

1.5 内置委托

C# 提供了一些内置的泛型委托,可以覆盖大部分常见场景,主要包括以下几个

1.5.1 Action系列

Action 是一个用于定义没有返回值的方法的委托。支持最多 16 个参数的重载。

Action action = () => Console.WriteLine("No parameters");
action();

Action<int, string> actionWithParams = (x, y) => Console.WriteLine($"x: {x}, y: {y}");
actionWithParams(10, "hello");

1.5.2 Func 系列

Func 是一个带有返回值的泛型委托。最多支持 16 个输入参数,最后一个泛型参数是返回值的类型,前面的泛型参数表示输入参数

Func<int, int, int> add = (x, y) => x + y;
int result = add(3, 5);
Console.WriteLine(result); // 输出 8

1.5.3 Predicate

Predicate<T> 是一个返回 bool 的泛型委托,常用于过滤或条件判断。

Predicate<int> isEven = x => x % 2 == 0;
bool check = isEven(4);
Console.WriteLine(check); // 输出 True

1.6 示例

下面的实例演示了委托的用法。委托 printString 可用于引用带有一个字符串作为输入的方法,并不返回任何东西。

我们使用这个委托来调用两个方法,第一个把字符串打印到控制台,第二个把字符串打印到文件:

using System;
using System.IO;

namespace DelegateAppl
{
   class PrintString
   {
      static FileStream fs;
      static StreamWriter sw;
      // 委托声明
      public delegate void printString(string s);

      // 该方法打印到控制台
      public static void WriteToScreen(string str)
      {
         Console.WriteLine("The String is: {0}", str);
      }
      // 该方法打印到文件
      public static void WriteToFile(string s)
      {
         fs = new FileStream("c:\\message.txt", FileMode.Append, FileAccess.Write);
         sw = new StreamWriter(fs);
         sw.WriteLine(s);
         sw.Flush();
         sw.Close();
         fs.Close();
      }
      // 该方法把委托作为参数,并使用它调用方法
      public static void sendString(printString ps)
      {
         ps("Hello World");
      }
      static void Main(string[] args)
      {
         printString ps1 = new printString(WriteToScreen);
         printString ps2 = new printString(WriteToFile);
         sendString(ps1);
         sendString(ps2);
         Console.ReadKey();
      }
   }
}

结果:
The String is: Hello World
上一篇:NLP 的发展历程