Funzioni locali (Guida per programmatori C#)Local functions (C# Programming Guide)

A partire dalla versione 7.0, C# supporta le funzioni locali.Starting with C# 7.0, C# supports local functions. Le funzioni locali sono metodi privati di un tipo annidati in un altro membro.Local functions are private methods of a type that are nested in another member. Possono essere chiamate solo dal relativo membro contenitore.They can only be called from their containing member. Le funzioni locali, in particolare, possono essere dichiarate in e chiamate da:Local functions can be declared in and called from:

  • Metodi, soprattutto metodi iteratori e metodi asincroniMethods, especially iterator methods and async methods
  • CostruttoriConstructors
  • Funzioni di accesso alle proprietàProperty accessors
  • Funzioni di accesso agli eventiEvent accessors
  • Metodi anonimiAnonymous methods
  • Espressioni lambdaLambda expressions
  • FinalizzatoriFinalizers
  • Altre funzioni localiOther local functions

Le funzioni locali, tuttavia, non possono essere dichiarate all'interno di un membro con corpo di espressione.However, local functions can't be declared inside an expression-bodied member.

Nota

In alcuni casi, è possibile usare un'espressione lambda per implementare le funzionalità supportate anche da una funzione locale.In some cases, you can use a lambda expression to implement functionality also supported by a local function. Per un confronto, vedere funzioni locali rispetto alle espressioni lambda.For a comparison, see Local functions vs. lambda expressions.

Le funzioni locali rendono chiaro l'obiettivo del codice.Local functions make the intent of your code clear. Chiunque legga il codice può vedere che il metodo non è richiamabile, ad eccezione del metodo contenitore.Anyone reading your code can see that the method is not callable except by the containing method. Per i progetti in team, le funzioni locali impediscono anche a un altro sviluppatore di chiamare per errore il metodo direttamente da un altro punto della classe o dello struct.For team projects, they also make it impossible for another developer to mistakenly call the method directly from elsewhere in the class or struct.

Sintassi delle funzioni localiLocal function syntax

Una funzione locale viene definita come metodo annidato all'interno di un membro contenitore.A local function is defined as a nested method inside a containing member. La definizione presenta la sintassi seguente:Its definition has the following syntax:

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

Con una funzione locale è possibile usare i modificatori seguenti:You can use the following modifiers with a local function:

  • async
  • unsafe
  • static (in C# 8,0 e versioni successive).static (in C# 8.0 and later). Una funzione locale statica non può acquisire le variabili locali o lo stato dell'istanza.A static local function can't capture local variables or instance state.
  • extern (in C# 9,0 e versioni successive).extern (in C# 9.0 and later). Una funzione locale esterna deve essere static .An external local function must be static.

Tutte le variabili locali definite nel membro contenitore, inclusi i parametri del metodo, sono accessibili in una funzione locale non statica.All local variables that are defined in the containing member, including its method parameters, are accessible in a non-static local function.

Diversamente da una definizione di metodo, una definizione di funzione locale non può includere il modificatore di accesso ai membri.Unlike a method definition, a local function definition cannot include the member access modifier. Poiché tutte le funzioni locali sono private, l'integrazione di un modificatore di accesso come la parola chiave private genera l'errore del compilatore CS0106: "Il modificatore 'private' non è valido per questo elemento".Because all local functions are private, including an access modifier, such as the private keyword, generates compiler error CS0106, "The modifier 'private' is not valid for this item."

L'esempio seguente definisce una funzione locale denominata AppendPathSeparator, privata di un metodo denominato GetText:The following example defines a local function named AppendPathSeparator that is private to a method named 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 + @"\";
     }
}

A partire da C# 9,0, è possibile applicare attributi a una funzione locale, i parametri e i parametri di tipo, come illustrato nell'esempio seguente:Beginning with C# 9.0, you can apply attributes to a local function, its parameters and type parameters, as the following example shows:

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

Nell'esempio precedente viene usato un attributo speciale per supportare il compilatore nell'analisi statica in un contesto Nullable.The preceding example uses a special attribute to assist the compiler in static analysis in a nullable context.

Funzioni locali ed eccezioniLocal functions and exceptions

Le funzioni locali offrono il vantaggio di consentire alle eccezioni di essere rilevate immediatamente.One of the useful features of local functions is that they can allow exceptions to surface immediately. Nel caso degli iteratori di metodo, le eccezioni vengono rilevate solo nel momento in cui la sequenza restituita viene enumerata e non quando viene recuperato l'iteratore.For method iterators, exceptions are surfaced only when the returned sequence is enumerated, and not when the iterator is retrieved. Nel caso dei metodi asincroni, qualsiasi eccezione generata viene rilevata mentre è attesa l'attività restituita.For async methods, any exceptions thrown in an async method are observed when the returned task is awaited.

Nell'esempio seguente viene definito un OddSequence metodo che enumera i numeri dispari in un intervallo specificato.The following example defines an OddSequence method that enumerates odd numbers in a specified range. Poiché al metodo enumeratore OddSequence viene trasmesso un numero maggiore di 100, il metodo genera una ArgumentOutOfRangeException.Because it passes a number greater than 100 to the OddSequence enumerator method, the method throws an ArgumentOutOfRangeException. Come illustrato dall'output dell'esempio, l'eccezione viene rilevata solo nel momento in cui vengono iterati i numeri e non quando si recupera l'enumeratore.As the output from the example shows, the exception surfaces only when you iterate the numbers, and not when you retrieve the enumerator.

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

Se si inserisce la logica iteratore in una funzione locale, vengono generate eccezioni di convalida degli argomenti quando si recupera l'enumeratore, come illustrato nell'esempio seguente:If you put iterator logic into a local function, argument validation exceptions are thrown when you retrieve the enumerator, as the following example shows:

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

Funzioni locali ed espressioni lambdaLocal functions vs. lambda expressions

A prima vista, le funzioni locali e le espressioni lambda sono molto simili.At first glance, local functions and lambda expressions are very similar. In molti casi, la scelta tra l'uso di funzioni locali ed espressioni lambda è una questione di stile e preferenze personali.In many cases, the choice between using lambda expressions and local functions is a matter of style and personal preference. Esistono tuttavia differenze reali di cui è necessario essere consapevoli nei casi in cui è possibile usare le une o le altre.However, there are real differences in where you can use one or the other that you should be aware of.

Si esamineranno ora le differenze tra implementazioni di funzioni locali e implementazioni di espressioni lambda dell'algoritmo fattoriale.Let's examine the differences between the local function and lambda expression implementations of the factorial algorithm. Di seguito è illustrata la versione che usa una funzione locale:Here's the version using a local function:

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

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

Questa versione USA le espressioni lambda:This version uses lambda expressions:

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

DenominazioneNaming

Le funzioni locali sono denominate in modo esplicito come metodi.Local functions are explicitly named like methods. Le espressioni lambda sono metodi anonimi che devono essere assegnate a variabili di un delegate tipo, in genere Action o Func .Lambda expressions are anonymous methods and need to be assigned to variables of a delegate type, typically either Action or Func types. Quando si dichiara una funzione locale, il processo è simile alla scrittura di un metodo normale; si dichiara un tipo restituito e una firma di funzione.When you declare a local function, the process is like writing a normal method; you declare a return type and a function signature.

Firme di funzione e tipi di espressione lambdaFunction signatures and lambda expression types

Le espressioni lambda si basano sul tipo della Action / Func variabile a cui sono assegnati per determinare l'argomento e i tipi restituiti.Lambda expressions rely on the type of the Action/Func variable that they're assigned to determine the argument and return types. Nelle funzioni locali, poiché la sintassi è molto simile alla scrittura di un metodo normale, i tipi di argomento e il tipo restituito fanno già parte della dichiarazione di funzione.In local functions, since the syntax is much like writing a normal method, argument types and return type are already part of the function declaration.

Assegnazione definitaDefinite assignment

Le espressioni lambda sono oggetti dichiarati e assegnati in fase di esecuzione.Lambda expressions are objects that are declared and assigned at runtime. Per poter usare un'espressione lambda, è necessario assegnarla in modo sicuro: la Action / Func variabile a cui verrà assegnato deve essere dichiarata e l'espressione lambda assegnata.In order for a lambda expression to be used, it needs to be definitely assigned: the Action/Func variable that it will be assigned to must be declared and the lambda expression assigned to it. Si noti che è LambdaFactorial necessario dichiarare e inizializzare l'espressione lambda nthFactorial prima di definirla.Notice that LambdaFactorial must declare and initialize the lambda expression nthFactorial before defining it. In caso contrario, si verifica un errore di compilazione per fare riferimento a nthFactorial prima di assegnarla.Not doing so results in a compile time error for referencing nthFactorial before assigning it.

Le funzioni locali sono definite in fase di compilazione.Local functions are defined at compile time. Poiché non sono assegnati alle variabili, è possibile farvi riferimento da qualsiasi posizione del codice in cui si trova nell'ambito. nel primo esempio LocalFunctionFactorial , è possibile dichiarare la funzione locale al di sopra o al di sotto dell' return istruzione e non attivare alcun errore del compilatore.As they're not assigned to variables, they can be referenced from any code location where it is in scope; in our first example LocalFunctionFactorial, we could declare our local function either above or below the return statement and not trigger any compiler errors.

Queste differenze fanno sì che gli algoritmi ricorsivi sino più facili da creare usando funzioni locali.These differences mean that recursive algorithms are easier to create using local functions. È possibile dichiarare e definire una funzione locale che chiama se stessa.You can declare and define a local function that calls itself. Le espressioni lambda devono essere dichiarate e a queste deve essere assegnato un valore predefinito prima che sia possibile riassegnarle a un corpo che fa riferimento alla stessa espressione lambda.Lambda expressions must be declared, and assigned a default value before they can be re-assigned to a body that references the same lambda expression.

Implementazione come delegatoImplementation as a delegate

Le espressioni lambda vengono convertite in delegati quando vengono dichiarate.Lambda expressions are converted to delegates when they're declared. Le funzioni locali sono più flessibili perché possono essere scritte come un metodo tradizionale o come delegato.Local functions are more flexible in that they can be written like a traditional method or as a delegate. Le funzioni locali vengono convertite in delegati solo se utilizzati come delegato.Local functions are only converted to delegates when used as a delegate.

Se si dichiara una funzione locale e si fa riferimento a questa solo chiamandola come un metodo, non verrà convertita in delegato.If you declare a local function and only reference it by calling it like a method, it will not be converted to a delegate.

Acquisizione di variabiliVariable capture

Le regole di assegnazione definita influiscono anche sulle variabili acquisite dalla funzione locale o dall'espressione lambda.The rules of definite assignment also affect any variables that are captured by the local function or lambda expression. Il compilatore può eseguire un'analisi statica che consente alle funzioni locali di assegnare definitivamente le variabili acquisite nell'ambito di inclusione.The compiler can perform static analysis that enables local functions to definitely assign captured variables in the enclosing scope. Prendere in considerazione questo esempio:Consider this example:

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

    void LocalFunction() => y = 0;
}

