unity的C#学习——委托、事件和匿名方法


C# 委托

委托是C#编程语言中的一个重要特性,它是一种类似于函数指针的引用类型变量,它可以存储对一个或多个方法的引用。

  • 委托是一种类型安全的“函数指针”,因为它会对被引用方法的签名和返回类型进行检查,确保方法参数和返回值类型与委托定义的一致。

委托可以在运行时被动态地改变,使得我们可以将方法作为参数进行传递,以及在需要时动态地调用它们,从而实现回调、事件处理和异步编程等功能。


1、委托的声明

在 C# 中,委托是一种引用类型变量,它的声明使用 delegate 关键字,其语法格式为:

<访问修饰符> delegate <返回类型> <委托名称>([参数列表]);

其中:

  • 访问修饰符是可选项,表示委托的访问级别,默认为internal
  • 返回类型表示委托所引用方法的返回类型
  • 委托名称是对委托类型的命名
  • 参数列表包含一个或多个参数,这些参数的类型和数量必须与委托所引用的方法的参数列表相同

例如,下面是声明一个简单的委托类型的示例:

delegate void MyDelegate(string name);

该委托类型可以引用一个参数为字符串类型、无返回值的方法。上述委托的访问等级为默认的 internal,这意味着仅同一个程序集内的其他代码可以访问该委托类型,但是在程序集之外的代码则不能直接访问该委托类型。


2、委托的实例化

委托的使用可以分为以下几步:

  1. 声明委托类型:使用delegate关键字声明一个委托类型,指定委托可以引用的方法的参数列表和返回值类型。
  2. 实例化委托对象:使用委托类型实例化一个委托对象,将该委托对象引用一个或多个方法。
  3. 调用委托:调用委托对象可以调用其所引用的方法。

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

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

以下是委托的声明、实例化并调用的示例代码:

using System;

// 声明一个委托类型
delegate int Calculate(int a, int b);

class Program
{
    
    
    static int Add(int a, int b)
    {
    
    
        return a + b;
    }

    static int Multiply(int a, int b)
    {
    
    
        return a * b;
    }

    static void Main(string[] args)
    {
    
    
        // 创建委托实例,并将其指向Add方法
        Calculate calc = Add;

        // 使用委托调用Add方法
        int result = calc(3, 5);
        Console.WriteLine(result);  // 输出 8

        // 将委托实例重新指向Multiply方法
        calc = Multiply;

        // 使用委托调用Multiply方法
        result = calc(3, 5);
        Console.WriteLine(result);  // 输出 15
    }
}

在这个示例中,我们首先声明了一个委托类型 Calculate,其定义了一个接收两个 int 参数并返回一个 int 类型值的方法签名。

然后,我们定义了两个静态方法 AddMultiply,用于执行加法和乘法运算。接下来,我们创建了一个委托实例 calc 并将其指向 Add 方法,然后使用 calc 调用了 Add 方法并输出结果。接着,我们将 calc 重新指向 Multiply 方法,并再次使用 calc 调用了 Multiply 方法并输出结果。


补充:委托实例化的快捷方式

using System;

delegate void MyDelegate(string message);

MyDelegate d1 = Method1;  //这种语法,是委托实例化的一种快捷方式,与下方等价
MyDelegate d1 = new MyDelegate(Method1);

需要注意的是,这种快捷方式只适用于将委托实例化为一个方法的情况。对于需要使用 new 语法实例化的其他类型,不能使用这种快捷方式。例如,如果要实例化一个自定义类的对象,仍然需要使用完整的 new 语法。


3、System.Delegate类

System.Delegate 是所有委托类型的基类,它提供了一些有用的方法,使得对委托的使用更加方便。以下是System.Delegate 类的一些主要功能:

  • Combine:用于将两个委托合并成一个新的委托,使得调用新的委托时,会依次调用原始的两个委托。例如,可以使用Combine方法将两个事件处理方法组合成一个委托,用于处理事件。

  • Remove:用于从一个委托中移除另一个委托。如果将一个委托从另一个委托中移除后,调用新的委托时就不会调用移除的委托了。

  • Equals:用于比较两个委托是否相等。如果两个委托调用的是同一个方法,它们就是相等的。

  • GetInvocationList:用于获取一个委托中引用的所有方法的列表。如果一个委托包含多个方法,可以使用GetInvocationList 方法获取它们的列表。

下面是关于委托的示例代码,演示了 Combine、Remove、Equals、GetInvocationList 方法的用法:

using System;

delegate void DelegateType();

