C#语言中的 Action 和 Func 委托使用详解

作者:微信公众号:【架构师老卢】
6-8 11:38
63

概述:委托是 C# 编程中最重要和最强大的功能之一。它们允许我们将方法作为参数传递,将方法存储为变量,并创建自定义事件和回调。在本文中,我们将探讨两种特殊类型的委托:Action 和 Func。这些委托在 .NET 中预定义,可以简化代码并使其更具可读性和可维护性。了解 C 语言中的委托#委托是表示对方法的引用的类型。委托可以指向与委托具有相同签名的任何方法,这意味着相同数量的参数和类型以及相同的返回类型。委托还可以指向多个方法,形成委托链或多播委托。声明委托的基本语法:// Declare a delegate type delegate return type delegate name (p

委托是 C# 编程中最重要和最强大的功能之一。它们允许我们将方法作为参数传递,将方法存储为变量,并创建自定义事件和回调。在本文中,我们将探讨两种特殊类型的委托:Action 和 Func。这些委托在 .NET 中预定义,可以简化代码并使其更具可读性和可维护性。

了解 C 语言中的委托#

委托是表示对方法的引用的类型。委托可以指向与委托具有相同签名的任何方法,这意味着相同数量的参数和类型以及相同的返回类型。委托还可以指向多个方法,形成委托链或多播委托。

声明委托的基本语法:

// Declare a delegate type  
delegate <return type> <delegate name> (<parameters>);  
  
// Example of a delegate that can point to any method that takes an int and returns a string  
delegate string IntToString(int x);

要使用委托,我们需要创建它的实例并为其分配一个方法。我们可以直接使用方法名称,也可以使用关键字并将方法名称作为参数传递。我们还可以使用匿名方法或 lambda 表达式来创建委托实例:

// A method that takes an int and returns a string  
static string IntToStringMethod(int number) {  
    return "The number is " + number;  
}// Create a delegate instance and assign it a method  
  
// using method name  
IntToString myDelegate = IntToStringMethod;  
  
//or using new keyword  
IntToString myDelegate = new IntToString(IntToStringMethod);// Invoke the delegate  
// calls IntToStringMethod(10) and returns "The number is 10"  
string result = myDelegate(10);// Create a delegate instance using an anonymous method  
IntToString myDelegate = delegate(int x) {  
    return "The number is " + x;  
};// Create a delegate instance using a lambda expression  
IntToString myDelegate = x => "The number is " + x;

以上是声明和使用委托的基本语法。我们可以将委托用于各种目的:

  • 将方法作为参数传递给其他方法。例如,我们可以使用委托将比较方法传递给排序方法,或将谓词方法传递给过滤方法。
  • 将方法存储为变量或字段。例如,我们可以使用委托来存储稍后将调用的回调方法,或者可以在运行时更改的方法。
  • 创建自定义事件和事件处理程序。例如,我们可以使用委托来定义在发生某些事情时将引发的事件,并附加将处理该事件的方法。

让我们更详细地探讨操作委托和功能委托。

操作委托

操作委托是预定义的委托,可以指向任何采用零个或多个参数并返回 void(无值)的方法。操作委托在 System 命名空间中定义,并具有以下通用形式:

// Action delegate with no parameters  
public delegate void Action();  
  
// Action delegate with one parameter  
public delegate void Action<T>(T obj);  
  
// Action delegate with two parameters  
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);  
  
// Action delegate with three parameters  
public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);  
  
// ... and so on, up to 16 parameters

以上是声明 Action 委托的基本语法。我们还可以使用关键字让编译器推断委托类型,并避免显式类型声明。使用 Action 委托的优点是,我们不需要为返回 void 的方法声明自己的委托类型。我们可以简单地使用预定义的 Action 委托并将参数类型指定为泛型参数。这可以使我们的代码更加简洁和一致。

例如,假设我们有一个将消息打印到控制台的方法:

// A method that prints a message  
public static void PrintMessage(string message) {  
    Console.WriteLine(message);  
}

要将此方法用作委托,我们可以声明自己的委托类型,也可以使用 Action 委托:

// Declare delegate type  
delegate void PrintDelegate(string message);  
  
// Create a delegate instance  
PrintDelegate print = PrintMessage;  
  
// Invoke the delegate  
print("Hello, world!");  
  
// Or, use an Action delegate  
Action<string> printWithAction = PrintMessage;  
  
// Invoke the delegate  
printWithAction("Hello, world!");

正如我们所看到的,使用 Action 委托可以省去声明委托类型的麻烦,并使代码更具可读性。

我们还可以使用匿名方法或 lambda 表达式来创建 Action 委托实例:

// Create an Action delegate instance using an anonymous method  
Action<string> print = delegate(string message) {  
    Console.WriteLine(message);  
};  
  
// Create an Action delegate instance using a lambda expression  
Action<string> print = message => Console.WriteLine(message);

上面的代码演示了如何使用匿名方法和 lambda 表达式创建委托的实例。Action

让我们继续讨论 func 代表。

Func 代表

Func 委托是预定义的委托,可以指向采用零个或多个参数并返回任何类型的值的任何方法。Func 委托在 System 命名空间中定义,并具有以下通用形式:

// Func delegate with no parameters and a return value  
public delegate TResult Func<out TResult>();  
  
// Func delegate with one parameter and a return value  
public delegate TResult Func<in T, out TResult>(T arg);  
  
// Func delegate with two parameters and a return value  
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);  
  
// Func delegate with three parameters and a return value  
public delegate TResult Func<in T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3);  
  
// ... and so on, up to 16 parameters and a return value

使用 Func 委托的优点是,我们不需要为返回值的方法声明自己的委托类型。我们可以简单地使用预定义的 Func 委托,并将参数类型和返回类型指定为泛型参数。这可以使我们的代码更加简洁和一致。

例如,假设我们有一个计算数字平方的方法:

// A method that calculates the square of a number  
public static int Square(int x) {  
    return x * x;  
}

要将此方法用作委托,我们可以声明自己的委托类型,也可以使用 Func 委托:

// Declare delegate type  
delegate int SquareDelegate(int x);  
  
// Create a delegate instance  
SquareDelegate square = Square;  
  
// Invoke the delegate  
int result = square(10); // returns 100  
  
// Or, use a Func delegate  
Func<int, int> square = Square;  
  
// Invoke the delegate  
int result = square(10); // returns 100

正如我们所看到的,使用 Func 委托省去了声明委托类型的麻烦,并使代码更具可读性。

我们还可以使用匿名方法或 lambda 表达式来创建 Func 委托实例:

// Create a Func delegate instance using an anonymous method  
Func<int, int> square = delegate(int x) {  
    return x * x;  
};  
  
// Create a Func delegate instance using a lambda expression  
Func<int, int> square = x => x * x;

上面的代码演示了如何使用匿名方法和 lambda 表达式创建委托的实例。Func

让我们比较一下 Action 和 Func 委托。

Action 和 Func 委托之间的比较

Action 和 Func 委托非常相似,但有一个区别:Action 委托返回 void,而 Func 委托返回值。这种差异会影响我们在代码中使用这些委托的方式:

  • 何时使用 Action 委托:当我们想要传递或存储执行某些操作且不返回任何值的方法时,我们应该使用 Action 委托。例如,我们可以使用 Action 委托来传递或存储将某些内容打印到控制台、将某些内容写入文件、更新某些数据或引发某些事件的方法。
  • 何时使用 Func 委托:当我们想要传递或存储执行某些计算并返回值的方法时,我们应该使用 Func 委托。例如,我们可以使用 Func 委托来传递或存储计算某些数学函数、检查某些条件、比较某些值或转换某些数据的方法。

性能注意事项和权衡

