Dela via


Lokala funktioner (C#-programmeringsguide)

Lokala funktioner är metoder av en typ som är kapslade i en annan medlem. De kan bara anropas från sin innehållande medlem. Lokala funktioner kan deklareras i och anropas från:

  • Metoder, särskilt iteratormetoder och asynkrona metoder
  • Konstruktorer
  • Egenskapsåtkomster
  • Händelseåtkomster
  • Anonyma metoder
  • Lambda-uttryck
  • Slutförare
  • Andra lokala funktioner

Lokala funktioner kan dock inte deklareras i en uttrycksbaserad medlem.

Kommentar

I vissa fall kan du använda ett lambda-uttryck för att implementera funktioner som också stöds av en lokal funktion. En jämförelse finns i Lokala funktioner jämfört med lambda-uttryck.

Lokala funktioner gör kodens avsikt tydlig. Alla som läser koden kan se att metoden inte kan anropas förutom med den innehållande metoden. För teamprojekt gör de det också omöjligt för en annan utvecklare att av misstag anropa metoden direkt från någon annanstans i klassen eller struct.

Lokal funktionssyntax

En lokal funktion definieras som en kapslad metod i en innehållande medlem. Definitionen har följande syntax:

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

Kommentar

<parameter-list> Får inte innehålla parametrarna med namnet med kontextuellt nyckelordvalue. Kompilatorn skapar den tillfälliga variabeln "value", som innehåller de refererade yttre variablerna, vilket senare orsakar tvetydighet och kan också orsaka ett oväntat beteende.

Du kan använda följande modifierare med en lokal funktion:

  • async
  • unsafe
  • static En statisk lokal funktion kan inte samla in lokala variabler eller instanstillstånd.
  • extern En extern lokal funktion måste vara static.

Alla lokala variabler som definieras i den innehållande medlemmen, inklusive dess metodparametrar, är tillgängliga i en icke-statisk lokal funktion.

Till skillnad från en metoddefinition kan en lokal funktionsdefinition inte innehålla medlemsåtkomstmodifieraren. Eftersom alla lokala funktioner är privata, inklusive en åtkomstmodifierare, till exempel nyckelordet private , genereras kompilatorfelet CS0106, "Modifieraren "privat" är inte giltig för det här objektet.

I följande exempel definieras en lokal funktion med namnet AppendPathSeparator som är privat för en metod med namnet 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 + @"\";
     }
}

Du kan använda attribut för en lokal funktion, dess parametrar och typparametrar, som följande exempel visar:

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

I föregående exempel används ett särskilt attribut för att hjälpa kompilatorn i statisk analys i en nullbar kontext.

Lokala funktioner och undantag

En av de användbara funktionerna i lokala funktioner är att de kan tillåta att undantag visas omedelbart. För iteratormetoder visas undantag endast när den returnerade sekvensen räknas upp och inte när iteratorn hämtas. För asynkrona metoder observeras eventuella undantag som genereras i en asynkron metod när den returnerade aktiviteten väntar.

I följande exempel definieras en OddSequence metod som räknar upp udda tal i ett angivet intervall. Eftersom det skickar ett tal som är större än 100 till OddSequence uppräkningsmetoden genererar metoden en ArgumentOutOfRangeException. Som utdata från exemplet visar visas undantaget endast när du itererar talen och inte när du hämtar uppräknaren.

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

Om du placerar iteratorlogik i en lokal funktion genereras undantag för argumentverifiering när du hämtar uppräknaren, som följande exempel visar:

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

Lokala funktioner jämfört med lambda-uttryck

Vid första anblicken är lokala funktioner och lambda-uttryck mycket lika. I många fall är valet mellan att använda lambda-uttryck och lokala funktioner en fråga om stil och personliga preferenser. Det finns dock verkliga skillnader i var du kan använda det ena eller det andra som du bör känna till.

Nu ska vi undersöka skillnaderna mellan den lokala funktionen och lambda-uttrycksimplementeringarna för den faktoriella algoritmen. Här är versionen med hjälp av en lokal funktion:

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

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

Den här versionen använder lambda-uttryck:

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

Namngivning

Lokala funktioner namnges uttryckligen som metoder. Lambda-uttryck är anonyma metoder och måste tilldelas till variabler av en delegate typ, vanligtvis antingen Action eller Func typer. När du deklarerar en lokal funktion är processen som att skriva en normal metod. deklarerar du en returtyp och en funktionssignatur.

Funktionssignaturer och lambda-uttryckstyper

Lambda-uttryck förlitar sig på den typ av Action/Func variabel som de tilldelas för att fastställa argument- och returtyperna. Eftersom syntaxen i lokala funktioner är ungefär som att skriva en normal metod är argumenttyper och returtyp redan en del av funktionsdeklarationen.