class Program
{
    
    
    static void Main(string[] args)
    {
    
    
        DelegateType handler1 = new DelegateType(Method1);
        DelegateType handler2 = new DelegateType(Method2);
        DelegateType handler3 = new DelegateType(Method3);

        // Combine two delegates
        DelegateType combined = handler1 + handler2;
        Console.WriteLine("Combined delegate calls:");
        combined();

        // Remove a delegate
        combined -= handler2;
        Console.WriteLine("Combined delegate after removal:");
        combined();

        // Check if two delegates are equal
        bool equals = handler1.Equals(combined);
        Console.WriteLine($"handler1 equals combined? {
      
      equals}");

        // Get an array of invocation list delegates
        Delegate[] invocationList = combined.GetInvocationList();
        Console.WriteLine("Invocation list:");
        foreach (Delegate del in invocationList)
        {
    
    
            del.DynamicInvoke();
        }
    }

    static void Method1()
    {
    
    
        Console.WriteLine("Method 1 called.");
    }

    static void Method2()
    {
    
    
        Console.WriteLine("Method 2 called.");
    }

    static void Method3()
    {
    
    
        Console.WriteLine("Method 3 called.");
    }
}

输出结果为:

Combined delegate calls:
Method 1 called.
Method 2 called.


Combined delegate after removal:
Method 1 called.


handler1 equals combined? True


Invocation list:
System.DelegateType Method1()

除此之外,System.Delegate 还提供了一些与安全性相关的方法,如 DynamicInvokeDynamicInvokeImpl ,这些方法允许在运行时动态地调用委托,并检查调用的方法是否具有正确的参数和返回类型


4、委托的高级特性

主要介绍一下委托的多播、匿名委托和 Lambda 表达式。

4.1 委托的多播能力

在C#中,委托还具有多播(Multicast)能力,即一个委托对象可以包含多个方法的引用,可以将多个委托对象合并为一个委托对象,并一次性调用所有包含的方法。这使得委托在事件处理中尤其有用,因为多个方法可以同时响应同一个事件。

  • 要创建一个多播委托,可以使用 "+=" 运算符将一个委托与另一个委托连接起来,形成一个包含多个方法引用的新委托。类似地,使用 "-=" 运算符可以将委托从多播委托中移除。
  • 此外也可以使用上一部分提到的 CombineRemove 两个方法。

以下是一个简单的示例代码,演示了如何创建、合并和移除委托的多播链

using System;

delegate void MyDelegate(string message);

class Program
{
    
    
    static void Main(string[] args)
    {
    
    
        MyDelegate d1 = Method1;
        MyDelegate d2 = Method2;
        MyDelegate d3 = Method3;

        MyDelegate d = d1 + d2 + d3;   // 创建多播委托
        d("Hello, world!");            // 调用多播委托

        d = d - d2;                   // 移除 d2 委托
        d("Hello again!");            // 调用修改后的多播委托
    }

    static void Method1(string message)
    {
    
    
        Console.WriteLine("Method1 says: " + message);
    }

    static void Method2(string message)
    {
    
    
        Console.WriteLine("Method2 says: " + message);
    }

    static void Method3(string message)
    {
    
    
        Console.WriteLine("Method3 says: " + message);
    }
}

这段代码的运行结果为:

Method1 says: Hello, world!
Method2 says: Hello, world!
Method3 says: Hello, world!


Method1 says: Hello again!
Method3 says: Hello again

4.2 匿名委托和Lambda表达式

匿名委托和Lambda表达式是两种创建委托实例的方式。

  • 匿名委托是指在创建委托实例时,不需要先定义一个方法,而是直接在实例化委托的过程中定义一个匿名方法。匿名委托的语法类似于定义一个方法,但不需要方法名,而是在后面跟上方法体。例如:
delegate int Calculate(int x, int y);

// 创建匿名委托实例
Calculate calc = delegate (int x, int y) {
    
    
    return x + y;
};
  • Lambda表达式是一种更为简洁的匿名方法写法,它可以把一个方法体压缩成一行代码,并且省略掉参数类型和返回类型的声明。Lambda表达式的语法是:参数列表 => 方法体,例如:
// 创建Lambda表达式委托实例
Calculate calc = (x, y) => x + y;

Lambda 表达式的优势在于可以大大简化代码,让代码更加易读和易懂。此外,Lambda 表达式也支持 LINQ 等一些强大的功能。


C# 事件

在 C# 中,事件是一种特殊的委托,它允许一个对象在发生某个特定的动作或状态改变时,通知其他对象对这个事件进行响应和处理。例如,当用户点击一个按钮时,系统会触发一个 Click 事件,应用程序可以通过响应该事件来实现对用户的操作。

