ローカル関数とラムダ式の比較Local functions compared to lambda expressions

一見したところ、ローカル関数ラムダ式は、非常に似ています。At first glance, local functions and lambda expressions are very similar. 多くの場合、ラムダ式とローカル関数の使用のどちらを選択するかは、スタイルと個人的な好みの問題です。In many cases, the choice between using lambda expressions and local functions is a matter of style and personal preference. ただし、どちらか一方を使用できる場合、認識しておくべき実質的な違いがあります。However, there are real differences in where you can use one or the other that you should be aware of.

階乗アルゴリズムのローカル関数とラムダ式の実装の違いについて見てみましょう。Let's examine the differences between the local function and lambda expression implementations of the factorial algorithm. まずは、ローカル関数を使用するバージョンです。First the version using a local function:

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

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

ラムダ式を使用するバージョンの実装と比較します。Contrast that implementation with a version that uses lambda expressions:

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

ローカル関数には名前があります。The local functions have names. ラムダ式は匿名メソッドであり、Func または Action 型である変数に割り当てられます。The lambda expressions are anonymous methods that are assigned to variables that are Func or Action types. ローカル関数を宣言する場合、引数の型と戻り値の型は関数宣言の一部となります。When you declare a local function, the argument types and return type are part of the function declaration. 引数の型と戻り値の型は、ラムダ式の本体の一部ではなく、ラムダ式の変数型宣言の一部となります。Instead of being part of the body of the lambda expression, the argument types and return type are part of the lambda expression's variable type declaration. これら 2 つの違いにより、コードがわかりやすくなる場合があります。Those two differences may result in clearer code.

ローカル関数には、ラムダ式とは異なる確実な代入のルールがあります。Local functions have different rules for definite assignment than lambda expressions. ローカル関数宣言は、スコープ内にある任意のコードの場所から参照できます。A local function declaration can be referenced from any code location where it is in scope. ラムダ式はデリゲート変数に割り当てないと、アクセスできません (ラムダ式を参照するデリゲートを通じて呼び出すこともできません)ラムダ式を使用したバージョンでは、ラムダ式 nthFactorial を定義する前に、宣言と初期化を行う必要があります。A lambda expression must be assigned to a delegate variable before it can be accessed (or called through the delegate referencing the lambda expression.) Notice that the version using the lambda expression must declare and initialize the lambda expression, nthFactorial before defining it. その手順を踏まないと、nthFactorial の割り当て前に参照することによるコンパイル時エラーが発生します。Not doing so results in a compile time error for referencing nthFactorial before assigning it. これらの違いは、再帰的なアルゴリズムの作成はローカル関数を使用する方が簡単であることを意味します。These differences mean that recursive algorithms are easier to create using local functions. 自身を呼び出すローカル関数を宣言して定義することができます。You can declare and define a local function that calls itself. ラムダ式は宣言して、既定値を割り当てないと、同じラムダ式を参照する本体に再割り当てできません。Lambda expressions must be declared, and assigned a default value before they can be re-assigned to a body that references the same lambda expression.

確実な代入ルールは、ローカル関数またはラムダ式でキャプチャされる変数にも影響します。Definite assignment rules also affect any variables that are captured by the local function or lambda expression. ローカル関数とラムダ式の両方のルールでは、ローカル関数またはラムダ式がデリゲートに変換された時点で、キャプチャされた変数が確実に代入されることが要求されます。Both local functions and lambda expression rules demand that any captured variables are definitely assigned at the point when the local function or lambda expression is converted to a delegate. 違いは、ラムダ式が宣言時にデリゲートに変換されることです。The difference is that lambda expressions are converted to delegates when they are declared. ローカル関数は、デリゲートとして使用される場合にのみ、デリゲートに変換されます。Local functions are converted to delegates only when used as a delegate. ローカル関数を宣言し、メソッドのように呼び出して参照のみを行う場合は、デリゲートに変換されません。If you declare a local function and only reference it by calling it like a method, it will not be converted to a delegate. このルールでは、外側のスコープ内の便利な場所でローカル関数を宣言できます。That rule enables you to declare a local function at any convenient location in its enclosing scope. 親メソッドの末尾 (すべての return ステートメントの後) にローカル関数を宣言するのが一般的です。It's common to declare local functions at the end of the parent method, after any return statements.

