C# Action 泛型委托使用方法 – wiki基地


深入探索 C# Action 泛型委托:定义、使用与实战场景

在 C# 编程语言中,委托(Delegate)扮演着至关重要的角色,它们是类型安全的回调机制,允许我们将方法作为参数传递、存储在变量中或从方法返回。在 .NET Framework 提供的众多预定义委托类型中,Action 系列泛型委托因其简洁性和广泛的应用场景而备受青睐。本文将深入探讨 Action<T> 及其泛型变体(Action<T1, T2>, …, Action<T1,...,T16>)的定义、核心概念、多种使用方式以及丰富的实战应用场景,旨在帮助开发者全面掌握这一强大的工具。

一、委托基础回顾:理解 Action<T> 的基石

在深入 Action<T> 之前,我们有必要简要回顾一下委托的基本概念。

什么是委托?

委托在 C# 中是一种引用类型,它可以持有对一个或多个方法的引用。可以将委托看作是方法的“指针”或“句柄”,但与 C/C++ 中的函数指针不同,C# 委托是类型安全的,并且是面向对象的。这意味着委托实例只能引用与其签名(参数类型列表和返回类型)相匹配的方法。

委托的核心作用:

  1. 回调机制: 允许一个方法在执行过程中调用由调用者提供的另一个方法。
  2. 事件处理: .NET 中的事件模型是基于委托构建的。
  3. 异步编程:Taskasync/await 模式中广泛用于表示异步操作完成后的回调。
  4. 代码解耦: 允许组件在不知道彼此具体实现的情况下进行交互。
  5. 行为参数化: 将算法或行为的一部分作为参数传递给方法。

二、Action 委托家族:专注无返回值的操作

.NET 类库提供了一系列内置的泛型委托类型,以简化常见委托模式的使用,避免了为每种方法签名都手动声明新委托类型的繁琐工作。Action 委托家族就是其中专注于封装不返回值(即返回 void)的方法的代表。

1. 非泛型 Action

最简单的形式是 System.Action,它表示一个不接受任何参数且不返回任何值的方法。

“`csharp
// 定义一个符合 Action 签名的方法
public static void PrintHello()
{
Console.WriteLine(“Hello, World!”);
}

// 声明并实例化 Action 委托
Action simpleAction = PrintHello; // 方法组转换

// 也可以使用 new 关键字(较少见)
// Action simpleAction = new Action(PrintHello);

// 调用委托引用的方法
simpleAction(); // 输出: Hello, World!
“`

2. 泛型 Action<T>

Action<T>Action 委托的第一个泛型版本。这里的 <T> 是一个类型参数,表示该委托引用的方法需要接受一个类型为 T 的参数,并且不返回值 (void)。

定义:

csharp
public delegate void Action<in T>(T obj);

注意这里的 in 关键字,它表示类型参数 T逆变 (Contravariant) 的。这意味着如果 Derived 类继承自 Base 类,那么 Action<Base> 类型的委托变量可以引用签名为 void Method(Derived obj) 的方法(因为接受 Derived 的方法自然也能处理 Base 对象,这里理解有误,逆变性是指参数类型可以更“泛化”,即 Action<Base> 可以赋值给 Action<Derived>,因为能处理 Base 的方法肯定能处理 Derived。而 Action 的参数类型是逆变的,所以 Action<Derived> 可以赋值给 Action<Base>。实际使用中,一个 Action<string> 委托可以指向一个接收 object 参数的方法,因为 string 可以隐式转换为 object)。更准确地说,逆变性允许你将一个方法签名参数类型更“基类”的方法赋值给参数类型更“派生”的委托变量(例如,Action<object> 可以赋值给 Action<string>,因为能处理 object 的方法自然也能处理 string)。(修正:应该是 Action<Base> 可以赋值给 Action<Derived>才对,能处理 Base 的方法一定能处理 Derived)。(再次修正理解:Action 的参数类型是逆变的,意味着 Action<父类> 类型的变量可以引用一个 Action<子类> 类型的实例。例如 Action<object> myAction = someStringAction; 是允许的,因为能处理 string 的方法可以被需要处理 object 的地方安全调用。反过来则不行。in 关键字标记的类型参数 T 是逆变的。)

