【C#】委托与匿名函数详解

C# 委托与匿名函数详解

目录

  1. 委托简介
  2. 委托的声明和使用
  3. 多播委托
  4. 匿名函数
  5. Lambda 表达式
  6. 常用委托类型
  7. 实际应用场景

委托简介

委托(Delegate) 是 C# 中的一种类型安全的函数指针,它允许你将方法作为参数传递,也可以将方法存储在变量中。委托提供了类型安全的回调机制。

为什么要使用委托?

  • 解耦合:将方法与调用方法分离
  • 灵活性:可以在运行时动态指定要调用的方法
  • 事件处理:C# 的事件系统基于委托实现
  • 回调机制:实现函数式编程风格

委托的声明和使用

声明委托

// 声明一个委托类型
delegate void MyDelegate(string message);
delegate int CalculateDelegate(int x, int y);

解析:
委托类似一种函数类型,在上面的声明中,MyDelegate代表一种:接收单个字符串参数,不返回数据的函数;CalculateDelegate则是一种计算类型的函数,接受两个整型参数,返回一个整型结果。在实例化的时候,委托方法就需要符合参数与结果的定义。

委托的特点

  • 委托是类型,不是实例
  • 委托定义了方法的签名(参数和返回类型)
  • 委托可以绑定到匹配签名的方法

基本使用示例

using System;

// 声明一个欢迎委托
delegate void GreetingDelegate(string name);

class Program
{
    // 定义方法
    static void SayHello(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }
    
    static void SayGoodbye(string name)
    {
        Console.WriteLine($"Goodbye, {name}!");
    }
    
    static void Main()
    {
        // 创建委托实例
        GreetingDelegate greet;
        
        // 绑定方法
        greet = SayHello;
        
        // 调用委托
        greet("张三"); // 输出: Hello, 张三!
        
        // 重新绑定
        greet = SayGoodbye;
        greet("李四"); // 输出: Goodbye, 李四!
    }
}

在上述代码中,实例化了一个委托变量 greet,但是并没有制定执行哪种方法。在命名空间内部存在SayHello和SayGoofBye两个方法,它们接受的参数与返回类型均符合委托的定义。因此,可以将委托的实例绑定到这两个方法上。

委托作为参数

delegate double MathOperation(double x, double y);

class Calculator
{
    static double Add(double x, double y) => x + y;
    static double Multiply(double x, double y) => x * y;
    
    static void ExecuteOperation(double a, double b, MathOperation operation)
    {
        double result = operation(a, b);
        Console.WriteLine($"结果: {result}");
    }
    
    static void Main()
    {
        ExecuteOperation(10, 5, Add);        // 输出: 结果: 15
        ExecuteOperation(10, 5, Multiply);   // 输出: 结果: 50
    }
}

在上面的示例代码中,MathOperation 是一个返回 double 类型、接收两个 double 参数的委托类型。Calculator 类中定义了两个与此委托签名一致的方法:AddMultiply。在 ExecuteOperation 方法中,第三个参数是一个委托类型,可以传入任何符合该签名的方法。这样做的好处是,可以根据需要灵活传递不同的算法实现,而不需要修改 ExecuteOperation 的内部逻辑。

Main 方法中,通过直接传递方法名(如 AddMultiply)给委托参数,实现了对不同数学操作的复用。程序运行时会输出传入对应的结果值。

这种做法体现了委托作为参数传递的强大能力——将方法作为“第一类对象”传递,使得代码更具扩展性和可维护性,也方便实现回调、事件等功能。


多播委托

委托可以绑定多个方法,调用委托时,所有绑定的方法都会被执行。

delegate void ProcessDelegate(string data);

class Program
{
    static void LogData(string data)
    {
        Console.WriteLine($"[日志] {data}");
    }
    
    static void SaveData(string data)
    {
        Console.WriteLine($"[保存] {data}");
    }
    
    static void Notify(string data)
    {
        Console.WriteLine($"[通知] {data}");
    }
    
    static void Main()
    {
        ProcessDelegate processor = LogData;
        
        // 使用 += 添加方法
        processor += SaveData;
        processor += Notify;
        
        // 调用时会执行所有方法
        processor("用户登录");
        
        // 输出:
        // [日志] 用户登录
        // [保存] 用户登录
        // [通知] 用户登录
        
        // 使用 -= 移除方法
        processor -= SaveData;
        processor("用户注销");
        
        // 输出:
        // [日志] 用户注销
        // [通知] 用户注销
    }
}

