C# 委托、Lambda表达式和事件

1. 引用方法

委托是寻址方法的 .NET 本。在 C++ 中,函数指针只不过是一个指向内存位置的指针,它不是类型安全的。我们无法判断这个指针实际指向什么,像参数和返回类型等项就更无从知晓了。而 .NET 委托完全不同;委托是类型安全的类,它定义了返回类型和参数的类型。委托类不仅包含对方法的引用,也可以包含对多个方法的引用。

lambda 表达式与委托直接相关。当参数是委托类型时,就可以使用 lambda 表达式实现委托引用的方法。

2. 委托

委托只是一种特殊类型的对象,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是一个或多个方法的地址。

在C#中,委托(Delegate)是一种引用类型,它允许开发人员将方法作为参数传递、存储和调用。委托提供了一种灵活的方式来处理回调函数、事件处理和异步编程等场景。

以下是关于C#中委托的详细介绍:

2.1 定义委托类型

在C#中,首先需要定义委托类型,它定义了委托可以引用的方法的签名。委托的定义类似于一个方法的声明,但没有实现体。委托定义可以放在命名空间、类或结构体内部。例如:

// 定义了一个名为 MyDelegate 的委托,它可以引用一个带有一个 int 类型参数并且没有返回值的方法
delegate void MyDelegate(int x);

// 其他例子:
// 定义一个委托 IntMethodInvoker,该委托表示的方法带一个 int 类型的参数,无返回类型
delegate void IntMethodInvoker(int x);
// 定义一个委托 TwoLongsOp,该委托表示的方法有两个 long 型参数,返回类型为 double
delegate double TwoLongsOp(long first, long second);
// 定义一个委托,它表示的方法不带参数,返回一个string型的值
delegate string GetAString();

上述代码定义了一个名为MyDelegate的委托,它可以引用一个带有一个int类型参数并且没有返回值的方法。

注意:实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生自基类 System.MulticastDelegate 的类,System.MulticastDelegate 又派生自基类 System.Delegate。C# 编译器能识别这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况。这是 C# 与基类共同合作以使编程更易完成的另一个范例。

2.2 创建委托实例

一旦定义了委托类型,就可以创建委托的实例。委托实例将引用一个或多个具有与委托签名匹配的方法。可以使用new关键字和委托类型的构造函数来创建委托的实例。例如:

MyDelegate del = new MyDelegate(MyMethod);

上述代码创建了一个名为del的委托实例,它引用了一个名为MyMethod的方法,该方法具有与MyDelegate委托类型相匹配的签名。

下面的代码段说明了如何使用委托。这是在 int 值上调用 ToString() 方法的一种相当冗长的方式:

private delegate string GetAString();
public static void Main()
{
    int x = 40;
    GetAString firstStringMethod = new GetAString(x.ToString);
    Console.WriteLine($"String is {firstStringMethod()}");
    // 上面这个语句相当于
    // Console.WriteLine($"String is {x.ToString()}");
}

在这段代码中,实例化类型为 GetAString 的委托,并对它进行初始化,使其引用整型变量 x 的 ToString() 方法。在 C# 中,委托在语法上总是接受一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中,如果不用不带参数并返回一个字符串的方法来初始化 firstStringMethod 变量,就会产生一个编译错误。注意,因为 int.ToString() 是一个实例方法(不是静态方法),所以需要指定实例(x)和方法名来正确地初始化委托。

下一行代码使用这个委托来显示字符串。在任何代码中,都应提供委托实例的名称,后面的圆括号中应包含调用该委托中的方法时使用的任何等效参数。所以在上面的代码中,Console.WriteLine() 语句完全等价于注释掉的代码行。

实际上,给委托实例提供圆括号与调用委托类的 Invoke() 方法完全相同。因为 firstStringMethod 是委托类型的一个变量,所以 C# 编译器会用 firstStringMethod.Invoke() 代替 firstStringMethod()。

firstStringMethod();
// firstStringMethod.Invoke();

为了减少输入量,在需要委托实例的每个位置可以只传送地址的名称。这称为委托推断。只要编译器可以把委托实例解析为特定的类型,这个 C# 特性就是有效的。下面的示例用 GetAString 委托的一个新实例初始化 GetAString 类型的 firstStringMethod 变量:

GetAString firstStringMethod = new GetAString(x.ToString);

只要用变量 x 把方法名传送给变量 firstStringMethod,就可以编写出作用相同的代码:

GetAString firstStringMethod = x.ToString;

C# 编译器创建的代码是一样的。由于编译器会用 firstStringMethod 检测需要的委托类型,因此它创建 GetAString委托类型的一个实例,用对象 x 把方法的地址传送给构造函数。

