本地函数(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 compared to 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: async | unsafe> <return-type> <method-name> <parameter-list>

本地函数可以使用 asyncunsafe 修饰符。Local functions can use the async and unsafe modifiers.

请注意,在包含成员中定义的所有本地变量(包括其方法参数)都可在本地函数中访问。Note that all local variables that are defined in the containing member, including its method parameters, are accessible in the 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."

备注

在 C# 8.0 之前,本地函数不能包含 static 修饰符。Prior to C# 8.0, local functions cannot include the static modifier. 包括 static 关键字将生成编译器错误 CS0106“修饰符‘static’对于此项无效”。Including the static keyword generates compiler error CS0106, "The modifier 'static' is not valid for this item."

此外,属性不能应用于本地函数或其参数和类型参数。In addition, attributes can't be applied to the local function or to its parameters and type parameters.

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

using System;
using System.IO;

class Example
{
    static void Main()
    {
        string contents = GetText(@"C:\temp", "example.txt");
        Console.WriteLine("Contents of the file:\n" + contents);
    }
   
    private static string GetText(string path, string filename)
    {
         var sr = File.OpenText(AppendPathSeparator(path) + filename);
         var text = sr.ReadToEnd();
         return text;
         
         // Declare a local function.
         string AppendPathSeparator(string filepath)
         {
            if (! filepath.EndsWith(@"\"))
               filepath += @"\";

            return filepath;   
         }
    } 
}

本地函数和异常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 between 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;

class Example
{
   static void Main()
   {
      IEnumerable<int> ienum = OddSequence(50, 110);
      Console.WriteLine("Retrieved enumerator...");
      
      foreach (var i in ienum)
      {
         Console.Write($"{i} ");
      }
   }

   public static IEnumerable<int> OddSequence(int start, int end)
   {
      if (start < 0 || start > 99)
         throw new ArgumentOutOfRangeException("start must be between 0 and 99.");
      if (end > 100)
         throw new ArgumentOutOfRangeException("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 following output:
//    Retrieved enumerator...
//    
//    Unhandled Exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
//    Parameter name: end must be less than or equal to 100.
//       at Sequence.<GetNumericRange>d__1.MoveNext() in Program.cs:line 23
//       at Example.Main() in Program.cs:line 43

相反,可以在执行验证时和通过从本地函数返回迭代器检索迭代器之前引发异常,如以下示例所示。Instead, you can throw an exception when performing validation and before retrieving the iterator by returning the iterator from a local function, as the following example shows.

using System;
using System.Collections.Generic;

class Example
{
   static void Main()
   {
      IEnumerable<int> ienum = OddSequence(50, 110);
      Console.WriteLine("Retrieved enumerator...");
      
      foreach (var i in ienum)
      {
         Console.Write($"{i} ");
      }
   }

   public static IEnumerable<int> OddSequence(int start, int end)
   {
      if (start < 0 || start > 99)
         throw new ArgumentOutOfRangeException("start must be between 0 and 99.");
      if (end > 100)
         throw new ArgumentOutOfRangeException("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 following output:
//    Unhandled Exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
//    Parameter name: end must be less than or equal to 100.
//       at Sequence.<GetNumericRange>d__1.MoveNext() in Program.cs:line 23
//       at Example.Main() in Program.cs:line 43

可以通过类似的方式使用本地函数来处理异步操作之外的异常。Local functions can be used in a similar way to handle exceptions outside of the asynchronous operation. 异步方法中引发的异常通常都需要检查 AggregateException 的内部异常。Ordinarily, exceptions thrown in async method require that you examine the inner exceptions of an AggregateException. 本地函数允许代码快速失败,并允许同步引发和观察异常。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. 如以下示例所示,开始执行 GetMultipleAsync 方法后,将值 6 传递到 GetMultipleAsync 方法时引发的异常将在 AggregateException 中进行包装。As the following example shows, the exception that is thrown when a value of 6 is passed to the GetMultipleAsync method is wrapped in an AggregateException after the GetMultipleAsync method begins execution.

using System;
using System.Threading.Tasks;

class Example
{
   static void Main()
   {
      int result = GetMultipleAsync(6).Result;
      Console.WriteLine($"The returned value is {result:N0}");
   }

   static async Task<int> GetMultipleAsync(int secondsDelay)
   {
      Console.WriteLine("Executing GetMultipleAsync...");
      if (secondsDelay < 0 || secondsDelay > 5)
         throw new ArgumentOutOfRangeException("secondsDelay cannot exceed 5.");
         
      await Task.Delay(secondsDelay * 1000);
      return secondsDelay * new Random().Next(2,10);
   } 
}
// The example displays the following output:
//    Executing GetMultipleAsync...
//
//    Unhandled Exception: System.AggregateException: 
//         One or more errors occurred. (Specified argument was out of the range of valid values.
//    Parameter name: secondsDelay cannot exceed 5.) ---> 
//         System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
//    Parameter name: secondsDelay cannot exceed 5.
//       at Example.<GetMultiple>d__1.MoveNext() in Program.cs:line 17
//       --- End of inner exception stack trace ---
//       at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
//       at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
//       at Example.Main() in C:\Users\ronpet\Documents\Visual Studio 2017\Projects\local-functions\async1\Program.cs:line 8

正如处理方法迭代器一样,可以在调用异步方法之前重构本示例中的代码以执行验证。As we did with the method iterator, we can refactor the code from this example to perform the validation before calling the asynchronous method. 如以下示例中的输出所示,ArgumentOutOfRangeException 不在 AggregateException 中进行包装。As the output from the following example shows, the ArgumentOutOfRangeException is not wrapped in a AggregateException.

using System;
using System.Threading.Tasks;

class Example
{
   static void Main()
   {
      int result = GetMultiple(6).Result;
      Console.WriteLine($"The returned value is {result:N0}");
   }

   static Task<int> GetMultiple(int secondsDelay)
   {
      if (secondsDelay < 0 || secondsDelay > 5)
         throw new ArgumentOutOfRangeException("secondsDelay cannot exceed 5.");
         
      return GetValueAsync();
      
      async Task<int> GetValueAsync()
      {
         Console.WriteLine("Executing GetValueAsync...");
         await Task.Delay(secondsDelay * 1000);
         return secondsDelay * new Random().Next(2,10);
      }   
   } 
}
// The example displays the following output:
//    Unhandled Exception: System.ArgumentOutOfRangeException: 
//       Specified argument was out of the range of valid values.
//    Parameter name: secondsDelay cannot exceed 5.
//       at Example.GetMultiple(Int32 secondsDelay) in Program.cs:line 17
//       at Example.Main() in Program.cs:line 8

请参阅See also