注意点

  • 委托方法按添加顺序执行
  • 使用 += 添加方法
  • 使用 -= 移除方法
  • 如果委托为 null 或没有方法,调用会抛出异常
// 安全的调用方式
processor?.Invoke("数据");

// 安全的调用方式解释:
// 当我们直接调用一个委托(如 processor(“数据”))时,如果委托为 null(即没有绑定任何方法),会抛出 NullReferenceException 异常。
// 使用 ?.Invoke 运算符可以避免这个异常。当 processor 不为 null 时才会调用,否则什么也不做。
// 这样可以让代码更加健壮,防止因委托未初始化而导致的程序崩溃。例如:

/*
ProcessDelegate processor = null;
// processor(“数据”); // ❶ 这行会抛出异常,因为 processor 为 null
processor?.Invoke(“数据”); // ❷ 这行不会抛异常,即使 processor 为 null
*/


匿名函数

匿名函数是不需要明确定义名称的方法,包括:

  1. 匿名方法(Anonymous Methods)
  2. Lambda 表达式(Lambda Expressions)

匿名方法

C# 2.0 引入的特性,使用 delegate 关键字创建无名的方法。

// 创建一个委托,定义方法签名
delegate int Operation(int x, int y);

class Program
{
    static void Main()
    {
        // 使用匿名方法
        Operation add = delegate(int x, int y)
        {
            return x + y;
        };
        
        Console.WriteLine(add(5, 3)); // 输出: 8
        
        // 当委托类型已明确时,可以省略参数类型
        Operation subtract = delegate(x, y) { return x - y; };
        Console.WriteLine(subtract(10, 4)); // 输出: 6
    }
}

上述代码演示了如何使用匿名方法(Anonymous Method)来创建没有名字的方法,并将其赋值给委托类型变量,从而可以像普通方法那样调用。
主要讲解了以下几点:

  1. 通过 delegate 关键字创建匿名方法,并赋值给一个委托变量,如 Operation add = delegate(int x, int y) { return x + y; };
  2. 匿名方法可以省略参数类型(由编译器推断),从而简化代码书写。
  3. 调用匿名方法非常直接,和调用委托绑定的普通方法一样:add(5, 3)
  4. 匿名方法通常用于临时、一次性使用的场景,比如集合的查找、排序条件,或事件的简单处理。
  5. 使用匿名方法可以减少代码量,提升代码的灵活性和可读性,无需单独为每个简单逻辑定义独立方法。

匿名方法的应用场景

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
        // 查找偶数
        List<int> evens = numbers.FindAll(
            delegate(int n) { return n % 2 == 0; }
        );
        
        foreach (int n in evens)
        {
            Console.Write($"{n} "); // 输出: 2 4 6 8 10
        }
        
        Console.WriteLine();
        
        // 使用匿名方法处理事件
        Action<string> handler = delegate(string msg)
        {
            Console.WriteLine($"处理: {msg}");
        };
        
        handler("事件触发");
    }
}

上述代码主要演示了匿名方法的实际应用场景,特别是如何在集合操作中使用匿名方法来实现逻辑。

以“查找偶数”这部分为例:

List<int> evens = numbers.FindAll(
    delegate(int n) { return n % 2 == 0; }
);

解释如下:

  1. numbers 是一个整数列表,包含了 1 到 10。
  2. FindAll 是 List 的一个方法,用于从列表中查找所有满足指定条件的元素。
  3. FindAll 方法需要一个“谓词”——即一个能够接受列表中元素并返回 bool 类型(true/false)的函数。
  4. 这里的 delegate(int n) { return n % 2 == 0; } 就是匿名方法。它的意思是:对于列表中的每个元素 n,如果 n 除以 2 的余数等于 0(即能被 2 整除),则返回 true——也就是偶数。
  5. FindAll 会把所有让该匿名方法返回 true 的元素收集到一个新的列表中,这里最终得到的 evens 就是一个只含有偶数的列表。

总结:

  • 这个匿名方法实际上就是一个“判断是不是偶数”的小函数。
  • FindAll 把它作用于 numbers 中的每个元素,筛选出所有偶数。

如果用传统方式写出来,代码相当于:

bool IsEven(int n)
{
    return n % 2 == 0;
}
List<int> evens = numbers.FindAll(IsEven);

用匿名方法可以让逻辑更加简洁地“内联”到调用处,不用单独定义一个新函数。

Lambda 表达式

Lambda 表达式是 C# 3.0 引入的简洁语法,用于创建匿名函数。它是写匿名方法的更简洁方式。

Lambda 表达式语法

// 基本语法: (parameters) => expression
// 或者:    (parameters) => { statements }

delegate int Calculate(int x, int y);

class Program
{
    static void Main()
    {
        // Lambda 表达式 - 单行
        Calculate add = (x, y) => x + y;
        Calculate multiply = (x, y) => x * y;
        
        Console.WriteLine(add(5, 3));      // 输出: 8
        Console.WriteLine(multiply(5, 3));  // 输出: 15
        
        // Lambda 表达式 - 多行(需要花括号和 return)
        Calculate complex = (x, y) =>
        {
            int result = x + y;
            return result * 2;
        };
        
        Console.WriteLine(complex(5, 3)); // 输出: 16
    }
}

Lambda 表达式的不同形式

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        
        // 形式1: 单个参数,括号可选
        var evens = numbers.Where(n => n % 2 == 0);
        
        // 形式2: 多个参数,必须使用括号
        var sum = numbers.Aggregate((a, b) => a + b);
        
        // 形式3: 无参数
        Func<string> getMessage = () => "Hello World";
        
        // 形式4: 显式指定参数类型
        var evens2 = numbers.Where((int n) => n % 2 == 0);
        
        // 形式5: 多行 Lambda
        var processed = numbers.Select(n =>
        {
            if (n % 2 == 0)
                return n * 2;
            else
                return n;
        });
        
        foreach (int n in processed)
        {
            Console.Write($"{n} "); // 输出: 1 4 3 8 5
        }
    }
}

Lambda 与闭包

Lambda 表达式可以捕获外部变量,形成闭包:

class Program
{
    static void Main()
    {
        int multiplier = 10;
        
        // Lambda 捕获外部变量 multiplier
        Func<int, int> multiply = x => x * multiplier;
        
        Console.WriteLine(multiply(5)); // 输出: 50
        
        multiplier = 20;
        Console.WriteLine(multiply(5)); // 输出: 100
        
        // 注意:闭包捕获的是变量本身,不是值
        Action<int> add = n => multiplier += n;
        add(5);
        Console.WriteLine(multiplier); // 输出: 25
    }
}

在以下代码片段中:

int multiplier = 10;
// Lambda 捕获外部变量 multiplier
Func<int, int> multiply = x => x * multiplier;
Console.WriteLine(multiply(5)); // 输出: 50
multiplier = 20;
Console.WriteLine(multiply(5)); // 输出: 100

形成了闭包。这里,Lambda 表达式 x => x * multiplier 并不仅仅读取multiplier当时的值,而是捕获了变量multiplier本身,即在 Lambda 被调用时,它会实时访问当前的multiplier变量。因此后续将multiplier修改为20后,multiply(5)的结果也随之改变。

什么是闭包?

闭包(Closure)是指:函数可以“捕获”并保存其所在外部作用域中的变量,即使这些变量的生命周期已经超出了其原始作用域,该函数依然可以访问和修改它们。

在C#中,Lambda表达式和匿名方法都能够形成闭包。当Lambda表达式引用了它方法外部的局部变量时,这些变量会随委托对象一起被“捕获”保存下来,并且生命周期延长至委托的生命周期。

简言之,闭包是“函数+其引用的外部变量状态”的组合,让函数能够带着上下文环境一起使用。

例如,上例中,multiply这个 Func<int, int> 委托对象和multiplier变量形成了一个闭包。

C# 与 Python 闭包对比

相似之处
  • 都允许函数捕获并访问外部作用域中的变量
  • 都延长被捕获变量的生命周期
  • 都支持读取和修改外部变量
关键差异

变量捕获时机:

// C# - 捕获的是变量本身(引用变量)
int multiplier = 10;
Func<int, int> multiply = x => x * multiplier;

Console.WriteLine(multiply(5));  // 50
multiplier = 20;
Console.WriteLine(multiply(5));  // 100(使用最新值)
# Python - 捕获的是变量在闭包创建时的值
def outer():
    multiplier = 10
    
    def inner(x):
        return x * multiplier
    
    return inner