核心用途: 封装需要接收单个参数并执行某个操作(无返回值)的方法。

3. 更多的泛型 Action 变体

为了处理需要多个参数的方法,.NET 提供了 Action<T1, T2>Action<T1, T2, ..., T16> 共 16 个泛型重载。

  • Action<T1, T2>: 封装需要两个参数(类型分别为 T1, T2)且返回 void 的方法。
  • Action<T1, T2, T3>: 封装需要三个参数(类型分别为 T1, T2, T3)且返回 void 的方法。
  • …以此类推,最多支持 16 个参数。

定义示例 (Action<T1, T2>):

csharp
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

这些泛型委托极大地提高了代码的可读性和复用性,使开发者无需为不同参数数量的 void 方法重复定义委托类型。

三、Action<T> 的声明、实例化与赋值

掌握如何创建和使用 Action<T> 委托实例是关键。以下是几种常见的方式:

1. 使用命名方法

可以将已定义的、签名匹配的静态方法或实例方法赋值给 Action<T> 委托变量。

“`csharp
public class Processor
{
// 静态方法
public static void LogMessage(string message)
{
Console.WriteLine($”[Log – Static]: {message}”);
}

// 实例方法
public void ProcessItem(int itemId)
{
    Console.WriteLine($"[Instance] Processing item with ID: {itemId}");
}

}

// — 使用 —
// 引用静态方法
Action logAction = Processor.LogMessage;
logAction(“System started.”); // 输出: [Log – Static]: System started.

// 引用实例方法 (需要先创建实例)
Processor myProcessor = new Processor();
Action processAction = myProcessor.ProcessItem;
processAction(101); // 输出: [Instance] Processing item with ID: 101

// 使用 new 关键字(语法上允许,但不常用)
Action logActionVerbose = new Action(Processor.LogMessage);
logActionVerbose(“Verbose log.”);
“`

C# 编译器支持方法组转换 (Method Group Conversion),可以直接将方法名赋值给兼容的委托变量,无需显式使用 new 关键字,代码更简洁。

2. 使用匿名方法

匿名方法是在 C# 2.0 引入的,允许在需要委托类型的地方以内联方式定义代码块。

“`csharp
// 使用匿名方法实例化 Action
Action printSquare = delegate (int number)
{
int square = number * number;
Console.WriteLine($”The square of {number} is {square}”);
};

printSquare(5); // 输出: The square of 5 is 25

// 匿名方法可以访问外部变量(形成闭包)
int multiplier = 10;
Action multiplyAndPrint = delegate (int x)
{
Console.WriteLine($”{x} * {multiplier} = {x * multiplier}”);
};

multiplyAndPrint(7); // 输出: 7 * 10 = 70
multiplier = 20; // 修改外部变量
multiplyAndPrint(7); // 输出: 7 * 20 = 140 (闭包捕获的是变量本身,不是值)
“`

匿名方法对于创建简单的、一次性的委托实例非常方便,但语法相对冗长。

3. 使用 Lambda 表达式 (最常用)

Lambda 表达式是在 C# 3.0 引入的,提供了一种更简洁、更强大的语法来创建匿名函数,是目前实例化委托(包括 Action<T>)的最常用方式。

基本语法: (parameters) => { statements; }(parameters) => expression

“`csharp
// Lambda 表达式实例化 Action
Action greet = (name) =>
{
Console.WriteLine($”Hello, {name}!”);
};
greet(“Alice”); // 输出: Hello, Alice!

// 如果只有一个参数,括号可以省略
Action farewell = name => Console.WriteLine($”Goodbye, {name}.”);
farewell(“Bob”); // 输出: Goodbye, Bob.

// 如果方法体只有一条语句,大括号和 return (对于 Func) 可以省略
// 对于 Action,如果只有一条语句,大括号也可以省略
Action printDouble = value => Console.WriteLine($”Double value: {value}”);
printDouble(3.14); // 输出: Double value: 3.14

// 使用 Lambda 表达式处理多个参数 (Action)
Action displayInfo = (id, category) =>
{
Console.WriteLine($”Item ID: {id}, Category: {category}”);
};
displayInfo(99, “Electronics”); // 输出: Item ID: 99, Category: Electronics

// Lambda 表达式同样可以形成闭包
string prefix = “DEBUG:”;
Action debugLog = message => Console.WriteLine($”{prefix} {message}”);
debugLog(“An error occurred.”); // 输出: DEBUG: An error occurred.
“`

