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 kluczowymvalue. 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 LocalFunctionFactorialmoż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 jako typ delegata.

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 addresszmienne i . indexname 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#.

Zobacz też