Lokale Funktionen (C#-Programmierhandbuch)
Lokale Funktionen sind private Methoden eines Typs, die in einem anderen Member geschachtelt sind. Sie können nur aus ihrem enthaltenden Member aufgerufen werden. Lokale Funktionen können deklariert und aufgerufen werden aus:
- Methoden, insbesondere Iteratormethoden und Async-Methoden
- Konstruktoren
- Eigenschaftenaccessoren
- Ereignisaccessoren
- Anonymen Methoden
- Lambdaausdrücke
- Finalizer
- Anderen lokalen Funktionen
Lokale Funktionen können jedoch nicht in einem Ausdruckskörpermember deklariert werden.
Hinweis
In einigen Fällen können Sie einen Lambdaausdruck zum Implementieren von Funktionen verwenden, die auch von einer lokalen Funktion unterstützt werden. Einen Vergleich finden Sie unter Lokale Funktionen im Vergleich zu Lambdaausdrücken.
Lokale Funktionen machen den Zweck Ihres Codes deutlich. Jeder, der Ihren Code liest, kann sehen, dass die Methode nicht aufgerufen werden kann, außer durch die enthaltende Methode. Bei Teamprojekten wird auch verhindert, dass ein anderer Entwickler die Methode versehentlich direkt an anderer Stelle in der Klasse oder Struktur aufruft.
Syntax einer lokalen Funktion
Eine lokale Funktion wird definiert als eine geschachtelte Methode in einem enthaltenden Member. Ihre Definition besitzt die folgende Syntax:
<modifiers> <return-type> <method-name> <parameter-list>
Hinweis
<parameter-list>
sollte die Parameter, die mit dem kontextbezogenen Schlüsselwortvalue
benannt sind, nicht enthalten.
Der Compiler erstellt die temporäre Variable „value“, die die referenzierten äußeren Variablen enthält, was später Mehrdeutigkeit verursacht und auch zu unerwartetem Verhalten führen kann.
Sie können die folgenden Modifizierer mit einer lokalen Funktion verwenden:
async
unsafe
static
Eine statische lokale Funktion kann keine lokalen Variablen oder den Instanzzustand erfassen.extern
Eine externe lokale Funktion mussstatic
sein.
Alle im enthaltenden Member definierten lokalen Variablen, einschließlich der Methodenparameter, sind in einer nicht statischen lokalen Funktion zugänglich.
Im Gegensatz zu einer Methodendefinition kann die Definition einer lokalen Funktion keinen Modifizierer für den Mitgliedszugriff enthalten. Da alle lokalen Funktionen privat sind, führt die Verwendung eines Zugriffsmodifizierers, wie das Schlüsselwort private
, zu dem Compilerfehler CS0106: "Der Modifizierer 'privat' ist für dieses Element nicht gültig."
Das folgende Beispiel definiert eine lokale Funktion mit dem Namen AppendPathSeparator
, die für eine Methode mit dem Namen GetText
privat ist:
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 + @"\";
}
}
Sie können Attribute auf eine lokale Funktion, deren Parameter und Typparameter anwenden, wie das folgende Beispiel zeigt:
#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;
}
}
Im vorherigen Beispiel wird ein spezielles Attribut verwendet, um den Compiler bei der statischen Analyse in einem Nullable-Kontext zu unterstützen.
Lokale Funktionen und Ausnahmen
Eine nützliche Funktion von lokalen Funktionen ist die Tatsache, dass sie Ausnahmen sofort verfügbar machen können. Bei Iterator-Methoden werden Ausnahmen erst sichtbar, wenn die zurückgegebene Sequenz aufgelistet wird, und nicht, wenn der Iterator abgerufen wird. Bei async-Methoden werden Ausnahmen festgestellt, wenn die zurückgegebene Aufgabe erwartet wird.
Das folgende Beispiel definiert eine OddSequence
-Methode, die ungerade Zahlen in einem angegebenen Bereich aufzählt. Da eine Zahl größer als 100 an die OddSequence
-Enumeratormethode übergeben wird, wird ArgumentOutOfRangeException ausgelöst. Die Ausgabe des Beispiels zeigt, dass die Ausnahme erst beim Durchlaufen der Zahlen und nicht beim Abrufen des Enumerators eingeblendet wird.
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
Wenn Sie Iteratorlogik in eine lokale Funktion platzieren, werden Ausnahmen bei der Argumentvalidierung ausgelöst, wenn Sie den Enumerator abrufen. Sehen Sie sich dazu das folgende Beispiel an:
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
Lokale Funktionen im Vergleich zu Lambdaausdrücken
Auf den ersten Blick sind lokale Funktionen und Lambda-Ausdrücke ähnlich. In vielen Fällen ist die Entscheidung zwischen Lambdaausdrücken und lokalen Funktionen eine Frage des Formats und persönlicher Präferenz. Es gibt allerdings tatsächliche Unterschiede, wann das eine oder das andere verwendet werden kann. Diese sollten Ihnen bekannt sein.
Sehen wir uns die Unterschiede zwischen der Implementierungen des Fakultätsalgorithmus als lokale Funktion und als Lambdaausdruck an. Dies ist 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);
}
Diese Version verwendet Lambdaausdrücke:
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);
}
Benennung
Lokale Funktionen werden explizit wie Methoden benannt. Lambdaausdrücke sind anonyme Methoden und müssen Variablen eines delegate
-Typs zugewiesen werden. In der Regel handelt es sich entweder um Action
- oder Func
-Typen. Die Deklaration einer lokalen Funktion erfolgt so ähnlich wie das Schreiben einer normalen Methode. Sie müssen dazu einen Rückgabetyp und eine Funktionssignatur deklarieren.
Funktionssignaturen und Typen für Lambdaausdrücke
Beim Bestimmen der Argument- und Rückgabetypen sind Lambdaausdrücke auf den Typ der Action
/Func
-Variablen angewiesen, der sie zugewiesen werden. Da die Syntax in lokalen Funktionen stark einer normalen Methode ähnelt, sind die Argumenttypen und der Rückgabetyp bereits Teil der Funktionsdeklaration.
Einige Lambdaausdrücke weisen einen natürlichen Typ auf, mit dem der Compiler den Rückgabetyp und die Parametertypen des Lambdaausdrucks ableiten kann.
Definite assignment (Festgelegte Zuweisung)
Lambdaausdrücke sind Objekte, die zur Laufzeit deklariert und zugewiesen werden. Damit ein Lambdaausdruck verwendet werden kann, muss er definitiv zugewiesen werden: die Action
/Func
-Variable, der er zugewiesen wird, muss deklariert werden. Anschließend muss der Lambdaausdruck der Variablen zugewiesen werden. Beachten Sie, dass LambdaFactorial
den Lambdaausdruck nthFactorial
deklarieren und initialisieren muss, bevor dieser definiert wird. Wird das nicht gemacht, führt dies zu einem Kompilierzeitfehler, weil auf nthFactorial
verwiesen wurde, bevor es zugewiesen wurde.
Lokale Funktionen werden zur Kompilierzeit definiert. Da sie keinen Variablen zugewiesen werden, kann an jeder Stelle im Code innerhalb des Gültigkeitsbereichs der Funktion darauf verwiesen werden. Im ersten Beispiel LocalFunctionFactorial
konnten Sie die lokale Funktion entweder vor oder nach der return
-Anweisung deklarieren, ohne Compilerfehler auszulösen.
Diese Unterschiede bedeuten, dass rekursive Algorithmen mit lokalen Funktionen leichter erstellt werden können. Sie können eine lokale Funktion deklarieren und definieren, die sich selbst aufruft. Lambda-Ausdrücke müssen deklariert und einem Standardwert zugewiesen werden, bevor sie einem Textkörper zugewiesen werden können, der auf denselben Lambda-Ausdruck verweist.
Implementierung als Delegat
Lambdaausdrücke werden bei der Deklaration in Delegate konvertiert. Lokale Funktionen sind flexibler, da sie wie eine herkömmliche Methode oder als Delegat geschrieben werden können. Lokale Funktionen werden nur dann in Delegate konvertiert, wenn sie als Delegate verwendet werden.
Wenn Sie eine lokale Funktion deklarieren und nur darauf verweisen, indem Sie sie wie eine Methode aufrufen, wird sie nicht in einen Delegaten konvertiert.
Erfassung von Variablen
Die Regeln für definitive Zuweisungen gelten auch für alle Variablen, die von der lokalen Funktion oder dem Lambdaausdruck erfasst werden. Zudem kann der Compiler statische Analysen durchführen, mit denen lokale Funktionen erfasste Variablen im einschließenden Gültigkeitsbereich definitiv zuweisen können. Betrachten Sie das folgende Beispiel:
int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}
Der Compiler kann festlegen, dass LocalFunction
y
bei Aufruf definitiv zuweist. Da LocalFunction
vor der return
-Anweisung aufgerufen wird, wird y
definitiv bei der return
-Anweisung zugewiesen.
Wenn eine lokale Funktion als Delegattyp implementiert wird, wird sie wie Delegat-Typen mit einem Abschluss implementiert.
Heapzuweisungen
Je nach Verwendung können lokale Funktionen Heapzuweisungen vermeiden, die immer für Lambdaausdrücke erforderlich sind. Wenn eine lokale Funktion nie in einen Delegaten konvertiert wird und keine der von der lokalen Funktion erfassten Variablen von anderen Lambdaausdrücken oder lokalen Funktionen, die in Delegate konvertiert werden, erfasst wird, kann der Compiler Heapzuweisungen vermeiden.
Betrachten Sie das folgende asynchrone Beispiel:
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();
}
Der Abschluss dieses Lambdaausdrucks enthält die Variablen address
, index
und name
. Bei lokalen Funktionen kann das Objekt, das die Schließung implementiert, ein Typ struct
sein. Dieser struct-Typ würde per Verweis an die lokale Funktion übergeben. Dieser Unterschied bei der Implementierung würde bei einer Zuweisung gespart.
Die für Lambda-Ausdrücke erforderliche Instanziierung bedeutet zusätzliche Speicherzuweisungen, was möglicherweise ein Leistungsfaktor in zeitkritischen Codepfaden sein kann. Lokale Funktionen verursachen diesen Aufwand nicht.
Wenn Sie wissen, dass Ihre lokale Funktion nicht in einen Delegaten konvertiert wird und dass keine der von ihr erfassten Variablen auch von anderen Lambdaausdrücken oder lokalen Funktionen, die in Delegate konvertiert werden, erfasst wird, können Sie durch Deklarieren der lokalen Funktion als statisch (static
) verhindern, dass Ihre lokale Funktion im Heap zugewiesen wird.
Tipp
Aktivieren Sie die .NET-Codeformatregel IDE0062, um sicherzustellen, dass lokale Funktionen immer mit static
gekennzeichnet sind.
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 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.";
}
}
Verwendung des Schlüsselworts yield
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.
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;
}
}
}
Die yield return
-Anweisung ist in Lambdaausdrücken unzulässig. Weitere Informationen finden Sie unter Compilerfehler CS1621.
Während lokale Funktionen redundant für Lambda-Ausdrücke erscheinen können, dienen sie tatsächlich unterschiedlichen Zwecken und weisen unterschiedliche Verwendungen auf. Lokale Funktionen sind effizienter, im Fall dass Sie eine Funktion schreiben möchten, die nur aus dem Kontext einer anderen Methode abgerufen wird.
C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Lokale Funktionsdeklarationen der C#-Sprachspezifikation.