Il compilatore può determinare che LocalFunction assegna in modo certo y quando viene chiamata.The compiler can determine that LocalFunction definitely assigns y when called. Poiché LocalFunction viene chiamata prima dell'istruzione return, y viene assegnata in modo certo in corrispondenza dell'istruzione return.Because LocalFunction is called before the return statement, y is definitely assigned at the return statement.

Si noti che quando una funzione locale acquisisce le variabili nell'ambito di inclusione, la funzione locale viene implementata come tipo delegato.Note that when a local function captures variables in the enclosing scope, the local function is implemented as a delegate type.

Allocazioni heapHeap allocations

A seconda del loro uso, le funzioni locali possono evitare le allocazioni di heap, che sono sempre necessarie per le espressioni lambda.Depending on their use, local functions can avoid heap allocations that are always necessary for lambda expressions. Se una funzione locale non viene mai convertita in un delegato e nessuna delle variabili acquisite dalla funzione locale viene acquisita da altre espressioni lambda o funzioni locali convertite in delegati, il compilatore può evitare allocazioni di heap.If a local function is never converted to a delegate, and none of the variables captured by the local function are captured by other lambdas or local functions that are converted to delegates, the compiler can avoid heap allocations.

Si consideri questo esempio asincrono:Consider this async example:

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

