區域函式 (C# 程式設計手冊)

區域函式 是另一個成員中巢狀類型的方法。 它們只可以從其包含成員呼叫。 區域函式可以宣告於下列項目中,以及從下列項目呼叫:

  • 方法,特別是迭代器方法和非同步方法
  • 建構函式
  • 屬性存取子
  • 事件存取子
  • 匿名方法
  • Lambda 運算式
  • 完成項
  • 其他區域函式

不過,區域函式不能宣告於運算式主體成員內。

注意

在某些情況下,您可以使用 Lambda 運算式來實作區域函式也支援的功能。 如需比較,請參閱 區域函式與 Lambda 運算式

區域函式讓程式碼的意圖更為清楚。 讀取程式碼的任何人都可以看到方法無法呼叫,但包含的方法除外。 對於 Team 專案,它們也可讓另一個開發人員無法從類別或結構的其他位置錯誤地直接呼叫方法。

區域函式語法

區域函式定義為包含成員內的巢狀方法。 其定義具有下列語法:

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

您可以使用下列修飾詞搭配區域函式:

  • async
  • unsafe
  • static 靜態區域函式無法擷取區域變數或實例狀態。
  • extern 外部區域函式必須是 static

在包含成員中定義的所有區域變數,包括其方法參數,都可以在非靜態區域函式中存取。

不同于方法定義,區域函式定義不能包含成員存取修飾詞。 因為所有區域函式都是私用,所以包含 private 這類關鍵字的存取修飾詞會產生編譯器錯誤 CS0106「修飾詞 'private' 對此項目無效」。

下列範例定義名為 AppendPathSeparator 的區域函式,而此區域函式是名為 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 開始,您可以將屬性套用至區域函式、其參數和類型參數,如下列範例所示:

#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;
    }
}

上述範例會使用 特殊屬性 來協助編譯器在可為 Null 的內容中進行靜態分析。

區域函式和例外狀況

區域函式的其中一個有用功能是它們可以允許立即顯示例外狀況。 對於方法迭代器,只有在列舉傳回的序列時,才會顯示例外狀況,而不是擷取迭代器時。 對於非同步方法,等候傳回的工作時,會觀察到非同步方法中擲回的任何例外狀況。

下列範例會 OddSequence 定義列舉指定範圍內奇數的方法。 因為它會將一個大於 100 的數字傳遞至 OddSequence 列舉值方法,所以此方法會擲回 ArgumentOutOfRangeException。 顯示範例輸出時,只有在您逐一查看數字時,才會顯示例外狀況,而不是擷取列舉值時。

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

如果您將反覆運算器邏輯放入本機函式,當您擷取列舉值時,就會擲回引數驗證例外狀況,如下列範例所示:

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

區域函式與 Lambda 運算式的比較

第一眼,區域函式和 Lambda 運算式十分類似。 在許多情況下,使用 Lambda 運算式和區域函式之間的選擇,是樣式和個人喜好設定。 不過,您應該注意可使用任一選項的實際差異。

讓我們檢查階乘演算法的區域函式與 Lambda 運算式實作差異。 以下是使用本機函式的版本:

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

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

此版本使用 Lambda 運算式:

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);
}

命名

區域函式會明確命名為類似方法。 Lambda 運算式是匿名方法,必須指派給類型的變數 delegate ,通常是 ActionFunc 類型。 當您宣告區域函式時,程式就像撰寫一般方法一樣;您宣告傳回型別和函式簽章。

函式簽章和 Lambda 運算式類型

Lambda 運算式依賴指派的變數類型 Action/Func 來判斷引數和傳回型別。 在區域函式中,由於語法非常類似撰寫一般方法,所以引數類型和傳回型別已經是函式宣告的一部分。

從 C# 10 開始,某些 Lambda 運算式具有 自然類型,可讓編譯器推斷 Lambda 運算式的傳回類型和參數類型。

明確指派

Lambda 運算式是在執行時間宣告和指派的物件。 若要使用 Lambda 運算式,它必須明確指派: Action/Func 必須為其指派的變數,以及指派給它的 Lambda 運算式。 請注意, LambdaFactorial 在定義 Lambda 運算式之前,必須先宣告並初始化 Lambda 運算式 nthFactorial 。 如果沒有這麼做的話,系統會在指派 nthFactorial 之前就加以參考,而導致編譯時期錯誤。

本機函式是在編譯時期定義。 當它們未指派給變數時,可以從範圍 中的任何程式碼位置加以參考;在第一個範例 LocalFunctionFactorial 中,我們可以在 語句上方或下方宣告區域函式, return 而不會觸發任何編譯器錯誤。

這些差異表示使用區域函式時,您可以更輕鬆地建立遞迴演算法。 您可以宣告並定義呼叫其本身的區域函式。 Lambda 運算式必須加以宣告並指派預設值,才可以重新指派給參考相同 Lambda 運算式的主體。

以委派身分實作

Lambda 運算式在宣告時會轉換成委派。 區域函式更有彈性,因為可以像傳統方法 委派一樣撰寫。 本機函式只會當做委派 使用 時轉換為委派。

如果您宣告區域函式,並只透過類似方法的呼叫方式來參考它,則不會轉換成委派。

變數擷

明確指派的規則也會影響局部函數或 Lambda 運算式所擷取的任何變數。 編譯器可以執行靜態分析,讓區域函式在封入範圍中明確指派擷取的變數。 請思考此範例:

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

    void LocalFunction() => y = 0;
}

編譯器可判斷 LocalFunction 是否在呼叫時明確指派 y。 由於 LocalFunction 是在 return 陳述式之前呼叫,因此會在 return 陳述式明確指派 y

請注意,當區域函式擷取封入範圍中的變數時,區域函式會實作為委派類型。

堆積配置

根據其使用方式,區域函式可避免 Lambda 運算式一律需要的堆積配置。 如果區域函式永遠不會轉換成委派,而且本機函式所擷取的變數不會由其他 Lambda 或轉換成委派的本機函式擷取,編譯器可以避免堆積配置。

請考慮使用以下非同步範例:

public async 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 await longRunningWorkImplementation();
}

此 Lambda 運算式的 closure 包含 addressindexname 變數。 如果是區域函式,實作關閉的物件可以是 struct 類型。 該結構類型會以傳址方式傳遞至區域函式。 這項實作差異可節省配置資源。

Lambda 運算式所需的具現化代表額外的記憶體配置,這可能會在具時效性的程式碼路徑中成為影響效能的因素。 區域函式不會造成這項額外負荷。 在上述範例中,本機函式版本配置比 Lambda 運算式版本少兩個。

如果您知道本機函式不會轉換成委派,而且其他 Lambda 或轉換成委派的本機函式不會擷取它所擷取的變數,您可以藉由將本機函式宣告為 static 區域函式,確保本機函式避免在堆積上配置。

提示

啟用 .NET 程式碼樣式規則 IDE0062 ,以確保區域函式一律標示為 static

注意

這個方法的對等區域函式也會使用關閉的類別。 不論區域函式的關閉實作為 class 還是 struct 都是實作詳細資料。 區域函式可以使用 struct,而 Lambda 一律會使用 class

public async 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 await 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

此範例中未示範的最後一個優點,在於可以使用 yield return 語法來產生一連串的值,以將區域函式實作為迭代器。

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;
        }
    }
}

yield returnLambda 運算式中不允許語句,請參閱編譯器錯誤 CS1621

雖然區域函式與 Lambda 運算式看似相同,但它們實際上有不同的用途與用法。 如果您要撰寫的函式只會從其他方法的內容進行呼叫時,使用區域函式會更有效率。

另請參閱