3 つ目は、コンパイラは静的分析を実行できることです。これにより、ローカル関数で外側のスコープ内のキャプチャされた変数を確実に割り当てることができます。Third, the compiler can perform static analysis that enables local functions to definitely assign captured variables in the enclosing scope. 次の例について考えます。Consider this example:

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

    void LocalFunction() => y = 0;
}

コンパイラは、呼び出し時に LocalFunctiony を確実に割り当てるかどうかを確認できます。The compiler can determine that LocalFunction definitely assigns y when called. return ステートメントの前に LocalFunction が呼び出されるため、yreturn ステートメントで確実に割り当てられます。Because LocalFunction is called before the return statement, y is definitely assigned at the return statement.

分析例を使用する分析では、4 つ目の違いを確認できます。The analysis that enables the example analysis enables the fourth difference. ローカル関数では、その使用に応じて、ラムダ式では常に必要なヒープの割り当てを回避できます。Depending on their use, local functions can avoid heap allocations that are always necessary for lambda expressions. ローカル関数がデリゲートに変換されておらず、ローカル関数でキャプチャされたいずれの変数も、デリゲートに変換された他のラムダやローカル関数でキャプチャされていない場合、コンパイラはヒープの割り当てを回避できます。If a local function is never converted to a delegate, and none of the variables captured by the local function is captured by other lambdas or local functions that are converted to delegates, the compiler can avoid heap allocations.

次の非同期の例について考えます。Consider this async example:

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

このラムダ式のクロージャに含まれるのは、addressindex、および name 変数です。The closure for this lambda expression contains the address, index and name variables. ローカル関数の場合、クロージャを実装するオブジェクトが struct になる場合があります。In the case of local functions, the object that implements the closure may be a struct type. その構造体型はローカル関数に参照によって渡されます。That struct type would be passed by reference to the local function. この実装の違いにより、割り当てが少なくなります。This difference in implementation would save on an allocation.

ラムダ式に必要なインスタンス化では、余分なメモリの割り当てが必要となり、タイム クリティカルなコード パスに影響を与えるパフォーマンス因子となる可能性があります。The instantiation necessary for lambda expressions means extra memory allocations, which may be a performance factor in time-critical code paths. ローカル関数では、このオーバーヘッドは発生しません。Local functions do not incur this overhead. 上記の例では、ローカル関数のバージョンは、ラムダ式のバージョンよりも割り当てが 2 つ少なくなっています。In the example above, the local functions version has 2 fewer allocations than the lambda expression version.

注意

このメソッドのローカル関数と同等のものも、同じクロージャのクラスを使用します。The local function equivalent of this method also uses a class for the closure. ローカル関数のクロージャが class として実装される場合でも、実装の詳細が struct である場合でも同様です。Whether the closure for a local function is implemented as a class or a struct is an implementation detail. ローカル関数は struct を使用する場合がありますが、ラムダは常に class を使用します。A local function may use a struct whereas a lambda will always use a class.

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

    async Task<string> longRunningWorkImplementation()
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    }
}

この例では説明しませんが、最後の 1 つの利点は値のシーケンスを生成するために yield return 構文を使用して、ローカル関数を反復子として実装できることです。One final advantage not demonstrated in this sample is that local functions can be implemented as iterators, using the yield return syntax to produce a sequence of values. ラムダ式では yield return ステートメントは許可されません。The yield return statement is not allowed in lambda expressions.

ローカル関数はラムダ式より冗長に思えるかもしれませんが、実際にはさまざまな目的に役立ち、用途もさまざまです。While local functions may seem redundant to lambda expressions, they actually serve different purposes and have different uses. ローカル関数は、別のメソッドのコンテキストからのみ呼び出される関数を記述する場合に、より効率が高くなります。Local functions are more efficient for the case when you want to write a function that is called only from the context of another method.