注意:调用上述方法名时,输入形式不能为 x.ToString()(不要输入圆括号),也不能把它传送给委托变量。输入圆括号会调用一个方法,而调用 x.ToString() 方法会返回一个不能赋予委托变量的字符串对象。只能把方法的地址赋予委托变量。

2.3 委托的调用

委托实例可以通过调用委托来间接调用其所引用的方法。使用委托调用时,可以像调用方法一样提供参数。例如:

delegate void MyDelegate(int x);
MyDelegate del = new MyDelegate(MyMethod);
del(10);

上述代码将调用del委托所引用的方法,并将整数值10作为参数传递给该方法。

2.4 委托的多播

委托可以引用一个或多个方法,这称为委托的多播(Multicast)功能。通过使用+-运算符,可以将一个委托实例与另一个委托实例进行组合或移除。组合多个委托时,它们将按照添加顺序依次调用。例如:

MyDelegate del1 = new MyDelegate(Method1);
MyDelegate del2 = new MyDelegate(Method2);
MyDelegate del3 = del1 + del2; // 组合委托
del3(10); // 调用组合后的委托
del3 -= del2; // 移除委托

上述代码将del1del2两个委托实例组合成一个新的委托del3,然后调用del3委托。接着,从del3委托中移除了del2委托。

using System;

delegate void MyDelegate(string message);

class Program
{
    static void Main()
    {
        MyDelegate myDelegate = null;

        myDelegate += Method1;
        myDelegate += Method2;
        myDelegate += Method3;

        myDelegate("Hello, world!");

        Console.ReadLine();
    }

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

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

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

/*
Method1: Hello, world!
Method2: Hello, world!
Method3: Hello, world!
*/

在上述示例中,我们定义了一个名为MyDelegate的委托类型,它接受一个字符串参数并返回void。然后,我们创建了一个myDelegate委托实例,并使用+=运算符将三个不同的方法Method1Method2Method3添加到委托中。

当我们调用myDelegate委托并传递字符串参数"Hello, world!"时,实际上会依次调用添加到委托中的三个方法,并将相同的消息打印到控制台上。

2.5 内置委托类型

C# 中还提供了一些内置的委托类型,可以直接使用,而无需自定义委托类型。一些常用的内置委托类型包括:

  • Action:不返回值的委托类型。

  • Func:带有返回值的委托类型。

  • Predicate:返回布尔值的委托类型。

这些内置委托类型具有不同数量和类型的参数,可以根据需要选择使用。

委托在C#中是一种强大的工具,它可以用于实现事件处理、回调函数、异步编程等各种场景。通过委托,可以将方法视为第一类对象,并在运行时动态地引用、组合和调用这些方法。

2.6 Action、Func 和 Predicate 委托

2.6.1 Action

除了为每个参数和返回类型定义一个新委托类型之外,还可以使用Action<T>Func<T>委托。泛型Action<T>委托表示引用一个 void 返回类型的方法。这个委托类存在不同的变体,可以传递至多16种不同的参数类型。没有泛型参数的 Action 类可调用没有参数的方法。Action<in T>调用带一个参数的方法,Action<in T1, in T2>调用带两个参数的方法,Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8>调用带8个参数的方法。

Action<int> myAction = (x) => Console.WriteLine(x);
myAction(10); // 调用委托,输出10

上述代码定义了一个名为myActionAction<int>委托实例,它引用一个具有一个int参数的方法,并通过Lambda表达式将该方法实现为输出参数值。

可以使用Action<T>委托来执行没有返回值的操作,例如事件处理、回调函数等。

2.6.2 Func

Func<T>委托可以以类似的方式使用。Func<T>允许调用带返回类型的方法。与Action<T>类似,Func<T>也定义了不同的变体,至多也可以传递16个参数类型和一个返回类型。Func<out TResult>委托类型可以调用带返回类型且无参数的方法,Func<in T, out TResult>调用带一个参数的方法,Func<in T1, in T2, in T3, in T4, out TResult>调用带4个参数的方法。

Func<int, int> myFunc = (x) => x * 2;
int result = myFunc(10); // 调用委托,结果为20

上述代码定义了一个名为myFuncFunc<int, int>委托实例,它引用一个具有一个int参数并返回int类型值的方法。通过Lambda表达式,我们将该方法实现为将输入参数乘以2并返回结果。

可以使用Func<T>委托来执行具有返回值的操作,例如计算、转换等。

2.6.3 Predicate 委托

Predicate<T>是一个泛型委托类型,用于表示一个返回布尔值的方法,该方法接受一个泛型参数。Predicate<T>委托通常用于在集合或数组中进行元素匹配或筛选。

Predicate<T>委托定义如下:

public delegate bool Predicate<in T>(T obj);

其中,T是要进行匹配或筛选的元素的类型。

下面是一个示例,演示了如何使用Predicate<T>委托进行元素筛选:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

Predicate<int> predicate = (x) => x % 2 == 0; // 筛选偶数

List<int> evenNumbers = numbers.FindAll(predicate);

foreach (int number in evenNumbers)
{
    Console.WriteLine(number);
}

在上述示例中,我们创建了一个名为numbers的整数列表,并定义了一个Predicate<int>委托实例predicate,使用Lambda 表达式筛选偶数。

然后,我们使用FindAll方法,该方法接受一个Predicate<T>委托作为参数,并返回符合条件的元素组成的新列表。我们将predicate委托传递给FindAll方法,以筛选出列表中的偶数。

最后,我们遍历筛选后的结果列表evenNumbers,并将每个偶数打印出来。

2.7 匿名方法

在 C# 中,匿名方法是一种可以在代码中直接定义的方法,它没有显式的方法名称,通常用于简化代码或在需要时提供一次性的逻辑。匿名方法可以作为委托类型的实例,用于传递给接受委托参数的方法或构造函数。

以下是匿名方法的一般语法:

delegate (parameters)
{
    // 匿名方法的代码逻辑
};

在匿名方法的定义中,可以指定参数列表和方法体。匿名方法可以使用外部作用域中的变量,并且它们具有与外部作用域相同的访问权限。

下面是一个示例,演示了如何使用匿名方法:

class Program
{
    delegate void MyDelegate(string message);