La chiusura per questa espressione lambda contiene le variabili address, index e name.The closure for this lambda expression contains the address, index and name variables. Nel caso delle funzioni locali, l'oggetto che implementa la chiusura può essere di tipo structIn the case of local functions, the object that implements the closure may be a struct type. Tale tipo struct verrebbe passato per riferimento alla funzione locale.That struct type would be passed by reference to the local function. Questa differenza di implementazione consentirebbe di risparmiare un'allocazione.This difference in implementation would save on an allocation.

La creazione di istanze necessaria per le espressioni lambda comporta allocazioni di memoria aggiuntive che possono ridurre le prestazioni nei percorsi di codice in cui il tempo è un fattore cruciale.The instantiation necessary for lambda expressions means extra memory allocations, which may be a performance factor in time-critical code paths. Questo sovraccarico non si verifica per le funzioni locali.Local functions do not incur this overhead. Nell'esempio precedente, la versione di funzioni locali dispone di due allocazioni minori rispetto alla versione dell'espressione lambda.In the example above, the local functions version has two fewer allocations than the lambda expression version.

Se si è certi che la funzione locale non verrà convertita in un delegato e nessuna delle variabili acquisite da essa viene acquisita da altre espressioni lambda o funzioni locali convertite in delegati, è possibile garantire che la funzione locale eviti di essere allocata nell'heap dichiarando come static funzione locale.If you know that your local function won't be converted to a delegate and none of the variables captured by it are captured by other lambdas or local functions that are converted to delegates, you can guarantee that your local function avoids being allocated on the heap by declaring it as a static local function. Si noti che questa funzionalità è disponibile in C# 8,0 e versioni successive.Note that this feature is available in C# 8.0 and newer.

