區域函式 (C# 程式設計手冊)
區域函式是巢狀在另一個成員型別的方法。 它們只可以從其包含成員呼叫。 區域函式可以宣告於下列項目中,以及從下列項目呼叫:
- 方法,特別是迭代器方法和非同步方法
- 建構函式
- 屬性存取子
- 事件存取子
- 匿名方法
- Lambda 運算式
- 完成項
- 其他區域函式
不過,區域函式不能宣告於運算式主體成員內。
注意
在某些情況下,您可以使用 Lambda 運算式來實作區域函式也支援的功能。 如需比較,請參閱區域函式對 Lambda 運算式。
區域函式讓程式碼的意圖更為清楚。 讀取您程式碼的任何人都可以知道無法呼叫這個方法,但包含的方法除外。 對於 Team 專案,它們也可讓另一個開發人員無法從類別或結構的其他位置錯誤地直接呼叫方法。
區域函式語法
區域函式定義為包含成員內的巢狀方法。 其定義具有下列語法:
<modifiers> <return-type> <method-name> <parameter-list>
注意
<parameter-list>
不應該包含以內容關鍵字 value
命名的參數。
編譯器會建立暫存變數「value」,其中包含參考的外部變數,後者稍後會造成模棱兩可,也可能造成非預期的行為。
您可以使用下列修飾詞搭配區域函式:
在非靜態區域函式中可以存取包含成員中所定義的所有區域變數,此區域變數也包含其方法參數。
與方法定義不同,區域函式定義不能包含成員存取修飾詞。 因為所有區域函式都是私用,所以包含 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 + @"\";
}
}
您可以將屬性套用至區域函式、其參數和型別參數,如下列範例所示:
#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
的變數,通常是 Action
或 Func
型別。 當您宣告區域函式時,流程就像寫入一般方法;您會宣告傳回型別和函式簽章。
函式簽章和 Lambda 運算式型別
Lambda 運算式依賴指派的 Action
/Func
變數型別,判斷引數和傳回型別。 在區域函式中,因為語法與撰寫一般方法非常類似,引數型別和傳回型別已經是函式宣告的一部分。
從 C# 10 開始,有些 Lambda 運算式具有自然類型,可讓編譯器推斷 Lambda 運算式的傳回型別和參數型別。
明確指派
Lambda 運算式是在執行階段宣告和指派的物件。 為了使用 Lambda 運算式,必須對其明確指派:必須宣告為其指派的 Action
/Func
變數,以及指派給它的 Lambda 運算式。 請注意,LambdaFactorial
必須在定義 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
。
請注意,當本地函式擷取封閉範圍中的變數時,該本地函式會使用閉包 (closure) 來實作,就像委派類型一樣。
堆積配置
根據其使用方式,區域函式可避免 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 包含 address
、index
和 name
變數。 如果是區域函式,實作關閉的物件可以是 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;
}
}
}
Lambda 運算式中不可以有 yield return
陳述式。 如需詳細資訊,請參閱編譯器錯誤 CS1621。
雖然區域函式與 Lambda 運算式看似相同,但它們實際上有不同的用途與用法。 如果您要撰寫的函式只會從其他方法的內容進行呼叫時,使用區域函式會更有效率。