本地函数(C# 编程指南)Local functions (C# Programming Guide)

从 C# 7.0 开始,C# 支持本地函数Starting with C# 7.0, C# supports local functions . 本地函数是一种嵌套在另一成员中的类型的私有方法。Local functions are private methods of a type that are nested in another member. 仅能从其包含成员中调用它们。They can only be called from their containing member. 可以在以下位置中声明和调用本地函数:Local functions can be declared in and called from:

  • 方法(尤其是迭代器方法和异步方法)Methods, especially iterator methods and async methods
  • 构造函数Constructors
  • 属性访问器Property accessors
  • 事件访问器Event accessors
  • 匿名方法Anonymous methods
  • Lambda 表达式Lambda expressions
  • 终结器Finalizers
  • 其他本地函数Other local functions

但是,不能在 expression-bodied 成员中声明本地函数。However, local functions can't be declared inside an expression-bodied member.

备注

在某些情况下,可以使用 lambda 表达式实现本地函数也支持的功能。In some cases, you can use a lambda expression to implement functionality also supported by a local function. 有关比较,请参阅本地函数与 lambda 表达式For a comparison, see Local functions vs. lambda expressions.

本地函数可使代码意图明确。Local functions make the intent of your code clear. 任何读取代码的人都可以看到,此方法不可调用,包含方法除外。Anyone reading your code can see that the method is not callable except by the containing method. 对于团队项目,它们也使得其他开发人员无法直接从类或结构中的其他位置错误调用此方法。For team projects, they also make it impossible for another developer to mistakenly call the method directly from elsewhere in the class or struct.

本地函数语法Local function syntax

本地函数被定义为包含成员中的嵌套方法。A local function is defined as a nested method inside a containing member. 其定义具有以下语法:Its definition has the following syntax:

<modifiers> <return-type> <method-name> <parameter-list>

可以将以下修饰符用于本地函数:You can use the following modifiers with a local function:

  • async
  • unsafe
  • static(在 C# 8.0 和更高版本中)。static (in C# 8.0 and later). 静态本地函数无法捕获局部变量或实例状态。A static local function can't capture local variables or instance state.
  • extern(在 C# 9.0 和更高版本中)。extern (in C# 9.0 and later). 外部本地函数必须为 staticAn external local function must be static.

在包含成员中定义的所有本地变量(包括其方法参数)都可在非静态本地函数中访问。All local variables that are defined in the containing member, including its method parameters, are accessible in a non-static local function.

与方法定义不同,本地函数定义不能包含成员访问修饰符。Unlike a method definition, a local function definition cannot include the member access modifier. 因为所有本地函数都是私有的,包括访问修饰符(如 private 关键字)会生成编译器错误 CS0106“修饰符‘private’对于此项无效”。Because all local functions are private, including an access modifier, such as the private keyword, generates compiler error CS0106, "The modifier 'private' is not valid for this item."

以下示例定义了一个名为 AppendPathSeparator 的本地函数,该函数对于名为 GetText 的方法是私有的:The following example defines a local function named AppendPathSeparator that is private to a method named GetText:

private static string GetText(string path, string filename)
{
     var reader = File.OpenText($"{AppendPathSeparator(path)}{filename}");
     var text = reader.ReadToEnd();
     return text;

     string AppendPathSeparator(string filepath)
     {
        return filepath.EndsWith(@"\") ? filepath : filepath + @"\";
     }
}

从 C# 9.0 开始,你可以将属性应用于本地函数、其参数和类型参数,如以下示例所示:Beginning with C# 9.0, you can apply attributes to a local function, its parameters and type parameters, as the following example shows:

#nullable enable
private static void Process(string?[] lines, string mark)
{
    foreach (var line in lines)
    {
        if (IsValid(line))
        {
            // Processing logic...
        }
    }

    bool IsValid([NotNullWhen(true)] string? line)
    {
        return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
    }
}

前面的示例使用特殊属性来帮助编译器在可为空的上下文中进行静态分析。The preceding example uses a special attribute to assist the compiler in static analysis in a nullable context.

本地函数和异常Local functions and exceptions

本地函数的一个实用功能是可以允许立即显示异常。One of the useful features of local functions is that they can allow exceptions to surface immediately. 对于方法迭代器,仅在枚举返回的序列时才显示异常,而非在检索迭代器时。For method iterators, exceptions are surfaced only when the returned sequence is enumerated, and not when the iterator is retrieved. 对于异步方法,在等待返回的任务时,将观察到异步方法中引发的任何异常。For async methods, any exceptions thrown in an async method are observed when the returned task is awaited.

以下示例定义 OddSequence 方法,用于枚举指定范围中的奇数。The following example defines an OddSequence method that enumerates odd numbers in a specified range. 因为它会将一个大于 100 的数字传递到 OddSequence 迭代器方法,该方法将引发 ArgumentOutOfRangeExceptionBecause it passes a number greater than 100 to the OddSequence enumerator method, the method throws an ArgumentOutOfRangeException. 如示例中的输出所示,仅当循环访问数字时才显示异常,而非检索迭代器时。As the output from the example shows, the exception surfaces only when you iterate the numbers, and not when you retrieve the enumerator.

using System;
using System.Collections.Generic;

public class IteratorWithoutLocalExample
{
   public static void Main()
   {
      IEnumerable<int> xs = OddSequence(50, 110);
      Console.WriteLine("Retrieved enumerator...");

      foreach (var x in xs)  // line 11
      {
         Console.Write($"{x} ");
      }
   }

   public static IEnumerable<int> OddSequence(int start, int end)
   {
      if (start < 0 || start > 99)
         throw new ArgumentOutOfRangeException(nameof(start), "start must be between 0 and 99.");
      if (end > 100)
         throw new ArgumentOutOfRangeException(nameof(end), "end must be less than or equal to 100.");
      if (start >= end)
         throw new ArgumentException("start must be less than end.");

      for (int i = start; i <= end; i++)
      {
         if (i % 2 == 1)
            yield return i;
      }
   }
}
// The example displays the output like this:
//
//    Retrieved enumerator...
//    Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100. (Parameter 'end')
//    at IteratorWithoutLocalExample.OddSequence(Int32 start, Int32 end)+MoveNext() in IteratorWithoutLocal.cs:line 22
//    at IteratorWithoutLocalExample.Main() in IteratorWithoutLocal.cs:line 11

如果将迭代器逻辑放入本地函数,则在检索枚举器时会引发参数验证异常,如下面的示例所示:If you put iterator logic into a local function, argument validation exceptions are thrown when you retrieve the enumerator, as the following example shows:

using System;
using System.Collections.Generic;

public class IteratorWithLocalExample
{
   public static void Main()
   {
      IEnumerable<int> xs = OddSequence(50, 110);  // line 8
      Console.WriteLine("Retrieved enumerator...");

      foreach (var x in xs)
      {
         Console.Write($"{x} ");
      }
   }

   public static IEnumerable<int> OddSequence(int start, int end)
   {
      if (start < 0 || start > 99)
         throw new ArgumentOutOfRangeException(nameof(start), "start must be between 0 and 99.");
      if (end > 100)
         throw new ArgumentOutOfRangeException(nameof(end), "end must be less than or equal to 100.");
      if (start >= end)
         throw new ArgumentException("start must be less than end.");

      return GetOddSequenceEnumerator();

      IEnumerable<int> GetOddSequenceEnumerator()
      {
         for (int i = start; i <= end; i++)
         {
            if (i % 2 == 1)
               yield return i;
         }
      }
   }
}
// The example displays the output like this:
//
//    Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100. (Parameter 'end')
//    at IteratorWithLocalExample.OddSequence(Int32 start, Int32 end) in IteratorWithLocal.cs:line 22
//    at IteratorWithLocalExample.Main() in IteratorWithLocal.cs:line 8

你可以通过类似于异步操作的方式来使用本地函数。You can use local functions in a similar way with asynchronous operations. 等待相应的任务时,异步方法图面中引发的异常。Exceptions thrown in an async method surface when the corresponding task is awaited. 本地函数允许代码快速失败,并允许同步引发和观察异常。Local functions allow your code to fail fast and allow your exception to be both thrown and observed synchronously.

以下示例使用名为 GetMultipleAsync 的异步方法暂停指定的秒数并返回一个值,该值是该秒数的任意倍数。The following example uses an asynchronous method named GetMultipleAsync to pause for a specified number of seconds and return a value that is a random multiple of that number of seconds. 最大延迟为 5 秒;如果该值大于 5,则结果为 ArgumentOutOfRangeExceptionThe maximum delay is 5 seconds; an ArgumentOutOfRangeException results if the value is greater than 5. 如下面的示例所示,仅当任务处于等待状态时,才会观察到将值 6 传递到 GetMultipleAsync 方法时引发的异常。As the following example shows, the exception that is thrown when a value of 6 is passed to the GetMultipleAsync method is observed only when the task is awaited.

using System;
using System.Threading.Tasks;

public class AsyncWithoutLocalExample
{
   public static async Task Main()
   {
      var t = GetMultipleAsync(6);
      Console.WriteLine("Got the task");
      
      var result = await t;  // line 11
      Console.WriteLine($"The returned value is {result:N0}");
   }

   static async Task<int> GetMultipleAsync(int delayInSeconds)
   {
      if (delayInSeconds < 0 || delayInSeconds > 5)
         throw new ArgumentOutOfRangeException(nameof(delayInSeconds), "Delay cannot exceed 5 seconds.");

      await Task.Delay(delayInSeconds * 1000);
      return delayInSeconds * new Random().Next(2,10);
   }
}
// The example displays the output like this:
//
// Got the task
// Unhandled exception. System.ArgumentOutOfRangeException: Delay cannot exceed 5 seconds. (Parameter 'delayInSeconds')
//   at AsyncWithoutLocalExample.GetMultipleAsync(Int32 delayInSeconds) in AsyncWithoutLocal.cs:line 18
//   at AsyncWithoutLocalExample.Main() in AsyncWithoutLocal.cs:line 11

与方法迭代器类似,你可以重构前面的示例,将异步操作的代码放入本地函数。Like with the method iterator, you can refactor the preceding example and put the code of asynchronous operation in a local function. 如以下示例中的输出所示,调用 GetMultiple 方法后,会引发 ArgumentOutOfRangeExceptionAs the output from the following example shows, the ArgumentOutOfRangeException is thrown as soon as the GetMultiple method is called.

using System;
using System.Threading.Tasks;

public class AsyncWithLocalExample
{
   public static async Task Main()
   {
      var t = GetMultiple(6);  // line 8
      Console.WriteLine("Got the task");
      
      var result = await t;
      Console.WriteLine($"The returned value is {result:N0}");
   }

   static Task<int> GetMultiple(int delayInSeconds)
   {
      if (delayInSeconds < 0 || delayInSeconds > 5)
         throw new ArgumentOutOfRangeException(nameof(delayInSeconds), "Delay cannot exceed 5 seconds.");

      return GetValueAsync();

      async Task<int> GetValueAsync()
      {
         await Task.Delay(delayInSeconds * 1000);
         return delayInSeconds * new Random().Next(2,10);
      }
   }
}
// The example displays the output like this:
//
// Unhandled exception. System.ArgumentOutOfRangeException: Delay cannot exceed 5 seconds. (Parameter 'delayInSeconds')
//   at AsyncWithLocalExample.GetMultiple(Int32 delayInSeconds) in AsyncWithLocal.cs:line 18
//   at AsyncWithLocalExample.Main() in AsyncWithLocal.cs:line 8

本地函数与 Lambda 表达式Local functions vs. lambda expressions

乍看之下,本地函数和 lambda 表达式非常相似。At first glance, local functions and lambda expressions are very similar. 在许多情况下,选择使用 Lambda 表达式还是本地函数是风格和个人偏好的问题。In many cases, the choice between using lambda expressions and local functions is a matter of style and personal preference. 但是,应该注意,从两者中选用一种的时机和条件其实是存在差别的。However, there are real differences in where you can use one or the other that you should be aware of.

让我们检查一下阶乘算法的本地函数实现和 lambda 表达式实现之间的差异。Let's examine the differences between the local function and lambda expression implementations of the factorial algorithm. 下面是使用本地函数的版本:Here's the version using a local function:

public static int LocalFunctionFactorial(int n)
{
    return nthFactorial(n);

    int nthFactorial(int number) => number < 2 
        ? 1 
        : number * nthFactorial(number - 1);
}

此版本使用 Lambda 表达式:This version uses lambda expressions:

public static int LambdaFactorial(int n)
{
    Func<int, int> nthFactorial = default(Func<int, int>);

    nthFactorial = number => number < 2
        ? 1
        : number * nthFactorial(number - 1);

    return nthFactorial(n);
}

命名Naming

本地函数的命名方式与方法相同。Local functions are explicitly named like methods. Lambda 表达式是一种匿名方法,需要分配给 delegate 类型的变量,通常是 ActionFunc 类型。Lambda expressions are anonymous methods and need to be assigned to variables of a delegate type, typically either Action or Func types. 声明本地函数时,此过程类似于编写普通方法;声明一个返回类型和一个函数签名。When you declare a local function, the process is like writing a normal method; you declare a return type and a function signature.

函数签名和 Lambda 表达式类型Function signatures and lambda expression types

Lambda 表达式依赖于为其分配的 Action/Func 变量的类型来确定参数和返回类型。Lambda expressions rely on the type of the Action/Func variable that they're assigned to determine the argument and return types. 在本地函数中,因为语法非常类似于编写常规方法,所以参数类型和返回类型已经是函数声明的一部分。In local functions, since the syntax is much like writing a normal method, argument types and return type are already part of the function declaration.

明确赋值Definite assignment

Lambda 表达式是在运行时声明和分配的对象。Lambda expressions are objects that are declared and assigned at runtime. 若要使用 Lambda 表达式,需要对其进行明确赋值:必须声明要分配给它的 Action/Func 变量,并为其分配 Lambda 表达式。In order for a lambda expression to be used, it needs to be definitely assigned: the Action/Func variable that it will be assigned to must be declared and the lambda expression assigned to it. 请注意,LambdaFactorial 必须先声明和初始化 Lambda 表达式 nthFactorial,然后再对其进行定义。Notice that LambdaFactorial must declare and initialize the lambda expression nthFactorial before defining it. 否则,会导致分配前引用 nthFactorial 时出现编译时错误。Not doing so results in a compile time error for referencing nthFactorial before assigning it.

本地函数在编译时定义。Local functions are defined at compile time. 由于未将它们分配给变量,因此可以从范围内的任意代码位置引用它们;在第一个示例 LocalFunctionFactorial 中,我们可以在 return 语句的上方或下方声明本地函数,而不会触发任何编译器错误。As they're not assigned to variables, they can be referenced from any code location where it is in scope ; in our first example LocalFunctionFactorial, we could declare our local function either above or below the return statement and not trigger any compiler errors.

这些区别意味着使用本地函数创建递归算法会更轻松。These differences mean that recursive algorithms are easier to create using local functions. 你可以声明和定义一个调用自身的本地函数。You can declare and define a local function that calls itself. 必须声明 Lambda 表达式,赋给默认值,然后才能将其重新赋给引用相同 Lambda 表达式的主体。Lambda expressions must be declared, and assigned a default value before they can be re-assigned to a body that references the same lambda expression.

实现为委托Implementation as a delegate

Lambda 表达式在声明时转换为委托。Lambda expressions are converted to delegates when they're declared. 本地函数更加灵活,可以像传统方法一样编写,也可以作为委托编写。Local functions are more flexible in that they can be written like a traditional method or as a delegate. 只有在用作委托时,本地函数才转换为委托。Local functions are only converted to delegates when used as a delegate.

如果声明了本地函数,但只是通过像调用方法一样调用该函数来引用该函数,它将不会转换成委托。If you declare a local function and only reference it by calling it like a method, it will not be converted to a delegate.

变量捕获Variable capture

明确分配的规则也会影响本地函数或 Lambda 表达式捕获的任何变量。The rules of definite assignment also affect any variables that are captured by the local function or lambda expression. 编译器可以执行静态分析,因此本地函数能够在封闭范围内明确分配捕获的变量。The compiler can perform static analysis that enables local functions to definitely assign captured variables in the enclosing scope. 请看以下示例:Consider this example:

int M()
{
    int y;
    LocalFunction();
    return y;

    void LocalFunction() => y = 0;
}

编译器可以确定 LocalFunction 在调用时明确分配 yThe compiler can determine that LocalFunction definitely assigns y when called. 因为在 return 语句之前调用了 LocalFunction,所以在 return 语句中明确分配了 yBecause LocalFunction is called before the return statement, y is definitely assigned at the return statement.

请注意,当本地函数捕获封闭范围中的变量时,本地函数将作为委托类型实现。Note that when a local function captures variables in the enclosing scope, the local function is implemented as a delegate type.

堆分配Heap allocations

根据它们的用途,本地函数可以避免 Lambda 表达式始终需要的堆分配。Depending on their use, local functions can avoid heap allocations that are always necessary for lambda expressions. 如果本地函数永远不会转换为委托,并且本地函数捕获的变量都不会被其他转换为委托的 lambda 或本地函数捕获,则编译器可以避免堆分配。If a local function is never converted to a delegate, and none of the variables captured by the local function are captured by other lambdas or local functions that are converted to delegates, the compiler can avoid heap allocations.

请看以下异步示例:Consider this async example:

public Task<string> PerformLongRunningWorkLambda(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    if (index < 0)
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

    Func<Task<string>> longRunningWorkImplementation = async () =>
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    };

    return longRunningWorkImplementation();
}

该 lambda 表达式的闭包包含 addressindexname 变量。The closure for this lambda expression contains the address, index and name variables. 就本地函数而言,实现闭包的对象可能为 struct 类型。In the case of local functions, the object that implements the closure may be a struct type. 该结构类型将通过引用传递给本地函数。That struct type would be passed by reference to the local function. 实现中的这个差异将保存在分配上。This difference in implementation would save on an allocation.

Lambda 表达式所需的实例化意味着额外的内存分配,后者可能是时间关键代码路径中的性能因素。The instantiation necessary for lambda expressions means extra memory allocations, which may be a performance factor in time-critical code paths. 本地函数不会产生这种开销。Local functions do not incur this overhead. 在以上示例中,本地函数版本具有的分配比 Lambda 表达式版本少 2 个。In the example above, the local functions version has two fewer allocations than the lambda expression version.

如果你知道本地函数不会转换为委托,并且本地函数捕获的变量都不会被其他转换为委托的 lambda 或本地函数捕获,则可以通过将本地函数声明为 static 本地函数来确保避免在堆上对其进行分配。If you know that your local function won't be converted to a delegate and none of the variables captured by it are captured by other lambdas or local functions that are converted to delegates, you can guarantee that your local function avoids being allocated on the heap by declaring it as a static local function. 请注意,此功能在 C# 8.0 及更高版本中提供。Note that this feature is available in C# 8.0 and newer.

备注

等效于此方法的本地函数还将类用于闭包。The local function equivalent of this method also uses a class for the closure. 实现详细信息包括本地函数的闭包是作为 class 还是 struct 实现。Whether the closure for a local function is implemented as a class or a struct is an implementation detail. 本地函数可能使用 struct,而 lambda 将始终使用 classA local function may use a struct whereas a lambda will always use a class.

public Task<string> PerformLongRunningWork(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    if (index < 0)
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

    return longRunningWorkImplementation();

    async Task<string> longRunningWorkImplementation()
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    }
}

yield 关键字的用法Usage of the yield keyword

在本示例中尚未演示的最后一个优点是,可将本地函数作为迭代器实现,使用 yield return 语法生成一系列值。One final advantage not demonstrated in this sample is that local functions can be implemented as iterators, using the yield return syntax to produce a sequence of values.

public IEnumerable<string> SequenceToLowercase(IEnumerable<string> input)
{
    if (!input.Any())
    {
        throw new ArgumentException("There are no items to convert to lowercase.");
    }
    
    return LowercaseIterator();
    
    IEnumerable<string> LowercaseIterator()
    {
        foreach (var output in input.Select(item => item.ToLower()))
        {
            yield return output;
        }
    }
}

Lambda 表达式中不允许使用 yield return 语句,请参阅编译器错误 CS1621The yield return statement is not allowed in lambda expressions, see compiler error CS1621.

虽然本地函数对 lambda 表达式可能有点冗余,但实际上它们的目的和用法都不一样。While local functions may seem redundant to lambda expressions, they actually serve different purposes and have different uses. 如果想要编写仅从上下文或其他方法中调用的函数,则使用本地函数更高效。Local functions are more efficient for the case when you want to write a function that is called only from the context of another method.

请参阅See also