Lambda 表达式因其简洁性、可读性和强大的闭包能力,已成为现代 C# 中使用委托(尤其是 Action<T>Func<T>)的首选方式。

四、调用 Action<T> 委托

调用 Action<T> 委托实例非常直接,就像调用普通方法一样,使用圆括号 () 并传入所需的参数。

“`csharp
Action processNumber = number => Console.WriteLine($”Processing {number}…”);

// 直接调用
processNumber(42); // 输出: Processing 42…

// 也可以使用 Invoke 方法(效果相同,但更明确)
processNumber.Invoke(99); // 输出: Processing 99…
“`

重要:处理 null 委托

委托是引用类型,其变量可能为 null。在调用委托之前,务必检查它是否为 null,否则会抛出 NullReferenceException

“`csharp
Action optionalAction = null; // 假设这个委托可能未被赋值

// 方式一:显式 null 检查
if (optionalAction != null)
{
optionalAction(“Safe call”);
}

// 方式二:使用 null 条件运算符 (?.) (C# 6.0 及以上)
optionalAction?.Invoke(“Even safer call”);
// 如果 optionalAction 为 null,?.Invoke(…) 表达式的结果为 null,整个调用被跳过,不会抛异常。
// 注意:必须使用 .Invoke() 才能配合 ?. 使用。直接 optionalAction?(“…”) 是无效语法。
“`

使用 null 条件运算符 ?.Invoke() 是现代 C# 中推荐的处理方式,代码更简洁、安全。

五、Action<T> 的实战应用场景

Action<T> 及其变体的应用非常广泛,是实现许多常见编程模式的基础。

1. 回调函数 (Callbacks)

这是 Action<T> 最经典的应用之一。当你调用一个耗时或异步的方法时,可以传递一个 Action<T> 委托作为回调,在操作完成时执行。

“`csharp
public class DataFetcher
{
// 模拟异步获取数据
public void FetchDataAsync(string url, Action onSuccess, Action onError)
{
Console.WriteLine($”Starting data fetch from: {url}”);
Task.Run(async () =>
{
try
{
// 模拟网络延迟和数据获取
await Task.Delay(1000);
if (url.Contains(“error”))
{
throw new InvalidOperationException(“Simulated network error”);
}
string data = $”Data from {url}: Content_{DateTime.Now.Ticks}”;
// 成功时调用 onSuccess 回调
onSuccess?.Invoke(data);
}
catch (Exception ex)
{
// 失败时调用 onError 回调
onError?.Invoke(ex);
}
});
}
}

// — 使用 —
DataFetcher fetcher = new DataFetcher();

// 定义成功和失败的回调逻辑 (使用 Lambda)
Action handleSuccess = data => Console.WriteLine($”SUCCESS: Received data – {data}”);
Action handleError = ex => Console.WriteLine($”ERROR: Fetch failed – {ex.Message}”);

fetcher.FetchDataAsync(“http://example.com/data”, handleSuccess, handleError);
fetcher.FetchDataAsync(“http://example.com/error”, handleSuccess, handleError);

// (程序需要保持运行以观察异步结果)
Console.WriteLine(“Fetch requests initiated…”);
// Console.ReadLine(); // 保持控制台打开
“`

在这个例子中,FetchDataAsync 方法接受两个 Action 委托:一个用于成功 (Action<string>),一个用于错误 (Action<Exception>)。调用者通过提供具体的 Lambda 表达式来定义数据获取成功或失败时的处理逻辑,实现了调用者与 DataFetcher 之间的解耦。

2. 参数化方法行为

Action<T> 允许你将一部分行为作为参数传递给方法,使得方法更加通用和灵活。