Nota

La funzione locale equivalente di questo metodo usa anche una classe per la chiusura.The local function equivalent of this method also uses a class for the closure. Se la chiusura di una funzione locale viene implementata come class o struct non ha molta importanza.Whether the closure for a local function is implemented as a class or a struct is an implementation detail. Una funzione locale può usare struct mentre un'espressione lambda userà sempre class.A local function may use a struct whereas a lambda will always use a 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.";
    }
}

Utilizzo della yield parola chiaveUsage of the yield keyword

Come ultimo vantaggio, non illustrato in questo esempio, le funzioni locali possono essere implementate come iteratori usando la sintassi yield return per produrre una sequenza di valori.One final advantage not demonstrated in this sample is that local functions can be implemented as iterators, using the yield return syntax to produce a sequence of values.

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

L' yield return istruzione non è consentita nelle espressioni lambda. vedere errore del compilatore CS1621.The yield return statement is not allowed in lambda expressions, see compiler error CS1621.

Sebbene le funzioni locali possano apparire ridondanti rispetto alle espressioni lambda, in realtà hanno finalità e usi diversi.While local functions may seem redundant to lambda expressions, they actually serve different purposes and have different uses. Le funzioni locali sono più efficienti nel caso si voglia scrivere una funzione che viene chiamata solo dal contesto di un altro metodo.Local functions are more efficient for the case when you want to write a function that is called only from the context of another method.

Vedi ancheSee also