區域函式 (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
變數型別,判斷引數和傳回型別。 在區域函式中,因為語法與撰寫一般方法非常類似,引數型別和傳回型別已經是函式宣告的一部分。
某些 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
陳述式之前呼叫,因此會在 y
陳述式明確指派 return
。
當區域函式擷取封閉範圍中的變數時,該區域函式會使用閉包來實作,就像委派類型一樣。
堆積配置
根據其使用方式,區域函式可避免 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 表達式的關閉包含 address
、index
和 name
變數。 針對區域函式,實作關閉的物件可以是 struct
類型。 該結構類型會以傳址方式傳遞至區域函式。 這項實作差異可節省配置資源。
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 運算式而言是多餘的,但它們實際上有不同的目的和用途。 如果您要撰寫的函式只會從其他方法的內容進行呼叫時,使用區域函式會更有效率。