func = outer()
print(func(5))  # 50

# 注意:C#中可以修改外部变量并影响闭包,
# Python中也有类似行为,但需要显式使用 nonlocal

装饰器应用:

Python中的装饰器确实是闭包最常见的应用之一:

def timer(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"耗时: {end - start}秒")
        return result
    return wrapper  # 返回闭包

@timer
def my_function():
    time.sleep(1)

my_function()  # 输出: 耗时: 1.0秒

C#中的等价实现:

// C#中使用闭包实现类似功能
Func<Action, Action> timer = (func) =>
{
    return () =>
    {
        var start = DateTime.Now;
        func();
        var end = DateTime.Now;
        Console.WriteLine($"耗时: {(end - start).TotalSeconds}秒");
    };
};

// 使用
Action myFunction = () => Thread.Sleep(1000);
Action timedFunction = timer(myFunction);
timedFunction();

总结:

  • C# 和 Python 的闭包本质相同,都是函数+外部变量的组合
  • Python 的装饰器是闭包最典型的应用场景
  • C# 中闭包更多用于事件处理、回调函数、LINQ 等场景

常用委托类型

.NET Framework 提供了一些预定义的委托类型:

Action 委托

用于无返回值的方法:

using System;

class Program
{
    static void Main()
    {
        // Action - 无参数
        Action greet = () => Console.WriteLine("Hello");
        
        // Action<T> - 一个参数
        Action<string> greet2 = name => Console.WriteLine($"Hello, {name}");
        
        // Action<T1, T2> - 两个参数
        Action<string, int> greet3 = (name, age) => 
            Console.WriteLine($"{name} is {age} years old");
        
        // 最多支持16个参数: Action<T1, ..., T16>
        
        greet();                           // 输出: Hello
        greet2("张三");                   // 输出: Hello, 张三
        greet3("李四", 25);               // 输出: 李四 is 25 years old
    }
}

Func 委托

用于有返回值的方法:

using System;

class Program
{
    static void Main()
    {
        // Func<TResult> - 无参数,返回 TResult
        Func<string> getMessage = () => "Hello World";
        
        // Func<T, TResult> - 一个参数,返回 TResult
        Func<int, int> square = x => x * x;
        
        // Func<T1, T2, TResult> - 两个参数,返回 TResult
        Func<int, int, int> add = (x, y) => x + y;
        
        // 最后一个类型参数是返回类型
        // Func<T1, ..., T16, TResult>
        
        Console.WriteLine(getMessage());  // 输出: Hello World
        Console.WriteLine(square(5));     // 输出: 25
        Console.WriteLine(add(3, 7));      // 输出: 10
    }
}

Predicate 委托

用于返回布尔值的委托:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
        // Predicate<T> 等同于 Func<T, bool>
        Predicate<int> isEven = n => n % 2 == 0;
        
        List<int> evens = numbers.FindAll(isEven);
        
        foreach (int n in evens)
        {
            Console.Write($"{n} "); // 输出: 2 4 6 8 10
        }
    }
}

Comparison 委托

用于比较两个对象:

using System;
using System.Collections.Generic;

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    
    public override string ToString() => $"{Name}, {Age}";
}

class Program
{
    static void Main()
    {
        List<Person> people = new List<Person>
        {
            new Person { Name = "张三", Age = 25 },
            new Person { Name = "李四", Age = 30 },
            new Person { Name = "王五", Age = 20 }
        };
        
        // 按年龄排序
        Comparison<Person> sortByAge = (p1, p2) => p1.Age.CompareTo(p2.Age);
        people.Sort(sortByAge);
        
        foreach (var person in people)
        {
            Console.WriteLine(person);
        }
        
        // 输出:
        // 王五, 20
        // 张三, 25
        // 李四, 30
    }
}

实际应用场景

1. 事件处理

委托是 C# 事件系统的基础:

using System;

// 定义事件发布者
class Button
{
    public event EventHandler Click;
    