“`csharp
// 一个通用的列表处理器,处理方式由外部传入
public static void ProcessList(List items, Action processAction)
{
if (items == null || processAction == null) return;

foreach (T item in items)
{
    processAction(item); // 对每个元素执行传入的操作
}

}

// — 使用 —
List numbers = new List { 1, 2, 3, 4, 5 };
List names = new List { “Ana”, “Tom”, “Sue” };

// 使用不同的 Action 来处理不同类型的列表
Action printNumber = n => Console.WriteLine($”Number: {n}”);
Action printNameUpperCase = name => Console.WriteLine($”Name: {name.ToUpper()}”);

Console.WriteLine(“Processing numbers:”);
ProcessList(numbers, printNumber);
// 输出:
// Number: 1
// Number: 2
// …

Console.WriteLine(“\nProcessing names:”);
ProcessList(names, printNameUpperCase);
// 输出:
// Name: ANA
// Name: TOM
// Name: SUE
“`

ProcessList 方法本身不关心如何处理元素,它只负责迭代列表并调用传入的 processAction。这使得该方法可以重用于处理任何类型的列表,只要调用者提供适当的处理逻辑即可。

3. LINQ 中的 ForEach

List<T> 类提供了一个 ForEach 扩展方法,它接受一个 Action<T> 参数,用于对列表中的每个元素执行指定的操作。

“`csharp
List colors = new List { “Red”, “Green”, “Blue” };

// 使用 ForEach 和 Lambda 表达式打印每个颜色
colors.ForEach(color => Console.WriteLine($”Color found: {color}”));
// 输出:
// Color found: Red
// Color found: Green
// Color found: Blue

int sum = 0;
List values = new List { 10, 20, 30 };
// 使用 ForEach 计算总和 (利用闭包)
values.ForEach(v => sum += v);
Console.WriteLine($”Sum: {sum}”); // 输出: Sum: 60
“`

ForEachAction<T> 在集合操作中简洁应用的一个典型例子。

4. 简单的事件或通知机制

虽然 .NET 有标准的 event 关键字和 EventHandler / EventHandler<TEventArgs> 委托模式,但在一些简单场景下,可以使用 ActionAction<T> 来实现轻量级的通知。

“`csharp
public class Counter
{
private int _count;
public Action? CountChanged; // 使用 Action 作为通知委托

public int Count
{
    get => _count;
    set
    {
        if (_count != value)
        {
            _count = value;
            // 当计数值改变时,触发通知
            CountChanged?.Invoke(_count);
        }
    }
}

}

// — 使用 —
Counter myCounter = new Counter();

// 订阅通知 (注册回调)
myCounter.CountChanged += newCount => Console.WriteLine($”Counter changed to: {newCount}”);
myCounter.CountChanged += newCount =>
{
if (newCount > 5) Console.WriteLine(“Count exceeded 5!”);
};

myCounter.Count = 3; // 输出: Counter changed to: 3
myCounter.Count = 6; // 输出: Counter changed to: 6
// 输出: Count exceeded 5!

// 取消订阅 (如果需要)
Action handlerToRemove = newCount => Console.WriteLine($”Counter changed to: {newCount}”);
// 注意:直接用 Lambda 表达式无法精确移除,需要保存 Lambda 赋值的委托实例或使用命名方法。
// 更稳妥的方式是保存 Action 实例:
Action specificHandler = newCount => Console.WriteLine($”Specific handler: {newCount}”);
myCounter.CountChanged += specificHandler;
myCounter.Count = 7;
// …
myCounter.CountChanged -= specificHandler; // 移除特定的处理程序
myCounter.Count = 8; // 不会再调用 specificHandler
“`

注意: 直接使用 Action 作为公开的 “事件” 存在风险,因为它允许外部代码直接赋值 (=) 覆盖所有订阅者,或者直接调用 Invoke()。标准的 event 关键字提供了封装,只允许通过 +=-= 订阅/取消订阅,更安全。但在内部实现或受控环境中,Action 可以作为一种简化选择。

5. 配置或初始化

Action 可以用来传递配置逻辑。