事件机制的作用在于实现对象间的解耦,也就是将事件的发布者和订阅者解耦。事件让发布者不需要知道谁或哪些对象对事件感兴趣,也不需要直接调用特定的对象或函数来响应事件。事件只需要向外部通知事件发生即可,而订阅者需要注册事件处理器来响应事件。这样一来,发布者与订阅者之间就可以保持独立,且可以随意变换,使得代码更加灵活、可维护性更高。


1、事件的声明和使用

在 C# 中,声明和使用事件通常需要以下步骤:

1.1 定义事件委托类型

事件委托类型定义了事件处理q器的方法签名(包括方法的名称、返回类型以及参数类型和顺序等信息)。在 C# 中,可以使用 delegate 关键字定义事件委托类型。例如,以下代码定义了一个事件委托类型 EventHandler

public delegate void EventHandler(object sender, EventArgs e);

这个委托类型有两个参数:

  • 一个是 object 类型的 sender,用于指示事件的来源
  • 另一个是 EventArgs 类型的 e,用于传递事件的附加信息

在使用事件时,可以使用这个委托类型作为事件的类型

1.2 声明事件

在一个类中声明一个事件,需要使用 event 关键字和事件委托类型。例如,以下代码声明了一个名为 ButtonClick 的事件:

public event EventHandler ButtonClick;

这个事件使用了之前定义的 EventHandler 委托类型。注意,在事件的使用中委托不再被视为引用变量,而是被视为一种特殊的数据类型,所以声明事件也叫创建委托类型的变量

  • 具体来说,事件是委托类型的实例,它具有和委托类型相同的特性,包括可以添加或移除多个委托实例,触发委托实例等。
  • 当我们声明一个事件时,实际上就是在声明一个委托类型的变量,然后使用关键字 event 修饰它,使得它只能在类内部被访问并且只能被用于绑定方法

1.3 触发事件

触发一个事件的通常做法是通过调用事件的委托变量来实现的。具体来说,事件触发器(包含触发语句的一个方法)将检查事件的委托变量是否为 null,如果不为 null,则调用委托变量。通常使用 Invoke 方法来调用委托变量。

例如,以下代码触发了 ButtonClick 事件:

ButtonClick?.Invoke(this, EventArgs.Empty);

其中:

  • Invoke() 是一个委托类型的方法,用于执行指定的委托。我们之前说的调用委托对象来调用其所引用的方法,底层就是使用了 Invoke() 方法。
  • ButtonClick 事件的类型为 EventHandler,它是一个委托类型,定义了接受两个参数(object sender, EventArgs e)且返回类型为 void 的方法。因此:
    • this 表示将事件源作为参数传递给这些方法;
    • EventArgs.Empty 表示将空的 EventArgs 对象传递给这些方法,即不传递附加信息。
  • ?. 是我们之前提到的可空运算符,表示当 ButtonClicknull 时不调用其 Invoke 方法。

当然也并非一定要使用上述方法触发事件,也可以写成下面的格式:

if ( ChangeNum != null )
{
    
    
	ButtonClick(this, EventArgs.Empty); // 事件被触发
}
else
{
    
    
	Console.WriteLine( "event not fire" );
	Console.ReadKey(); // 回车继续 
}

1.4 实现事件处理器

事件处理器是一个方法,实现该方法时,需要遵循事件委托类型定义的方法签名,这里主要指该方法的返回类型、参数类型及顺序需要与委托类型定义的相同。

例如,以下代码实现了一个名为 OnButtonClick 的事件处理器:

// 事件处理器
private void OnButtonClick(object sender, EventArgs e)
{
    
    
    // 处理事件
}

在事件处理器中,可以编写处理事件的代码。其中,sender 参数表示事件的来源,e 参数表示事件的附加信息。

1.5 添加和移除事件处理器

在使用事件时,可以添加和移除事件处理器。

例如,以下代码通过 += 运算符,添加了一个名为 OnButtonClick 的事件处理器:

ButtonClick += OnButtonClick;

这个代码将 OnButtonClick 方法添加到 ButtonClick 事件的事件处理器列表中。要从事件处理器列表中移除事件处理器,可以使用 -= 运算符:

ButtonClick -= OnButtonClick;

这个代码将 OnButtonClick 方法从 ButtonClick 事件的事件处理器列表中移除。


2、通过事件使用委托的案例

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

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

  • 订阅器(subscriber) 是一个接受事件并提供事件处理器的对象。在发布器类中触发事件后,调用订阅器类中处理事件的方法。