使用 Action 和 Func 委托可以提高代码的可读性和可维护性,但它们也有一些性能影响。在使用这些委托时,我们应该意识到这些影响并做出明智的决定:

  • 内存分配:每次创建委托实例时,都会在堆上分配一些内存。这可能会增加应用程序的内存使用率和垃圾回收开销。为了避免不必要的内存分配,我们应该尽可能重用委托实例,并避免在循环或经常调用的方法中创建委托实例。
  • 调用开销:每次调用委托时,都会为委托调用产生一些开销。这种开销通常可以忽略不计,但如果我们多次调用委托或在性能关键场景中调用委托,它可能会变得很大。为了减少调用开销,我们应该避免在循环或经常调用的方法中调用委托,并尽可能选择直接方法调用。
  • 装箱和拆箱:如果我们使用值类型(如 int、double 或 struct)的 Action 和 Func 委托,我们可能会产生一些装箱和拆箱操作。装箱和拆箱是将值类型转换为对象类型的过程,反之亦然。这些操作可能会降低应用程序的性能并增加内存使用率。为了避免装箱和拆箱,我们应该使用具有特定值类型的泛型 Action 和 Func 委托,并避免使用采用对象参数的非泛型 Action 和 Func 委托。

使用案例和最佳实践

Action 和 Func 委托非常通用,可用于许多方案。下面是可以使用这些委托的实际方案的一些示例:

  • LINQ:LINQ(语言集成查询)是 C# 的一项功能,它允许在各种数据源(如数组、集合、数据库、XML 等)上编写富有表现力的简洁查询。 LINQ 严重依赖 Action 和 Func 委托来传递定义查询逻辑的方法,例如筛选、投影、排序、分组、聚合等。例如,我们可以使用该方法根据谓词过滤数字集合,并使用 Func 委托将每个数字投影到其平方的方法:
// A collection of numbers  
int[] numbers = { 1, 2, 3, 4, 5 };  
  
// Use LINQ to filter and project the numbers  
var query = numbers.Where(x => x % 2 == 0) // use a Func delegate to filter even numbers  
                   .Select(x => x * x); // use a Func delegate to project each number to its square  
  
// Execute the query and print the results  
foreach (var item in query) {  
    Console.WriteLine(item);  
}

上述代码的输出为 4、16。该方法根据谓词过滤数字,该谓词对偶数返回 true,对奇数返回 false。该方法使用 lambda 表达式将每个数字投影到其平方。Wherex => x % 2 == 0Selectx => x * x

  • 任务并行库:任务并行库 (TPL) 是 C# 的一项功能,它使我们能够更轻松、更高效地编写并行和异步代码。TPL 使用 Action 和 Func 委托来传递表示可以并发或异步执行的工作单元的方法,例如任务、延续、回调等。例如,我们可以使用该方法启动一个在单独的线程中执行方法的新任务,并使用 Action 和 Func 委托附加在任务完成后运行的延续方法:Task.RunContinueWith
// A method that performs some long-running work  
public static void DoWork() {  
    Console.WriteLine("Doing work...");  
    Thread.Sleep(5000); // simulate some work  
    Console.WriteLine("Work done.");  
}  
  
// A method that performs some continuation work  
public static void DoMoreWork(Task t) {  
    Console.WriteLine("Doing more work...");  
    Thread.Sleep(3000); // simulate some work  
    Console.WriteLine("More work done.");  
}  
  
// Use TPL to run the methods in parallel  
var task = Task.Run((Action)DoWork); // use an Action delegate to start a new task  
task.ContinueWith(DoMoreWork); // use an Action delegate to attach a continuation task  
  
// Wait for the tasks to finish  
task.Wait();

上述代码的输出如下所示。该方法启动一个新任务,该任务在单独的线程中执行该方法。该方法附加一个延续任务,该任务在任务完成后执行该方法。

输出:

Doing work...  
Work done.  
Doing more work...  
More work done.
  • 委托作为回调:委托的一个常见用例是将方法作为回调传递,回调是在发生某些事件或条件时调用的方法。例如,我们可以使用委托来传递处理异步操作(如 Web 请求、文件操作或数据库查询)结果的方法。我们可以使用 Action 和 Func 委托来传递处理操作成功或失败的方法,以及操作返回的数据或错误。例如,我们可以使用该类发送异步 Web 请求,并使用 Func 委托传递处理响应或异常的方法:
// A method that handles the web response  
public static async Task HandleResponse(HttpResponseMessage response) {  
    Console.WriteLine("Response status code: " + response.StatusCode);  
    string content = await response.Content.ReadAsStringAsync();  
    Console.WriteLine("Response content: " + content);  
}  
  
// A method that handles the web exception  
public static void HandleException(Exception ex) {  
    Console.WriteLine("Exception: " + ex.Message);  
}  
  
// Use HttpClient to send an asynchronous web request  
var client = new HttpClient();  
var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com");  
client.SendAsync(request)  
      // use a Func delegate to handle the response  
      .ContinueWith((Func<Task<HttpResponseMessage>, Task>)HandleResponse, TaskContinuationOptions.OnlyOnRanToCompletion)  
      // use an Action delegate to handle the exception  
      .ContinueWith((Action<Task>)HandleException, TaskContinuationOptions.OnlyOnFaulted);

上面的代码使用该类向 URL 发送异步 Web 请求。该方法附加一个在请求成功完成时执行该方法的延续任务,以及另一个在请求失败并出现异常时执行该方法的延续任务。

让我们探讨有效使用 Action 和 Func 委托的技巧和最佳实践,以及要避免的常见陷阱。

有效使用 Action 和 Func 委托的提示和最佳实践

下面是在 C# 项目中有效使用 Action 和 Func 委托的一些提示和最佳实践:

  • 尽可能使用 Action 和 Func 委托,而不是声明我们自己的委托类型。这可以使代码更加简洁、一致,并且可与使用这些委托的其他 .NET 库和框架进行互操作。
  • 使用 lambda 表达式或匿名方法创建委托实例,而不是命名方法。这可以使代码更具表现力和可读性,并避免不必要的方法声明。
  • 使用具有特定参数和返回类型的泛型 Action 和 Func 委托,而不是采用对象参数的非泛型 Action 和 Func 委托。这样可以避免装箱和拆箱操作,提高代码的类型安全性和性能。
  • 尽可能重用委托实例,而不是每次都创建新的委托实例。这可以减少内存分配和垃圾回收开销,并提高代码的性能。
  • 避免在循环或经常调用的方法中调用委托,并尽可能选择直接方法调用。这可以减少调用开销并提高代码的性能。

常见陷阱以及如何避免它们

下面是一些常见的陷阱,以及如何在 C# 项目中使用 Action 和 Func 委托时避免这些陷阱:

  • 将错误数量的参数或类型传递给委托。这可能会导致编译时错误或运行时异常。为避免这种情况,我们应该始终检查委托的签名和方法,并确保它们匹配。我们还可以使用关键字让编译器为我们推断委托类型,并避免显式类型声明。var
  • 将有副作用的方法传递给委托。这可能会导致意外或不一致的行为,尤其是在多次或并行调用委托时。为了避免这种情况,我们应该始终传递纯方法,这意味着它们不修改任何外部状态或依赖于任何外部状态。我们还可以使用修饰符来标记该方法不应修改的参数或字段。readonly
  • 将 null 值传递给委托。这可能会导致在调用委托时出现运行时异常。为了避免这种情况,我们应该在调用委托之前始终检查委托是否为 null,或者仅在委托不为 null 时才使用 null 条件运算符 () 调用委托。如果委托为 null,我们还可以使用运算符为委托提供默认值。

在本文中,我们了解了 C# 中的 Action 和 Func 委托,以及它们如何简化我们的代码并使其更具可读性和可维护性。我们还看到了一些实际场景的示例,在这些场景中,这些委托很有用,以及一些有效使用它们的技巧和最佳实践。我们还讨论了一些性能注意事项和权衡,以及一些常见的陷阱以及如何避免它们。

Action 和 Func 委托是强大的工具,可以帮助我们用 C# 编写富有表现力和简洁的代码。它们能够将方法作为参数传递,将方法存储为变量,以及创建自定义事件和回调。它们还广泛用于许多 .NET 库和框架,例如 LINQ 和 TPL。

阅读排行