“`csharp
public class ServiceConfigurator
{
public string ConnectionString { get; private set; } = “Default”;
public int TimeoutSeconds { get; private set; } = 30;

// 接受一个 Action<ServiceConfigurator> 来进行配置
public void Configure(Action<ServiceConfigurator> configureAction)
{
    configureAction?.Invoke(this); // 执行传入的配置逻辑
}

}

// — 使用 —
ServiceConfigurator configurator = new ServiceConfigurator();

configurator.Configure(cfg =>
{
cfg.ConnectionString = “Server=myServer;Database=myDb;”;
// TimeoutSeconds 保持默认
});
Console.WriteLine($”Configured ConnectionString: {configurator.ConnectionString}”);
Console.WriteLine($”Configured Timeout: {configurator.TimeoutSeconds}”);

configurator.Configure(cfg => cfg.TimeoutSeconds = 60); // 只修改 Timeout
Console.WriteLine($”Updated Timeout: {configurator.TimeoutSeconds}”);
“`

这种模式常见于构建器(Builder Pattern)或选项(Options Pattern)的配置中,允许以流畅、类型安全的方式设置对象的属性。

六、Action<T> vs. Func<T>

Action<T> 的近亲是 Func<T> 泛型委托家族。它们的主要区别在于:

  • Action<...>: 封装的方法必须返回 void。用于执行操作、产生副作用,不关心返回值。
  • Func<..., TResult>: 封装的方法必须返回一个值,其类型由最后一个泛型参数 TResult 指定。用于执行计算、查询数据并返回结果。

选择依据:

  • 如果你的方法需要执行一个动作(如打印、保存、修改状态)并且不需要返回任何结果,使用 Action
  • 如果你的方法需要进行计算或查询,并需要返回一个结果(如计算平方根、从数据库获取用户、检查条件是否满足),使用 Func

“`csharp
// Action: 执行一个操作
Action print = message => Console.WriteLine(message);
print(“Hello using Action”);

// Func: 计算并返回值
Func square = number => number * number;
int result = square(5);
Console.WriteLine($”Square using Func: {result}”); // 输出: Square using Func: 25

Func isNullOrEmpty = s => string.IsNullOrEmpty(s);
bool check = isNullOrEmpty(“”);
Console.WriteLine($”Is empty? {check}”); // 输出: Is empty? True
“`

七、性能考量

调用委托(包括 Action<T>)通常比直接调用方法有轻微的性能开销,因为涉及到一次额外的间接调用。然而,在绝大多数应用程序中,这种开销是微不足道的,可以忽略不计。Action<T> 及其变体是 .NET BCL(基类库)的一部分,其实现经过了高度优化。

只有在性能极其敏感的热路径(Hot Path)代码中,例如每秒需要执行数百万次的循环内部,才可能需要考虑委托调用与直接方法调用的细微差别。但在通常的业务逻辑、UI 处理、数据访问等场景下,Action<T> 带来的代码灵活性和可读性收益远超其微小的性能成本。

八、总结

Action<T> 及其泛型变体是 C# 中一组强大而灵活的预定义委托类型,专门用于封装不返回任何值 (void) 的方法。它们的核心优势在于:

  1. 简化委托声明: 无需为各种参数签名的 void 方法手动定义委托类型。
  2. 增强代码可读性: 使用 Lambda 表达式配合 Action<T> 可以写出非常简洁和表达力强的代码。
  3. 促进代码解耦: 实现回调、事件通知、策略模式等,降低组件间的依赖。
  4. 实现行为参数化: 使方法更加通用,可以将操作逻辑作为参数传递。
  5. 广泛应用于现代 C# 特性: 如 LINQ、异步编程 (Task)、依赖注入配置等。

掌握 Action<T> 的声明、实例化(特别是使用 Lambda 表达式)、调用(注意 null 检查)以及其适用的场景,对于编写优雅、灵活、可维护的 C# 代码至关重要。它是每一位 C# 开发者工具箱中不可或缺的一部分,能够显著提升代码设计水平和开发效率。当你需要传递“要做什么”而不是“返回什么”时,Action<T> 就是你的得力助手。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部