下列代码演示了如何定义事件,如何订阅事件以及如何触发事件:

using System;

namespace EventDemo
{
    
    
    // 定义一个委托类型,用于处理事件(也可以把它定义在发布器类中)
    public delegate void EventHandler(object sender, EventArgs args);

    // 定义一个发布器类,它会触发事件
    public class Publisher
    {
    
    
        // 定义事件
        public event EventHandler MyEvent;

        // 触发事件的方法
        public void RaiseEvent()
        {
    
    
            Console.WriteLine("事件被触发了!");
            // 检查是否有订阅者( MyEvent是否为null),如果有则调用其事件处理器
            MyEvent?.Invoke(this, EventArgs.Empty);
        }
    }

    // 定义一个订阅器类,它会接收事件
    public class Subscriber
    {
    
    
        // 事件处理方法,需要和委托类型定义的签名一致
        public void HandleEvent(object sender, EventArgs args)
        {
    
    
            Console.WriteLine("事件被处理了!");
        }
    }

    class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            // 创建一个发布器实例
            Publisher pub = new Publisher();

            // 创建一个订阅器实例
            Subscriber sub = new Subscriber();

            // 订阅事件
            pub.MyEvent += sub.HandleEvent;

            // 触发事件
            pub.RaiseEvent();

            Console.ReadKey();
        }
    }
}

上述代码中,判断事件的委托变量 MyEvent 是否为 null ,其实就是在判定创建该变量后,有没有通过 += 订阅事件,为事件的委托变量添加可引用的事件处理器。同时因为事件在发布器内部是作为一个委托类型的变量被定义的,所以在类的外部要通过 实例名.事件名 来访问。

运行上面的代码会产生如下结果:

事件被触发了!
事件被处理了!

下面是一个简单的用于热水锅炉系统故障排除的应用程序,用于展示事件和委托的实际应用场景。当维修工程师检查锅炉时,锅炉的温度和压力会随着维修工程师的备注自动记录到日志文件中:

using System;
using System.IO;

namespace BoilerEventAppl
{
    
    
   // 锅炉类
   class Boiler
   {
    
    
      private int temp;      // 锅炉温度
      private int pressure;  // 锅炉压力

      // 构造函数,初始化温度和压力
      public Boiler(int t, int p)
      {
    
    
         temp = t;
         pressure = p;
      }

      // 获取锅炉温度
      public int getTemp()
      {
    
    
         return temp;
      }

      // 获取锅炉压力
      public int getPressure()
      {
    
    
         return pressure;
      }
   }

   // 事件发布器类
   class DelegateBoilerEvent
   {
    
    
      // 声明一个委托类型,用于指向处理 BoilerLog 事件的方法
      public delegate void BoilerLogHandler(string status);

      // 声明 BoilerLog 事件,事件类型为 BoilerLogHandler 委托
      public event BoilerLogHandler BoilerEventLog;

      // 发布 BoilerLog 事件,处理日志信息
      public void LogProcess()
      {
    
    
         string remarks = "O. K";
         Boiler b = new Boiler(100, 12);
         int t = b.getTemp();
         int p = b.getPressure();

         // 判断锅炉温度和压力是否正常,如果不正常,需要进行维修
         if (t > 150 || t < 80 || p < 12 || p > 15)
         {
    
    
            remarks = "Need Maintenance";
         }

         // 发布 BoilerLog 事件,传递日志信息
         OnBoilerEventLog("Logging Info:\n");
         OnBoilerEventLog("Temparature " + t + "\nPressure: " + p);
         OnBoilerEventLog("\nMessage: " + remarks);
      }

      // 触发 BoilerLog 事件,调用事件委托
      protected void OnBoilerEventLog(string message)
      {
    
    
         if (BoilerEventLog != null)
         {
    
    
            BoilerEventLog(message);
         }
         /* 也可以写成:BoilerEventLog?.Invoke(message); */
      }
   }

   // 该类保留写入日志文件的条款
   class BoilerInfoLogger
   {
    
    
      FileStream fs;
      StreamWriter sw;

      // 构造函数,打开日志文件
      public BoilerInfoLogger(string filename)
      {
    
    
         fs = new FileStream(filename, FileMode.Append, FileAccess.Write);
         sw = new StreamWriter(fs);
      }

      // 将日志信息写入文件
      public void Logger(string info)
      {
    
    
         sw.WriteLine(info);
      }

      // 关闭日志文件
      public void Close()
      {
    
    
         sw.Close();
         fs.Close();
      }
   }