    static void Main()
    {
        MyDelegate myDelegate = delegate (string message) { Console.WriteLine("Hello, " + message); };

        myDelegate("world");

        Console.ReadLine();
    }
}

// Hello, world

在上述示例中,我们定义了一个名为MyDelegate的委托类型,它接受一个字符串参数并返回void。然后,我们创建了一个myDelegate委托实例,并使用匿名方法来定义委托的行为。

匿名方法的代码逻辑是在委托调用时执行的。在此示例中,匿名方法将传递的字符串参数与固定的前缀连接起来,并将结果打印到控制台。

通过使用匿名方法,我们可以直接在代码中定义简单的逻辑,而无需单独声明具名方法。这对于需要一次性使用的简单逻辑非常有用,可以减少代码量并提高可读性。匿名方法通常用于事件处理、LINQ查询、异步编程等场景中。

在使用匿名方法时,必须遵循两条规则。

  • 在匿名方法中不能使用跳转语句(break、goto或continue)跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。

  • 在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。

如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法。此时与复制代码相比,编写一个命名方法比较好,因为该方法只需要编写一次,以后可通过名称引用它。

注意:匿名方法的语法在 C# 2 中引入。在新的程序中,并不需要这个语法,因为 lambda 表达式提供了相同的功能,还提供了其他功能。但是,在已有的源代码中,许多地方都使用了匿名方法,所以最好了解它。从 C# 3.0 开始,可以使用 lambda 表达式。

3. Lambda表达式

自 C# 3.0 开始,就可以使用一种新语法把实现代码赋予委托:lambda 表达式。只要有委托参数类型的地方,就可以使用 lambda 表达式。前面使用匿名方法的例子可以改为使用 lambda 表达式。

class Program
{
    delegate void MyDelegate(string message);

    static void Main()
    {
        // MyDelegate myDelegate = delegate (string message) { Console.WriteLine("Hello, " + message); };
        MyDelegate myDelegate = message => Console.WriteLine("Hello, " + message);

        myDelegate("world");

        Console.ReadLine();
    }
}

lambda 运算符“=>”的左边列出了需要的参数,而其右边定义了赋予 lambda 变量的方法的实现代码。

3.1 Lambda 表达式示例

在 C# 中,Lambda 表达式是一种用于创建匿名函数的简洁语法。Lambda 表达式可以用于定义委托或函数式接口的实例,使代码更加简洁和可读。

Lambda 表达式的语法如下:

(parameters) => expression

其中,parameters 是一个逗号分隔的参数列表,可以包含类型或隐式类型推断,而 expression 则是 Lambda 表达式的主体。

以下是一些Lambda表达式的示例:

  • Lambda 表达式作为参数传递给方法:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 使用 Lambda 表达式作为参数传递给 Where 方法
List<int> evenNumbers = numbers.Where(x => x % 2 == 0).ToList();
  • Lambda 表达式作为委托类型的实例:
// 委托类型定义
delegate int CalculateSquare(int x);

// 使用 Lambda 表达式创建委托实例
CalculateSquare square = x => x * x;

int result = square(5);  // 输出 25
  • Lambda 表达式作为比较器传递给排序方法:
List<string> names = new List<string> { "John", "Alice", "Bob", "David" };

// 使用 Lambda 表达式作为比较器传递给 Sort 方法
names.Sort((a, b) => a.CompareTo(b));

// 输出排序后的结果:Alice, Bob, David, John
foreach (string name in names)
{
    Console.WriteLine(name);
}

Lambda 表达式可以捕获外部变量,这使得它们非常灵活。捕获的变量可以在 Lambda 表达式内部使用,就像它们在表达式范围内声明的一样。

int multiplier = 2;

// 使用 Lambda 表达式计算每个元素的乘积
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
List<int> multipliedNumbers = numbers.Select(x => x * multiplier).ToList();

// 输出结果:2, 4, 6, 8, 10
foreach (int number in multipliedNumbers)
{
    Console.WriteLine(number);
}

Lambda 表达式是 C# 中强大且常用的功能之一,它简化了许多常见的编码任务,例如集合筛选、排序和转换。通过使用 Lambda 表达式,可以编写更简洁、更具可读性的代码。

3.2 参数

Lambda 表达式有几种定义参数的方式。如果只有一个参数,只写出参数名就足够了。下面的 Lambda 表达式使用了参数 s。因为委托类型定义了一个 string 参数,所以 s 的类型就是 string。

Func<string, string> oneParam = s => $"change uppercase {s.ToUpper()}";
Console.WriteLine(OneParam("test"));

如果委托使用多个参数,就把这些参数名放在花括号中。这里参数 x 和 y 的类型是 double,由Func<double, double, double>委托定义:

Func<double, double, double> towParams = (x, y) => x * y;
Console.WriteLine(twoParams(3, 2));

为了方便起见,可以在花括号中给变量名添加参数类型。如果编译器不能匹配重载后的版本,那么使用参数类型可以帮助找到匹配的委托:

Func<double, double, double> towParamsWithTypes = (double x, double y) => x * y;
Console.WriteLine(towParamsWithTypes(4, 2));

3.3 多行代码

如果 Lambda 表达式只有一条语句,在方法块内就不需要花括号和 return 语句,因为编译器会添加一条隐式的 return 语句:

Func<double, double> square = x => x * x;

添加花括号、return 语句和分号是完全合法的,通常这比不添加这些符号更容易阅读:

Func<double, double> square = x =>
{
    return x * x;
}

但是,如果在 Lambda 表达式的实现代码中需要多条语句,就必须添加花括号和 return 语句:

Func<string, string> lambda = param =>
{
    param += mid;
    param += " and this was added to the string.";
    return param;
};

3.4 闭包

在C#中,闭包(Closure)是一种特殊的函数对象,它可以捕获在其定义范围外部的变量,并在函数体内部使用这些变量。简单来说,闭包允许在函数中访问函数外部的变量。

闭包在 C# 中的主要应用场景是通过委托或 Lambda 表达式来创建匿名函数,并在函数内部使用外部的变量。当一个函数捕获了外部的变量时,这些变量的生命周期会得到延长,即使在变量的作用域已经结束之后,闭包仍然可以使用这些变量。

下面是一个简单的示例来说明闭包的概念:

using System;

class Program
{
    static Func<int, int> CreateMultiplier(int factor)
    {
        // 创建闭包,捕获外部变量 factor
        return x => x * factor;
    }

    static void Main()
    {
        int multiplier = 5;

        // 创建闭包实例
        var multiplyByFive = CreateMultiplier(multiplier);

        Console.WriteLine(multiplyByFive(3)); // 输出 15

        multiplier = 10; // 修改外部变量的值

        Console.WriteLine(multiplyByFive(3)); // 输出 30,闭包仍然使用旧的变量值
    }
}

在上面的示例中,CreateMultiplier函数创建了一个闭包,并将外部的变量factor捕获在闭包内部。然后,我们调用CreateMultiplier函数并传入一个值为5的multiplier变量,它将返回一个可以将输入乘以5的函数。我们将这个函数赋值给multiplyByFive变量。

接着,我们调用multiplyByFive函数并传入3作为参数,它将返回15。然后,我们修改了外部的变量multiplier的值为10,但闭包仍然使用旧的变量值,因此再次调用multiplyByFive函数并传入3,它将返回30。

这个示例展示了闭包如何在函数内部捕获并使用外部的变量,并且闭包保留了捕获变量的状态,即使这些变量在创建闭包后发生了改变。

闭包的使用可以带来很多好处,例如在事件处理程序、LINQ查询和异步编程中都能够灵活地应用。但要注意闭包可能导致内存泄漏的问题,因为闭包会持有对外部变量的引用,使得这些变量的生命周期得到延长。在使用闭包时,需要注意变量的生命周期和垃圾回收的问题,确保不会造成资源的浪费或泄漏。

4. 事件

在 C# 中,事件是一种基于委托的机制,用于实现发布和订阅模式。它允许对象在发生特定事件时通知其他对象,并且其他对象可以注册为事件的监听器,以便在事件发生时执行相应的操作。

C#中的事件主要涉及三个方面:事件发布(event publishing)、事件监听(event subscribing)和弱事件(weak events)。

