Sdílet prostřednictvím


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, vidí, že metodu lze volat pouze z metody, která ji obsahuje. 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é s 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 je například klíčové slovo private, generuje 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 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 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.

Některé výrazy lambda mají přirozeného typu, což 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é je 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, lze je odkazovat z libovolného umístění kódu , kde je v oboru; v prvním příkladu LocalFunctionFactorialmůžete deklarovat místní funkci buď před příkazem return, nebo za příkazem return a neaktivovat žádné chyby kompilátoru.

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řiřazeny výchozí hodnotu, aby bylo možné je znovu přiřadit k textu, který odkazuje na stejný výraz lambda.

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 zavoláním metody, nebude převedena na delegáta.

Zachycení proměnných

Pravidla určitého přiřazení mají také vliv na proměnné zachycené místní funkcí nebo lambda výrazem. 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.

Když místní funkce zachycuje proměnné v uzavřeném oboru, místní funkce se implementuje pomocí uzavření, jako jsou typy delegátů.

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í tohoto výrazu lambda obsahuje proměnné address, indexa name. U místních funkcí může být objekt, který implementuje uzavření, typu 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 faktorem výkonu v časově kritických úsecích kódu. U místních funkcí se tento náklad nevyskytuje.

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í ve výrazech lambda povolený. 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é