   // 事件订阅器类
   public class RecordBoilerInfo
   {
    
    
      // 定义一个处理 BoilerLog 事件的方法,输出日志信息到控制台
      static void Logger(string info)
      {
    
    
         Console.WriteLine(info);
      }

      static void Main(string[] args)
      {
    
    
         // 创建一个文本日志记录器
         BoilerInfoLogger filelog = new BoilerInfoLogger("e:\\boiler.txt");
	    
         // 创建一个委托发布器对象
         DelegateBoilerEvent boilerEvent = new DelegateBoilerEvent();
	    
         // 向 BoilerLog 事件添加两个事件处理器,一个将日志信息输出到控制台,另一个将日志信息写入文件
         boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(Logger);
         boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(filelog.Logger);
	    
         // 触发事件并记录日志
         boilerEvent.LogProcess();
	    
         // 等待用户按下回车键后关闭文件日志
         Console.ReadLine();
         filelog.Close();
      }
   }
}

当上面的代码被编译和执行时,它会产生下列结果:

Logging info:


Temperature 100
Pressure 12


Message: O. K


C# 匿名方法

在C#中,匿名方法是一种没有名称只有主体的方法。使用匿名方法可以避免显式定义方法的麻烦,同时提供了一种灵活的方式来传递代码块作为委托参数。在匿名方法中我们也不需要指定返回类型,它是从方法主体内的 return 语句推断的。

  • 需要注意的是匿名方法必须与委托类型一起使用,否则编译器将会报错。(用法其实已经在委托部分介绍过了,下面主要是进行一些补充)

1、匿名方法的语法

匿名方法的语法与普通方法的语法非常相似,但是没有方法名和返回类型。在 C# 中,匿名方法可以使用 delegate 关键字来定义,其一般形式如下:

delegate (parameter_list) {
    
     anonymous_method_body }

其中,parameter_list 指定了方法的参数列表,anonymous_method_body 指定了方法的主体。

匿名方法可以使用已经定义的委托类型进行声明,也可以使用 var 关键字自动推断委托类型,例如:

delegate int Calculator(int x, int y);

Calculator add = delegate(int x, int y) {
    
     return x + y; };
var multiply = delegate(int x, int y) {
    
     return x * y; };

这里我们使用了 Calculator 委托类型来声明了一个 add 方法和使用 var 关键字推断出的 multiply 方法。两个方法都是匿名方法,没有方法名,但是都具有参数列表和方法体。

2、final 变量与闭包变量

在 C# 中,final 变量是指一旦赋值之后就不能再被修改的变量。在匿名方法中,final 变量被称为闭包变量。这是因为匿名方法可以访问其外部作用域中声明的变量,当匿名方法引用一个 final 变量时,它会创建一个闭包,以保存这个变量的引用。

当一个匿名方法引用了一个外部变量时,这个变量的生命周期将被延长,直到匿名方法执行完毕为止。这就是所谓的闭包行为。在匿名方法中,闭包变量是只读的,因为它们在方法执行期间不能被修改。

以下是一个使用闭包变量的匿名方法的示例:

class Program
{
    
    
    static void Main(string[] args)
    {
    
    
        int x = 5;
        Func<int, int> addX = delegate(int y) {
    
     return x + y; };
        Console.WriteLine(addX(3)); // 输出 8
        Console.WriteLine(addX(7)); // 输出 12
    }
}

在这个示例中,我们创建了一个名为 addX 的匿名方法,它使用了外部的变量 x。在匿名方法中,x 是一个闭包变量,因为它被匿名方法引用,并且它的值在匿名方法执行期间不能被修改。

需要注意的是,在匿名方法中使用的外部变量如果不是闭包变量,编译器将会报错

3、充当委托的参数

我们在前面提到匿名方法可以作为委托的参数传递,其实就是指用匿名方法替代了委托类型实例化时需要将被引用的方法名作为参数传递给实例对象,例如:

class Program
{
    
    
    delegate void Printer(string s);

	static void Method(string s)
    {
    
    
        Console.WriteLine(s);
    }

    static void Main()
    {
    
    
        Printer p = delegate(string s) {
    
     Console.WriteLine(s); };
        /* 可以替代以下代码:
        Printer p = Method; 或
        Printer p = new Printer(Method); */
        p("Hello, world!");
    }
}

这里我们定义了一个委托类型 Printer,然后在 Main 方法中使用匿名方法创建了一个 p 委托对象,最后调用 p 方法输出了一个字符串。

猜你喜欢

转载自blog.csdn.net/qq_50571974/article/details/129953638