    public void OnClick()
    {
        Click?.Invoke(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main()
    {
        Button button = new Button();
        
        // 使用 Lambda 表达式订阅事件
        button.Click += (sender, e) => 
        {
            Console.WriteLine("按钮被点击了!");
        };
        
        button.OnClick();
    }
}

2. LINQ 查询

LINQ 大量使用 Lambda 表达式:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<Person> people = new List<Person>
        {
            new Person { Name = "张三", Age = 25 },
            new Person { Name = "李四", Age = 30 },
            new Person { Name = "王五", Age = 20 }
        };
        
        // 使用 Lambda 表达式过滤和投影
        var youngPeople = people
            .Where(p => p.Age < 30)        // Lambda: 筛选
            .Select(p => p.Name)            // Lambda: 投影
            .ToList();
        
        foreach (var name in youngPeople)
        {
            Console.WriteLine(name);
        }
        
        // 输出: 张三, 王五
    }
}

3. 异步编程

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // 使用 Lambda 表达式创建异步委托
        Func<Task> asyncTask = async () =>
        {
            await Task.Delay(1000);
            Console.WriteLine("异步任务完成");
        };
        
        await asyncTask();
        
        // 使用 Lambda 处理异步结果
        Func<int, Task<int>> processAsync = async (n) =>
        {
            await Task.Delay(100);
            return n * 2;
        };
        
        int result = await processAsync(10);
        Console.WriteLine($"结果: {result}"); // 输出: 结果: 20
    }
}

4. 回调函数

using System;

class Downloader
{
    public delegate void ProgressCallback(int percentage);
    
    public void Download(ProgressCallback callback)
    {
        for (int i = 0; i <= 100; i += 10)
        {
            callback?.Invoke(i);
            System.Threading.Thread.Sleep(200);
        }
    }
}

class Program
{
    static void Main()
    {
        Downloader downloader = new Downloader();
        
        // 使用 Lambda 传递回调
        downloader.Download(percentage => 
            Console.WriteLine($"下载进度: {percentage}%")
        );
        
        // 或者使用局部变量
        int lastProgress = 0;
        downloader.Download(percentage =>
        {
            if (percentage - lastProgress >= 20)
            {
                Console.WriteLine($"已达到 {percentage}%");
                lastProgress = percentage;
            }
        });
    }
}

5. 策略模式

委托可以实现策略模式:

using System;

// 策略接口(使用委托)
delegate double PricingStrategy(double price);

class Product
{
    public string Name { get; set; }
    public double Price { get; set; }
    
    public double GetFinalPrice(PricingStrategy strategy)
    {
        return strategy(Price);
    }
}

class Program
{
    static void Main()
    {
        Product product = new Product { Name = "笔记本电脑", Price = 10000 };
        
        // 定义不同的策略
        PricingStrategy normalPrice = p => p;
        PricingStrategy discount10 = p => p * 0.9;
        PricingStrategy discount20 = p => p * 0.8;
        PricingStrategy vipPrice = p => p * 0.7;
        
        Console.WriteLine($"原价: {product.GetFinalPrice(normalPrice)}");
        Console.WriteLine($"9折: {product.GetFinalPrice(discount10)}");
        Console.WriteLine($"8折: {product.GetFinalPrice(discount20)}");
        Console.WriteLine($"VIP价: {product.GetFinalPrice(vipPrice)}");
    }
}

最佳实践

1. 使用空条件运算符

// 推荐
myDelegate?.Invoke();

// 不推荐
if (myDelegate != null)
{
    myDelegate();
}

2. 避免空委托

// 推荐:初始化为空委托
Action<int> handler = _ => { };

handler += n => Console.WriteLine(n);
handler(5);

3. Lambda vs 方法组转换

// 当只传递方法时,使用方法组更简洁
Action<int> handler1 = Console.WriteLine;

// 需要参数转换时使用 Lambda
Action<string> handler2 = s => Console.WriteLine(int.Parse(s));

4. 委托的可读性

// 复杂逻辑使用命名方法而不是 Lambda
Func<int, int, int> complexCalculation = CalculateComplexValue;

static int CalculateComplexValue(int x, int y)
{
    // 复杂的计算逻辑
    int result = 0;
    // ... 更多代码
    return result;
}

总结

  • 委托提供了类型安全的函数指针机制
  • 多播委托允许一个委托调用多个方法
  • 匿名方法Lambda 表达式提供了更灵活的代码组织方式
  • ActionFunc 是 .NET 提供的最常用的泛型委托
  • 委托广泛应用于事件处理、LINQ、异步编程等场景

掌握委托和 Lambda 表达式是编写现代 C# 代码的基础技能!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值