Funkcje lokalne (Przewodnik programowania w języku C#)
Funkcje lokalne to metody typu zagnieżdżonego w innym elemencie członkowskim. Mogą być wywoływane tylko ze swojego zawierającego elementu członkowskiego. Funkcje lokalne mogą być deklarowane w i wywoływane z:
- Metody, zwłaszcza metody iteracyjne i metody asynchroniczne
- Konstruktory
- Akcesory
- Akcesory zdarzeń
- Metody anonimowe
- Wyrażenia lambda
- Finalizatory
- Inne funkcje lokalne
Nie można jednak zadeklarować funkcji lokalnych wewnątrz elementu członkowskiego wyrażeń.
Uwaga
W niektórych przypadkach można użyć wyrażenia lambda, aby zaimplementować funkcje obsługiwane również przez funkcję lokalną. Aby uzyskać porównanie, zobacz Funkcje lokalne a wyrażenia lambda.
Funkcje lokalne umożliwiają czyszczenie intencji kodu. Każda osoba czytająca kod może zobaczyć, że metoda nie jest wywoływana z wyjątkiem metody zawierającej. W przypadku projektów zespołowych uniemożliwią również innemu deweloperowi błędne wywołanie metody bezpośrednio z innego miejsca w klasie lub w strukturę.
Składnia funkcji lokalnej
Funkcja lokalna jest definiowana jako metoda zagnieżdżona wewnątrz elementu członkowskiego zawierającego. Jego definicja ma następującą składnię:
<modifiers> <return-type> <method-name> <parameter-list>
Uwaga
Parametr <parameter-list>
nie powinien zawierać parametrów o nazwie z kontekstowym słowem kluczowym value
.
Kompilator tworzy zmienną tymczasową "value", która zawiera przywoływane zmienne zewnętrzne, co później powoduje niejednoznaczność i może również spowodować nieoczekiwane zachowanie.
Można użyć następujących modyfikatorów z funkcją lokalną:
async
unsafe
static
Statyczna funkcja lokalna nie może przechwytywać zmiennych lokalnych ani stanu wystąpienia.extern
Zewnętrzna funkcja lokalna musi mieć wartośćstatic
.
Wszystkie zmienne lokalne zdefiniowane w zawierającym elemencie członkowskim, w tym jego parametry metody, są dostępne w niestacjonanej funkcji lokalnej.
W przeciwieństwie do definicji metody definicja funkcji lokalnej nie może zawierać modyfikatora dostępu do składowych. Ponieważ wszystkie funkcje lokalne są prywatne, w tym modyfikator dostępu, taki jak private
słowo kluczowe, generuje błąd kompilatora CS0106, "Modyfikator "prywatny" jest nieprawidłowy dla tego elementu.
W poniższym przykładzie zdefiniowano funkcję lokalną o nazwie AppendPathSeparator
prywatną dla metody o nazwie 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 + @"\";
}
}
Atrybuty można zastosować do funkcji lokalnej, jej parametrów i parametrów typu, jak pokazano w poniższym przykładzie:
#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;
}
}
W poprzednim przykładzie użyto specjalnego atrybutu , aby pomóc kompilatorowi w analizie statycznej w kontekście dopuszczającym wartość null.
Funkcje lokalne i wyjątki
Jedną z przydatnych funkcji lokalnych jest możliwość natychmiastowego zezwalania na odejmowania wyjątków. W przypadku metod iteratora wyjątki są widoczne tylko wtedy, gdy zwracana sekwencja jest wyliczana, a nie po pobraniu iteratora. W przypadku metod asynchronicznych wszelkie wyjątki zgłoszone w metodzie asynchronicznej są obserwowane, gdy zwracane zadanie jest oczekiwane.
W poniższym przykładzie zdefiniowano metodę OddSequence
, która wylicza liczby nieparzyste w określonym zakresie. Ponieważ przekazuje liczbę większą niż 100 do metody modułu OddSequence
wyliczającego, metoda zgłasza błąd ArgumentOutOfRangeException. Jak pokazano w danych wyjściowych z przykładu, wyjątek pojawia się tylko wtedy, gdy iterujesz liczby, a nie podczas pobierania modułu wyliczającego.
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
Jeśli logika iteratora zostanie umieszczona w funkcji lokalnej, podczas pobierania modułu wyliczającego są zgłaszane wyjątki weryfikacji argumentów, jak pokazano w poniższym przykładzie:
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
Funkcje lokalne a wyrażenia lambda
Na pierwszy rzut oka funkcje lokalne i wyrażenia lambda są bardzo podobne. W wielu przypadkach wybór między używaniem wyrażeń lambda a funkcjami lokalnymi jest kwestią preferencji stylu i osobistych. Istnieją jednak rzeczywiste różnice, w których można użyć jednego lub drugiego, o którym należy pamiętać.
Przyjrzyjmy się różnicom między funkcją lokalną a implementacjami wyrażeń lambda algorytmu czynnikowego. Oto wersja używająca funkcji lokalnej:
public static int LocalFunctionFactorial(int n)
{
return nthFactorial(n);
int nthFactorial(int number) => number < 2
? 1
: number * nthFactorial(number - 1);
}
Ta wersja używa wyrażeń 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);
}
Nazewnictwo
Funkcje lokalne są jawnie nazywane metodami takimi jak. Wyrażenia lambda są metodami anonimowymi i muszą być przypisane do zmiennych delegate
typu, zazwyczaj Action
lub Func
typów. Podczas deklarowania funkcji lokalnej proces jest podobny do pisania normalnej metody; deklarujesz zwracany typ i podpis funkcji.
Podpisy funkcji i typy wyrażeń lambda
Wyrażenia lambda opierają się na typie Action
/Func
zmiennej przypisanej do określania argumentu i zwracanych typów. W funkcjach lokalnych, ponieważ składnia jest podobna do pisania normalnej metody, typy argumentów i typ zwracany są już częścią deklaracji funkcji.
Począwszy od języka C# 10, niektóre wyrażenia lambda mają typ naturalny, co umożliwia kompilatorowi wnioskowanie zwracanego typu i typów parametrów wyrażenia lambda.
Określone przypisanie
Wyrażenia lambda to obiekty zadeklarowane i przypisane w czasie wykonywania. Aby można było użyć wyrażenia lambda, musi być zdecydowanie przypisane: Action
/Func
zmienna, do której zostanie przypisana, musi zostać zadeklarowana, a przypisane do niego wyrażenie lambda. Zwróć uwagę, że LambdaFactorial
przed zdefiniowaniem wyrażenia nthFactorial
lambda należy zadeklarować i zainicjować je. Nie powoduje to wystąpienia błędu czasu kompilacji w przypadku odwoływania nthFactorial
się do niego przed przypisaniem.
Funkcje lokalne są definiowane w czasie kompilacji. Ponieważ nie są one przypisane do zmiennych, można odwoływać się do nich z dowolnej lokalizacji kodu, w której znajduje się ona w zakresie. W naszym pierwszym przykładzie LocalFunctionFactorial
możemy zadeklarować naszą funkcję lokalną powyżej lub poniżej return
instrukcji i nie wyzwalać żadnych błędów kompilatora.
Te różnice oznaczają, że algorytmy cyklicznego są łatwiejsze do tworzenia przy użyciu funkcji lokalnych. Można zadeklarować i zdefiniować funkcję lokalną, która wywołuje samą siebie. Wyrażenia lambda muszą być zadeklarowane i przypisane wartości domyślne, zanim będą mogły zostać ponownie przypisane do treści odwołującej się do tego samego wyrażenia lambda.
Implementacja jako delegat
Wyrażenia lambda są konwertowane na delegatów po ich zadeklarowaniu. Funkcje lokalne są bardziej elastyczne, ponieważ mogą być napisane jak tradycyjna metoda lub jako delegat. Funkcje lokalne są konwertowane tylko na delegatów, gdy są używane jako delegat.
Jeśli deklarujesz funkcję lokalną i odwołujesz się tylko do niej, wywołując ją jak metodę, nie zostanie ona przekonwertowana na delegata.
Przechwytywanie zmiennych
Reguły określonego przypisania mają również wpływ na wszystkie zmienne przechwycone przez funkcję lokalną lub wyrażenie lambda. Kompilator może wykonywać analizę statyczną, która umożliwia funkcjom lokalnym zdecydowanie przypisanie przechwyconych zmiennych w otaczającym zakresie. Rozważ taki przykład:
int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}
Kompilator może określić, że LocalFunction
zdecydowanie przypisuje y
element po wywołaniu. Ponieważ LocalFunction
jest wywoływana przed instrukcją return
, y
jest zdecydowanie przypisywana w instrukcji return
.
Należy pamiętać, że gdy funkcja lokalna przechwytuje zmienne w otaczającym zakresie, funkcja lokalna jest implementowana przy użyciu zamknięcia, takiego jak typy delegatów.
Alokacje sterty
W zależności od ich użycia funkcje lokalne mogą unikać alokacji sterty, które są zawsze niezbędne dla wyrażeń lambda. Jeśli funkcja lokalna nigdy nie jest konwertowana na delegata, a żadna ze zmiennych przechwyconych przez funkcję lokalną nie jest przechwytywana przez inne funkcje lambda lub lokalne, które są konwertowane na delegatów, kompilator może uniknąć alokacji sterty.
Rozważmy ten przykład asynchroniczny:
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();
}
Zamknięcie tego wyrażenia lambda zawiera address
zmienne i . index
name
W przypadku funkcji lokalnych obiekt implementujący zamknięcie może być typem struct
. Ten typ struktury zostanie przekazany przez odwołanie do funkcji lokalnej. Ta różnica w implementacji pozwoli zaoszczędzić na alokacji.
Wystąpienie niezbędne dla wyrażeń lambda oznacza dodatkowe alokacje pamięci, które mogą być czynnikiem wydajności ścieżek kodu o krytycznym czasie. Funkcje lokalne nie wiążą się z tym obciążeniem. W powyższym przykładzie wersja funkcji lokalnych ma dwie mniej alokacji niż wersja wyrażenia lambda.
Jeśli wiesz, że funkcja lokalna nie zostanie przekonwertowana na delegata i żadna ze zmiennych przechwyconych przez nią nie zostanie przechwycona przez inne funkcje lambda lub lokalne, które są konwertowane na delegaty, możesz zagwarantować, że funkcja lokalna nie zostanie przydzielona na stercie, deklarując ją jako funkcję lokalną static
.
Napiwek
Włącz regułę stylu kodu platformy .NET IDE0062, aby upewnić się, że funkcje lokalne są zawsze oznaczone .static
Uwaga
Odpowiednik funkcji lokalnej tej metody używa również klasy do zamknięcia. Niezależnie od tego, czy zamknięcie funkcji lokalnej jest implementowane jako , class
czy jest struct
szczegółem implementacji. Funkcja lokalna może używać struct
elementu , natomiast funkcja lambda zawsze będzie używać elementu 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.";
}
}
Użycie słowa kluczowego yield
Jedną z ostatecznych zalet, które nie zostały przedstawione w tym przykładzie, jest to, że funkcje lokalne mogą być implementowane jako iteratory przy użyciu yield return
składni w celu utworzenia sekwencji wartości.
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;
}
}
}
Instrukcja yield return
nie jest dozwolona w wyrażeniach lambda. Aby uzyskać więcej informacji, zobacz błąd kompilatora CS1621.
Chociaż funkcje lokalne mogą wydawać się nadmiarowe dla wyrażeń lambda, faktycznie służą one różnym celom i mają różne zastosowania. Funkcje lokalne są bardziej wydajne w przypadku, gdy chcesz napisać funkcję wywoływaną tylko z kontekstu innej metody.
specyfikacja języka C#
Aby uzyskać więcej informacji, zobacz sekcję Lokalne deklaracje funkcji specyfikacji języka C#.