Lokale Funktionen im Vergleich zu Lambdaausdrücken

Auf den ersten Blick sind lokale Funktionen und Lambdaausdrücke sehr ähnlich. Je nach Ihren Anforderungen sind lokale Funktionen möglicherweise aber eine viel bessere und einfachere Lösung.

Sehen wir uns die Unterschiede zwischen der Implementierungen des Fakultätsalgorithmus als lokale Funktion und als Lambdaausdruck an. Erste die Version mit einer lokalen Funktion:

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

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

Vergleichen Sie diese Implementierung mit einer Version, die Lambdaausdrücke verwendet:

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

Zuerst werden Lambdaausdrücke durch das Instanziieren und Abrufen eines Delegats implementiert. Lokale Funktionen werden als Methodenaufrufe implementiert. Die für Lambdaausdrücke erforderliche Instanziierung bedeutet zusätzliche Speicherbelegung, was ein Leistungsfaktor in zeitkritischen Codepfaden sein kann. Lokale Funktionen erfordern diesen Mehraufwand nicht. Im obigen Beispiel hat die Version mit der lokalen Funktion zwei Zuordnungen weniger als die Version mit dem Lambdaausdruck.

Diese rekursive Methode ist einfach genug, dass die lokale Funktion als private Methode mit einem vom Compiler generierten Namen implementiert wird. Der einzige Unterschied zu anderen privaten Methoden ist, dass sie semantisch innerhalb der äußeren Funktion begrenzt ist.

Zweitens können lokale Funktionen aufgerufen werden, bevor sie definiert werden. Lambdaausdrücke müssen deklariert werden, bevor sie definiert werden. Dies bedeutet, dass lokale Funktionen wie oben dargestellt einfacher in rekursiven Algorithmen verwendet werden.

Beachten Sie, dass die Version mit Lambdaausdrücken den Lambdaausdruck nthFactorial deklarieren und initialisieren muss, bevor er definiert wird. Wird das nicht gemacht, führt dies zu einem Kompilierzeitfehler, weil auf nthFactorial verwiesen wurde, bevor es zugewiesen wurde. Rekursiver Algorithmen sind einfacher mit lokalen Funktion zu erstellen.

Drittens muss der Compiler für Lambdaausdrücke zunächst immer eine anonyme Klasse und eine Instanz dieser Klasse erstellen, um alle Variablen zu speichern, die vom Abschluss erfasst wurden. Betrachten Sie das folgende asynchrone Beispiel:

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

Der Abschluss dieses Lambdaausdrucks enthält die Variablen address, index und name. Im Fall von lokalen Funktionen ist das Objekt, das den Abschluss implementiert, möglicherweise vom Typ struct. Dadurch würde bei einer Zuweisung gespart.

Hinweis

Die Entsprechung dieser Methode mit der lokalen Funktion verwendet auch eine Klasse für den Abschluss. Ob der Abschluss für eine lokale Funktion als class oder struct implementiert wird, ist ein Implementierungsdetail. Eine lokale Funktion verwendet möglicherweise struct, während ein Lambdaausdruck immer class nutzt.

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

Eine letzter Vorteil, der in diesem Beispiel zu kurz gekommen ist, besteht darin, dass lokale Funktionen mithilfe der yield return-Syntax als Iteratoren implementiert werden können, um eine Sequenz von Werten zu erzeugen.

Während lokale Funktionen für Lambdaausdrücke als überflüssig erscheinen, dienen sie tatsächlich anderen Zwecken und haben unterschiedliche Verwendungen. Lokale Funktionen sind effizienter, im Fall dass Sie eine Funktion schreiben möchten, die nur aus dem Kontext einer anderen Methode abgerufen wird.