  • 事件发布

事件发布是指在发生特定条件或动作时,通知所有订阅该事件的监听器。在 C# 中,通常使用event关键字定义事件,该关键字用于声明一个事件,并且事件通常由委托类型表示。

public event EventHandler MyEvent;

上面的代码定义了一个名为MyEvent的事件,它是一个EventHandler类型的委托。其他对象可以通过订阅该事件来接收通知。

在类中触发事件的代码通常遵循以下模式:

protected virtual void OnMyEvent(EventArgs e)
{
    MyEvent?.Invoke(this, e);
}

在发生特定条件时,调用OnMyEvent方法来触发事件。这会通知所有订阅了MyEvent事件的监听器。

  • 事件监听

事件监听是指对象注册为事件的订阅者,以便在事件发生时执行相应的操作。在 C# 中,可以通过委托或 Lambda 表达式来定义事件的处理程序,然后使用+=操作符将其附加到事件上。

myObject.MyEvent += MyEventHandler;

上面的代码将名为MyEventHandler的事件处理程序附加到myObject对象的MyEvent事件上。

事件处理程序的签名通常与事件的委托类型相匹配:

void MyEventHandler(object sender, EventArgs e)
{
    // 处理事件的逻辑
}

当事件发生时,所有注册的事件处理程序都会被依次调用。

  • 弱事件

弱事件是一种解决事件监听器与事件发布者之间潜在内存泄漏问题的机制。当一个对象订阅了事件,但没有显式地取消订阅时,事件发布者将保留对订阅者的引用,导致订阅者无法被垃圾回收。

为了避免这种情况,可以使用弱事件模式。弱事件利用弱引用(Weak Reference)来存储事件监听器的引用,从而避免对监听器的显式引用,并允许垃圾回收器在不再需要监听器时将其回收。

.NET Framework中的WeakEventManager类提供了一个通用的框架来实现弱事件模式。

这是一个简单的示例,演示如何使用弱事件模式:

public class EventPublisher
{
    private WeakEventManager _eventManager = new WeakEventManager();

    public event EventHandler MyEvent
    {
        add => _eventManager.AddEventHandler(value);
        remove => _eventManager.RemoveEventHandler(value);
    }

    public void RaiseEvent()
    {
        _eventManager.HandleEvent(this, EventArgs.Empty, nameof(MyEvent));
    }
}

public class EventListener
{
    public EventListener(EventPublisher publisher)
    {
        publisher.MyEvent += HandleEvent;
    }

    private void HandleEvent(object sender, EventArgs e)
    {
        Console.WriteLine("Event handled");
    }
}

public class Program
{
    public static void Main()
    {
        EventPublisher publisher = new EventPublisher();
        EventListener listener = new EventListener(publisher);

        publisher.RaiseEvent(); // 输出 "Event handled"

        // 在没有其他引用的情况下,listener 可以被垃圾回收
        listener = null;
        GC.Collect();

        publisher.RaiseEvent(); // listener 已被垃圾回收,不再处理事件
    }
}

在上面的示例中,EventPublisher是一个事件发布者,EventListener是一个事件监听器。通过使用WeakEventManager,订阅MyEvent事件的监听器将以弱引用的方式存储。当没有其他引用指向监听器时,垃圾回收器可以回收它,从而避免内存泄漏。

这些是C#中事件的基本概念和使用方式。事件提供了一种松耦合的通信机制,使对象之间可以以一种灵活的方式进行交互。通过事件,对象可以发布消息,并让其他对象对这些消息做出响应,从而实现高效的应用程序设计。

#知识点##技术分享##学习笔记##学习##CSharp#
C# 知识库 文章被收录于专栏

C#知识库用于学习和复习C#知识~

全部评论

相关推荐

4 9 评论
分享
牛客网
牛客企业服务