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

Místní funkce jsou metody typu, které jsou vnořené do jiného člena. Mohou být volána pouze ze svého obsahujícího člena. Místní funkce lze deklarovat a volat z:

  • Metody, zejména metody iterátoru a asynchronní metody
  • Konstruktory
  • Přístupové objekty vlastností
  • Přístupové objekty událostí
  • Anonymní metody
  • Výrazy lambda
  • Finalizační metody
  • Další místní funkce

Místní funkce však nelze deklarovat uvnitř člena s výrazem.

Poznámka:

V některých případech můžete k implementaci funkcí podporovaných místní funkcí použít výraz lambda. Porovnání najdete v tématu Místní funkce vs. výrazy lambda.

Místní funkce zjasní záměr vašeho kódu. Každý, kdo čte váš kód, může vidět, že metoda není volán s výjimkou obsahující metody. V případě týmových projektů také znemožní jinému vývojáři omylem volat metodu přímo z jiného prostředí třídy nebo struktury.

Syntaxe místní funkce

Místní funkce je definována jako vnořená metoda uvnitř obsahujícího člena. Její definice má následující syntaxi:

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

Poznámka:

Parametr <parameter-list> by neměl obsahovat parametry pojmenované kontextovým klíčovým slovemvalue. Kompilátor vytvoří dočasnou proměnnou "value", která obsahuje odkazované vnější proměnné, které později způsobí nejednoznačnost a může také způsobit neočekávané chování.

S místní funkcí můžete použít následující modifikátory:

  • async
  • unsafe
  • static Statická místní funkce nemůže zachytit místní proměnné ani stav instance.
  • extern Externí místní funkce musí být static.

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

Na rozdíl od definice metody nemůže definice místní funkce obsahovat modifikátor přístupu člena. Protože všechny místní funkce jsou soukromé, včetně modifikátoru přístupu, jako private je například klíčové slovo, generuje chybu kompilátoru CS0106, modifikátor 'private' není platný pro tuto položku.

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

Atributy můžete použít 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 , který kompilátoru pomáhá 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í místních funkcí je, že umožňují okamžité zobrazení výjimek. U metod iterátoru se výjimky zobrazí pouze v případě, že je vrácená sekvence vyčíslována, a ne při načtení iterátoru. U asynchronních metod se při čekání na vrácenou úlohu pozorují všechny výjimky vyvolané v asynchronní metodě.

Následující příklad definuje metodu OddSequence , která vyčíslí lichá čísla v zadané oblasti. Protože předá číslo větší než 100 do OddSequence metody enumerátoru, metoda vyvolá ArgumentOutOfRangeExceptionvýjimku . Jak ukazuje výstup z příkladu, výjimka se zobrazí pouze v případě, že iterujete čísla, a ne při načtení výčtu.

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, při načtení enumerátoru se vyvolá výjimky ověření argumentu, jak ukazuje následující příklad:

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

Místní 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žitím výrazů lambda a místních funkcí otázkou stylu a osobní preference. Existují však skutečné 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 místními funkcemi a implementacemi výrazů lambda faktoriálního algoritmu. Tady je verze využívající místní funkci:

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 potřeba je přiřadit proměnným delegate typu, obvykle buď Action nebo Func typům. Když deklarujete místní funkci, proces se podobá zápisu normální metody; deklarujete návratový typ a podpis funkce.

Podpisy funkcí a typy výrazů lambda

Výrazy lambda spoléhají na typ Action/Func proměnné, kterou jsou přiřazeny k určení argumentu a návratových typů. V místních funkcích je syntaxe podobně jako zápis normální metody, typy argumentů a návratový typ jsou již součástí deklarace funkce.

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

Určité přiřazení

