Místní funkce (Průvodce programováním v C#)

Počínaje jazykem C# 7,0 podporuje jazyk C# místní funkce. Lokální funkce jsou soukromé metody typu, které jsou vnořené v jiném členu. Mohou být volány pouze ze svých nadřazených členů. Místní funkce mohou být deklarovány v a volány z:

  • Metody, zejména iterátorové metody a asynchronní metody
  • Konstruktory
  • Přistupující objekty vlastnosti
  • Přístupové objekty událostí
  • Anonymní metody
  • Výrazy lambda
  • Finalizační metody
  • Jiné místní funkce

Místní funkce však nelze deklarovat uvnitř člena Expression-těle.

Poznámka

V některých případech můžete použít výraz lambda pro implementaci funkcí, které podporuje také místní funkce. Porovnání naleznete v tématu místní funkce vs. lambda výrazy.

Místní funkce usnadňují záměr vašeho kódu. Každý, kdo čte váš kód, uvidí, že metoda není možné volat s výjimkou obsahující metody. U týmových projektů také znemožňuje, aby jiný vývojář omylem volal metodu přímo z jiného místa ve třídě nebo struktuře.

Syntaxe lokální funkce

Lokální funkce je definována jako vnořená metoda uvnitř nadřazeného člena. Jeho definice má následující syntaxi:

<modifiers> <return-type> <method-name> <parameter-list>

Pomocí místní funkce můžete použít následující modifikátory:

  • async
  • unsafe
  • static (v C# 8,0 a novější). Statická lokální funkce nemůže zachytit místní proměnné nebo stav instance.
  • extern (v C# 9,0 a novější). Externí místní funkce musí být static .

Všechny místní proměnné, které jsou definovány v nadřazeném členu, včetně jeho parametrů metody, jsou přístupné v nestatické místní funkci.

Na rozdíl od definice metody nemůže definice lokální funkce zahrnovat modifikátor přístupu ke členu. Vzhledem k tomu, že všechny místní funkce jsou soukromé, včetně modifikátoru přístupu, jako je private klíčové slovo, vygeneruje chybu kompilátoru CS0106, modifikátor Private není pro tuto položku platný.

Následující příklad definuje místní funkci s názvem AppendPathSeparator , která je soukromá pro metodu s názvem 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 + @"\";
     }
}

Počínaje jazykem C# 9,0 můžete použít atributy na místní funkci, její parametry a parametry typu, jak ukazuje následující příklad:

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

Předchozí příklad používá speciální atribut pro pomoc kompilátoru při statické analýze v kontextu s možnou hodnotou null.

Místní funkce a výjimky

Jednou z užitečných funkcí lokálních funkcí je to, že může dojít k okamžitému povrchování výjimek. U iterátorů metod jsou výjimky vyhodnoceny pouze v případě, že je vyhodnocena vrácená sekvence, a ne při načtení iterátoru. Pro asynchronní metody jsou při očekávaných úlohách pozorovány jakékoli výjimky vyvolané v asynchronní metodě.

Následující příklad definuje OddSequence metodu, která vytváří výčty lichých čísel v zadaném rozsahu. Protože předá metodě Enumerator číslo větší než 100 OddSequence , vyvolá metoda ArgumentOutOfRangeException . Jak výstup z příkladu ukazuje, plochy výjimky pouze při iteraci čísel, a ne při načtení čítače výčtu.

using System;
using System.Collections.Generic;

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

Pokud vložíte logiku iterátoru do místní funkce, jsou vyvolány výjimky ověření argumentů při načtení čítače, jak ukazuje následující příklad:

using System;
using System.Collections.Generic;

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

Lokální funkce vs. výrazy lambda

Na první pohled jsou místní funkce a výrazy lambda velmi podobné. V mnoha případech je volba mezi používáním výrazů lambda a místními funkcemi v oblasti stylu a osobní preference. Existují však reálné rozdíly v tom, kde můžete použít jednu nebo druhou, o které byste měli vědět.

Pojďme se podívat na rozdíly mezi implementací algoritmu faktoriál lokální funkce a výrazu lambda. Tady je verze s použitím místní funkce:

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

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

Tato verze používá výrazy 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);
}

Pojmenování

Místní funkce jsou explicitně pojmenované jako metody. Výrazy lambda jsou anonymní metody a je třeba je přiřadit proměnným delegate typu, obvykle buď Action nebo Func typy. Při deklaraci místní funkce je proces podobný zápisu normální metody; deklarujete návratový typ a signaturu funkce.

Signatury funkcí a typy výrazů lambda

Výrazy lambda spoléhají na typ Action / Func proměnné, ke které jsou přiřazené, aby určily argument a návratové typy. V místních funkcích, protože syntaxe je podobně jako při psaní normální metody, typy argumentů a návratový typ jsou již součástí deklarace funkce.

