ローカル関数 (C# プログラミング ガイド)
''ローカル関数'' は、別のメンバーの入れ子になっているタイプのメソッドです。 親メンバーからのみ呼び出すことができます。 ローカル関数は次の要素で宣言し、呼び出すことができます。
- メソッド (特に反復子メソッドと非同期メソッド)
- コンストラクター
- プロパティ アクセサー
- イベント アクセサー
- 匿名メソッド
- ラムダ式
- ファイナライザー
- その他のローカル関数
ただし、ローカル関数は、式形式のメンバーの内部では宣言できません。
注意
場合によっては、ラムダ式を使用して、ローカル関数でもサポートされている機能を実装できます。 比較については、「ローカル関数とラムダ式の比較」を参照してください。
ローカル関数を使用すると、コードの意図が明確になります。 コードを読んでいる人は誰でも、メソッドを含むメソッドを除いて呼び出し可能ではないことを確認できます。 また、チーム プロジェクトの場合は、別の開発者がクラスや構造体の別の場所から誤ってメソッドを直接呼び出すことができなくなります。
ローカル関数の構文
ローカル関数は、親メンバーの内側に、入れ子になったメソッドとして定義されます。 その定義の構文は次のとおりです。
<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 許容コンテキストでの静的分析に関してコンパイラをサポートしています。
ローカル関数と例外
ローカル関数の便利な機能の 1 つは、例外を直ちに検出できることです。 反復子メソッドの場合、例外は返されたシーケンスを列挙する時点でしか検出されず、反復子を取得した時点では検出されません。 非同期メソッドの場合、非同期メソッドでスローされた例外は、タスクの戻りを待機中に検出されます。
次の例は、指定した範囲にある奇数を列挙する 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
ローカル関数とラムダ式の比較
一見すると、ローカル関数と ラムダ式 似ています。 多くの場合、ラムダ式とローカル関数の使用のどちらを選択するかは、スタイルと個人的な好みの問題です。 ただし、どちらか一方を使用できる場合、認識しておくべき実質的な違いがあります。
階乗アルゴリズムのローカル関数とラムダ式の実装の違いについて見てみましょう。 ローカル関数を使用するバージョンを次に示します。
public static int LocalFunctionFactorial(int n)
{
return nthFactorial(n);
int nthFactorial(int number) => number < 2
? 1
: number * nthFactorial(number - 1);
}
このバージョンでは、ラムダ式が使用されます。
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);
}
名前を付ける
ローカル関数には、メソッドと同様に明示的に名前が付けられます。 ラムダ式は匿名メソッドであり、delegate
型の変数 (通常は Action
型または Func
型) に割り当てる必要があります。 ローカル関数を宣言する場合、プロセスは通常のメソッドを記述するのと似ています。戻り値の型と関数シグネチャを宣言します。
関数シグネチャとラムダ式の型
ラムダ式は、引数と戻り値の型を決定するために割り当てられている Action
/Func
変数の型に依存します。 ローカル関数では、構文は通常のメソッドの記述とよく似ているため、引数の型と戻り値の型は既に関数宣言の一部になっています。
一部のラムダ式には、自然型があり、これにより、コンパイラはラムダ式の戻り値の型とパラメーター型を推論できます。
確実な代入
ラムダ式は、実行時に宣言されて割り当てられるオブジェクトです。 ラムダ式を使用するには、ラムダ式を確実に割り当てる必要があります。割り当てる Action
/Func
変数を宣言し、それにラムダ式を割り当てる必要があります。 LambdaFactorial
では、ラムダ式 nthFactorial
を定義する前に、宣言と初期化を行う必要があることにご注意ください。 その手順を踏まないと、nthFactorial
の割り当て前に参照することによるコンパイル時エラーが発生します。
ローカル関数は、コンパイル時に定義されます。 変数に割り当てられないため、スコープ内の任意のコード位置 から参照できます。最初の LocalFunctionFactorial
例では、return
ステートメントの前または後にローカル関数を宣言し、コンパイラ エラーをトリガーすることはできません。
これらの違いは、再帰的なアルゴリズムの作成はローカル関数を使用する方が簡単であることを意味します。 自身を呼び出すローカル関数を宣言して定義することができます。 ラムダ式は、同じラムダ式を参照する本体に再割り当てする前に、宣言して既定値を割り当てる必要があります。
デリゲートとしての実装
ラムダ式は、宣言時にデリゲートに変換されます。 ローカル関数は、従来のメソッド "または" デリゲートと同様に記述できるので、より柔軟性があります。 ローカル関数は、デリゲートとして "使用される" 場合にのみ、デリゲートに変換されます。
ローカル関数を宣言し、メソッドのように呼び出すことによってのみ参照する場合、デリゲートには変換されません。
変数のキャプチャ
明確な代入 の規則は、ローカル関数またはラムダ式によってキャプチャされるすべての変数にも影響します。 コンパイラによって静的分析を実行できます。これにより、ローカル関数で外側のスコープ内のキャプチャされた変数を確実に割り当てることができます。 次の例について考えます。
int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}
コンパイラは、呼び出し時に LocalFunction
が y
を確実に割り当てるかどうかを確認できます。 LocalFunction
ステートメントの前に return
が呼び出されるため、y
は return
ステートメントで確実に割り当てられます。
ローカル関数が外側のスコープ内の変数をキャプチャする場合、デリゲート型と同様に、ローカル関数はクロージャを使用して実装されます。
ヒープの割り当て
ローカル関数では、その使用に応じて、ラムダ式では常に必要なヒープの割り当てを回避できます。 ローカル関数がデリゲートに変換されておらず、ローカル関数でキャプチャされたいずれの変数も、デリゲートに変換された他のラムダやローカル関数でキャプチャされていない場合は、コンパイラによってヒープの割り当てを回避できます。
次の非同期の例について考えます。
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();
}
このラムダ式のクロージャには、address
、index
、および name
変数が含まれています。 ローカル関数の場合、クロージャを実装するオブジェクトは struct
型にすることができます。 その構造体型はローカル関数に参照によって渡されます。 この実装の違いにより、割り当てが少なくなります。
ラムダ式に必要なインスタンス化は、余分なメモリ割り当てを意味します。これは、タイム クリティカルなコード パスのパフォーマンス要因になる可能性があります。 ローカル関数では、このオーバーヘッドは発生しません。
ローカル関数がデリゲートに変換されず、それによってキャプチャされた変数が、デリゲートに変換された他のラムダまたはローカル関数によってキャプチャされないことがわかっている場合は、ローカル関数を static
ローカル関数として宣言することで、ヒープに割り当てられないようにすることができます。
ヒント
.NET コード スタイル規則 IDE0062 を有効にして、ローカル関数に常に static
がマークされるようにします。
注意
このメソッドのローカル関数と同等のものも、同じクロージャのクラスを使用します。 ローカル関数のクロージャが class
として実装される場合でも、実装の詳細が struct
である場合でも同様です。 ローカル関数は struct
を使用する場合がありますが、ラムダは常に 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
キーワードの使用法
この例では説明しませんが、最後の 1 つの利点は値のシーケンスを生成するために 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 return
ステートメントは使用できません。 詳細については、「コンパイラ エラー CS1621」を参照してください。
ローカル関数はラムダ式に対して冗長に見えるかもしれませんが、実際には異なる目的を果たし、用途が異なります。 ローカル関数は、別のメソッドのコンテキストからのみ呼び出される関数を記述する場合に、より効率が高くなります。
C# 言語仕様
詳細については、「C# 言語仕様」のローカル関数の宣言に関するセクションを参照してください。
関連項目
.NET