Från och med C# 10 har vissa lambda-uttryck en naturlig typ, vilket gör att kompilatorn kan härleda returtypen och parametertyperna för lambda-uttrycket.

Bestämd tilldelning

Lambda-uttryck är objekt som deklareras och tilldelas vid körning. För att ett lambda-uttryck ska kunna användas måste det definitivt tilldelas: variabeln Action/Func som den ska tilldelas till måste deklareras och lambda-uttrycket tilldelas till det. Observera att LambdaFactorial måste deklarera och initiera lambda-uttrycket nthFactorial innan du definierar det. Om du inte gör det resulterar det i ett kompileringstidsfel för att nthFactorial referera till innan du tilldelar det.

Lokala funktioner definieras vid kompileringstillfället. Eftersom de inte har tilldelats till variabler kan de refereras från valfri kodplats där den finns i omfånget. I vårt första exempel LocalFunctionFactorialkan vi deklarera vår lokala funktion antingen ovanför eller under -instruktionen return och inte utlösa några kompilatorfel.

Dessa skillnader innebär att rekursiva algoritmer är enklare att skapa med hjälp av lokala funktioner. Du kan deklarera och definiera en lokal funktion som anropar sig själv. Lambda-uttryck måste deklareras och tilldelas ett standardvärde innan de kan omtilldelas till en brödtext som refererar till samma lambda-uttryck.

Implementering som ombud

Lambda-uttryck konverteras till ombud när de deklareras. Lokala funktioner är mer flexibla eftersom de kan skrivas som en traditionell metod eller som ombud. Lokala funktioner konverteras endast till ombud när de används som ombud.

Om du deklarerar en lokal funktion och bara refererar till den genom att anropa den som en metod konverteras den inte till ett ombud.

Variabelfångst

Reglerna för bestämd tilldelning påverkar också alla variabler som fångas upp av den lokala funktionen eller lambda-uttrycket. Kompilatorn kan utföra statisk analys som gör det möjligt för lokala funktioner att definitivt tilldela insamlade variabler i omfånget. Ta det här exemplet:

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

    void LocalFunction() => y = 0;
}

Kompilatorn kan fastställa att tilldelas LocalFunction definitivt när den anropas y . Eftersom LocalFunction anropas före -instruktionen yreturn tilldelas den definitivt i -instruktionenreturn.

Observera att när en lokal funktion samlar in variabler i omfånget för omslutning implementeras den lokala funktionen med hjälp av en stängning, som ombudstyper.

Heap-allokeringar

Beroende på hur de används kan lokala funktioner undvika heapallokeringar som alltid är nödvändiga för lambda-uttryck. Om en lokal funktion aldrig konverteras till ett ombud och ingen av variablerna som samlas in av den lokala funktionen fångas upp av andra lambdas eller lokala funktioner som konverteras till ombud, kan kompilatorn undvika heapallokeringar.

Tänk på det här asynkrona exemplet:

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

Stängningen för det här lambda-uttrycket innehåller variablerna address, index och name . När det gäller lokala funktioner kan objektet som implementerar stängningen vara en struct typ. Den här structtypen skickas med referens till den lokala funktionen. Den här skillnaden i implementering skulle spara på en allokering.

Den instansiering som krävs för lambda-uttryck innebär extra minnesallokeringar, vilket kan vara en prestandafaktor i tidskritiska kodsökvägar. Lokala funktioner medför inte den här kostnaden. I exemplet ovan har den lokala funktionsversionen två färre allokeringar än lambda-uttrycksversionen.

Om du vet att din lokala funktion inte konverteras till ett ombud och ingen av variablerna som fångas av den fångas upp av andra lambdas eller lokala funktioner som konverteras till ombud, kan du garantera att din lokala funktion undviker att allokeras på heapen genom att deklarera den som en static lokal funktion.

Dricks

Aktivera regel för .NET-kodformat IDE0062 för att säkerställa att lokala funktioner alltid är markerade static.

Kommentar

Den lokala funktionsekvivalenten för den här metoden använder också en klass för stängningen. Om stängningen för en lokal funktion implementeras som en class eller en struct är en implementeringsinformation. En lokal funktion kan använda en struct medan en lambda alltid använder en 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.";
    }
}

Användning av nyckelordet yield

En sista fördel som inte visas i det här exemplet är att lokala funktioner kan implementeras som iteratorer med hjälp av syntaxen yield return för att skapa en sekvens med värden.

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

-instruktionen yield return tillåts inte i lambda-uttryck. Mer information finns i kompilatorfelET CS1621.

Även om lokala funktioner kan verka redundanta för lambda-uttryck, tjänar de faktiskt olika syften och har olika användningsområden. Lokala funktioner är mer effektiva för fallet när du vill skriva en funktion som bara anropas från kontexten för en annan metod.

Språkspecifikation för C#

Mer information finns i avsnittet Lokala funktionsdeklarationer i C#-språkspecifikationen.

Se även