Výrazy lambda jsou objekty, které jsou deklarovány a přiřazeny za běhu. Aby bylo možné výraz lambda použít, musí být rozhodně přiřazen: Action/Func proměnná, ke které bude přiřazena, musí být deklarována a výraz lambda přiřazen k němu. Všimněte si, že LambdaFactorial před definováním musí deklarovat a inicializovat výraz nthFactorial lambda. Výsledkem toho není chyba doby kompilace pro odkazování nthFactorial před jeho přiřazením.

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

Tyto rozdíly znamenají, že rekurzivní algoritmy se snadněji vytvářejí pomocí místních funkcí. Můžete deklarovat a definovat místní funkci, která volá sama sebe. Výrazy lambda musí být deklarovány a před opětovným přiřazením k textu, který odkazuje na stejný výraz lambda, přiřadit výchozí hodnotu.

Implementace jako delegát

Výrazy lambda se při deklaraci převedou na delegáty. Místní funkce jsou flexibilnější, protože můžou být napsané jako tradiční metoda nebo jako delegát. Místní funkce se převedou jenom na delegáty, pokud se používají jako delegát.

Pokud deklarujete místní funkci a odkazujete na ni pouze voláním jako metoda, nebude převedena na delegáta.

Zachycení proměnných

Pravidla určitého přiřazení mají vliv také na proměnné zachycené lokální funkcí nebo výrazem lambda. Kompilátor může provádět statickou analýzu, která umožňuje místním funkcím jednoznačně přiřadit zachycené proměnné v uzavřené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 LocalFunction se při zavolání rozhodně přiřadí y . Protože LocalFunction se volá před příkazem return , y je rozhodně přiřazen k return příkazu.

Všimněte si, že když místní funkce zachycuje proměnné v uzavřeném oboru, místní funkce se implementuje jako typ delegáta.

Přidělení haldy

V závislosti na jejich použití mohou místní funkce zabránit přidělení haldy, které jsou vždy nezbytné pro výrazy lambda. Pokud se místní funkce nikdy nepřevedou na delegáta a žádná z proměnných zachycených místní funkcí se nezachytí jinými lambdami nebo místními funkcemi, které jsou převedeny na delegáty, kompilátor se může vyhnout přidělení haldy.

Podívejte se na 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í pro tento výraz lambda obsahuje addressindex a name proměnné. V případě místních funkcí může být objekt, který implementuje uzavření, typem struct . Tento typ struktury by byl předán odkazem na místní funkci. Tento rozdíl v implementaci by ušetřil přidělení.

Vytvoření instance nezbytné pro výrazy lambda znamená dodatečné přidělení paměti, což může být faktor výkonu v časových kritických cestách kódu. U místních funkcí se tato režie neúčtují. V předchozím příkladu má verze místních funkcí dvě menší 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 v ní nejsou zachyceny jinými lambdami nebo místními funkcemi, které jsou převedeny na delegáty, můžete zaručit, že místní funkce nebude přidělena na haldu tím, že ji deklaruje jako static místní funkci.

Tip

Povolte pravidlo stylu kódu .NET IDE0062 , abyste zajistili, že jsou místní funkce vždy označené static.

Poznámka:

Ekvivalent místní funkce této metody také používá třídu pro uzavření. Zda je uzavření místní funkce implementováno jako class nebo struct je podrobnosti implementace. Místní funkce může používat výraz lambda struct vždy .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í klíčového yield slova

Jednou z konečných výhod, které v této ukázce nejsou demonstrované, je, že místní funkce je možné implementovat jako iterátory pomocí yield return syntaxe k vytvoření posloupnosti 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;
        }
    }
}

Příkaz yield return není povolen ve výrazech lambda. Další informace najdete v tématu Chyba kompilátoru CS1621.

I když se místní funkce můžou zdát nadbytečné pro výrazy lambda, ve skutečnosti slouží různým účelům a mají různá použití. Místní funkce jsou pro případ efektivnější, když chcete napsat funkci, která je volána pouze z kontextu jiné metody.

specifikace jazyka C#

Další informace naleznete v části Deklarace místní funkce specifikace jazyka C#.

Viz také