Počínaje jazykem C# 10 některé výrazy lambda mají přirozený typ, který umožňuje kompilátoru odvodit návratový typ a typy parametrů výrazu lambda.

Jednoznačné přiřazení

Výrazy lambda jsou objekty deklarované a přiřazené v době běhu. Aby se výraz lambda mohl použít, musí být jednoznačně přiřazený: Action / Func Proměnná, ke které se má přiřadit, musí být deklarovaná a má přiřazený výraz lambda. Všimněte si, že LambdaFactorial před definováním je nutné deklarovat a inicializovat výraz lambda nthFactorial . Neprovádí se proto Chyba kompilace pro odkazování nthFactorial před přiřazením.

Místní funkce jsou definovány v době kompilace. Vzhledem k tomu, že nejsou přiřazeny proměnným, mohou být odkazována z libovolného umístění kódu, kde je v oboru; v našem prvním příkladu LocalFunctionFactorial jsme mohli deklarovat naši místní funkci buď nad nebo pod return příkazem a neaktivovat žádné chyby kompilátoru.

Tyto rozdíly znamenají, že rekurzivní algoritmy je snazší vytvořit pomocí místních funkcí. Můžete deklarovat a definovat místní funkci, která volá sama sebe. Lambda výrazy musí být deklarovány a přiřazena výchozí hodnota, aby bylo možné je znovu přiřadit k těle, který odkazuje na stejný výraz lambda.

Implementace jako delegát

Výrazy lambda jsou převedeny na delegáty při jejich deklaraci. Lokální funkce jsou flexibilnější v tom, že mohou být napsány jako tradiční metoda nebo jako delegát. Lokální funkce jsou převedeny pouze na delegáty, kteří se používají jako delegát.

Deklarujete-li místní funkci a pouze na ni odkazujete tak, že ji zavoláte jako metodu, nebude převedena na delegáta.

Zachycení proměnné

Pravidla jednoznačného přiřazení ovlivňují také všechny proměnné, které jsou zachyceny místní funkcí nebo výrazem lambda. Kompilátor může provést statickou analýzu, která umožňuje místním funkcím omezit přiřazení zachycených proměnných v ohraničujícím oboru. Podívejte se na tento příklad:

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

    void LocalFunction() => y = 0;
}

Kompilátor může určit, že se LocalFunction při volání jednoznačně přiřadí y . Protože LocalFunction je volána před return příkazem, y je jednoznačně přiřazen v return příkazu.

Všimněte si, že když místní funkce zachycuje proměnné v ohraničujícím oboru, je místní funkce implementována jako typ delegáta.

Přidělení haldy

V závislosti na jejich použití se můžou místní funkce vyhnout přidělení haldy, které jsou vždy nutné pro výrazy lambda. Pokud místní funkce není nikdy převedena na delegáta a žádná z proměnných zachycených lokální funkcí není zachycena jinými výrazy lambda nebo místními funkcemi, které jsou převedeny na delegáty, kompilátor může vyhnout přidělení haldy.

Vezměte v úvahu tento asynchronní příklad:

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

Uzavření tohoto výrazu lambda obsahuje address index name proměnné a. V případě místních funkcí může být objekt, který implementuje uzávěr, struct typu. Tento typ struktury by byl předán odkazem na místní funkci. Tento rozdíl v implementaci by byl uložen při přidělení.

Vytváření instancí nezbytných pro výrazy lambda znamená dodatečné přidělení paměti, což může být výkonový faktor v časově důležitých cestách kódu. Místní funkce tyto režie neúčtují. V příkladu výše má verze lokálních funkcí dvě méně přidělení než verze výrazu lambda.

Pokud víte, že vaše místní funkce nebude převedena na delegáta a žádná z proměnných zachycených tímto objektem není zachycena jinými výrazy lambda nebo místními funkcemi, které jsou převedeny na delegáty, můžete zaručit, že se místní funkce vyhne přidělení na haldě tím, že ji deklarujete jako static místní funkci. Všimněte si, že tato funkce je dostupná v C# 8,0 a novějších.

Poznámka

Lokální funkce ekvivalentní této metodě používá také třídu pro uzavření. Zda je uzavření místní funkce implementováno jako class nebo jako struct Podrobnosti implementace. Místní funkce může používat, struct zatímco lambda bude vždy používat 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.";
    }
}

Použití yield klíčového slova

Jedna poslední výhoda není v této ukázce znázorněna, protože lokální funkce lze implementovat jako iterátory pomocí yield return syntaxe k vytvoření sekvence hodnot.

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

yield returnPříkaz není ve výrazech lambda povolený, podívejte se na téma Chyba kompilátoru CS1621.

I když se místní funkce můžou jevit jako redundantní pro lambda výrazy, mají ve skutečnosti různé účely a mají odlišná použití. Lokální funkce jsou efektivnější pro případ, když chcete napsat funkci, která je volána pouze z kontextu jiné metody.

Viz také