Funzioni locali (Guida per programmatori C#)
Le funzioni locali sono metodi di un tipo annidati in un altro membro. Possono essere chiamate solo dal relativo membro contenitore. Le funzioni locali, in particolare, possono essere dichiarate in e chiamate da:
- Metodi, soprattutto metodi iteratori e metodi asincroni
- Costruttori
- Funzioni di accesso alle proprietà
- Funzioni di accesso agli eventi
- Metodi anonimi
- Espressioni lambda
- Finalizzatori
- Altre funzioni locali
Le funzioni locali, tuttavia, non possono essere dichiarate all'interno di un membro con corpo di espressione.
Nota
In alcuni casi, è possibile usare un'espressione lambda per implementare le funzionalità supportate anche da una funzione locale. Per un confronto, vedere Confronto tra funzioni locali ed espressioni Lambda.
Le funzioni locali rendono chiaro l'obiettivo del codice. Chiunque stia leggendo il codice può vedere che il metodo non è chiamabile se non dal metodo contenitore. 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.
Sintassi delle funzioni locali
Una funzione locale viene definita come metodo annidato all'interno di un membro contenitore. La definizione presenta la sintassi seguente:
<modifiers> <return-type> <method-name> <parameter-list>
Nota
<parameter-list>
non deve contenere i parametri denominati con la parola chiave contestualevalue
.
Il compilatore crea la variabile temporanea "value", che contiene le variabili esterne a cui si fa riferimento, cosa che potrà in seguito causare ambiguità e anche comportamenti imprevisti.
È possibile usare i modificatori seguenti con una funzione locale:
async
unsafe
-
static
Una funzione locale statica non può acquisire variabili locali o lo stato dell'istanza. -
extern
Una funzione locale esterna deve esserestatic
.
Tutte le variabili locali definite nel membro contenitore, inclusi i relativi parametri di metodo, sono accessibili in una funzione locale non statica.
A differenza di una definizione di metodo, una definizione di funzione locale non può includere il modificatore di accesso ai membri. Poiché tutte le funzioni locali sono private, incluso un modificatore di accesso, ad esempio la parola chiave private
, genera l'errore del compilatore CS0106, "Il modificatore 'private' non è valido per questo elemento".
L'esempio seguente definisce una funzione locale denominata AppendPathSeparator
, privata di un metodo denominato 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 + @"\";
}
}
È possibile applicare attributi a una funzione locale, ai relativi parametri e ai parametri di tipo, come illustrato nell'esempio seguente:
#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 assistere il compilatore nell'analisi statica in un contesto che ammette i valori Null.
Funzioni locali ed eccezioni
Le funzioni locali offrono il vantaggio di consentire alle eccezioni di essere rilevate immediatamente. 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. Nel caso dei metodi asincroni, qualsiasi eccezione generata viene rilevata mentre è attesa l'attività restituita.
L'esempio seguente definisce un metodo OddSequence
che enumera i numeri dispari in un intervallo specifico. Poiché al metodo enumeratore OddSequence
viene trasmesso un numero maggiore di 100, il metodo genera una 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.
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, le eccezioni di convalida degli argomenti vengono generate quando si recupera l'enumeratore, come illustrato nell'esempio seguente:
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 lambda
A prima vista, le funzioni locali e espressioni lambda sono simili. In molti casi, la scelta tra l'uso di funzioni locali ed espressioni lambda è una questione di stile e preferenze personali. Esistono tuttavia differenze reali di cui è necessario essere consapevoli nei casi in cui è possibile usare le une o le altre.
Si esamineranno ora le differenze tra implementazioni di funzioni locali e implementazioni di espressioni lambda dell'algoritmo fattoriale. Ecco la versione che usa una funzione locale:
public static int LocalFunctionFactorial(int n)
{
return nthFactorial(n);
int nthFactorial(int number) => number < 2
? 1
: number * nthFactorial(number - 1);
}
Questa versione usa espressioni 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);
}
Denominazione
Le funzioni locali sono denominate in modo esplicito come metodi. Le espressioni lambda sono metodi anonimi e devono essere assegnate a variabili di un tipo delegate
, in genere tipi Action
o Func
. Quando si dichiara una funzione locale, il processo è simile alla scrittura di un metodo normale; si dichiarano un tipo restituito e una firma di funzione.
Firme della funzione e tipi di espressioni lambda
Le espressioni lambda si basano sul tipo della variabile Action
/Func
assegnata per determinare l'argomento e i tipi restituiti. 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.
Alcune espressioni lambda hanno un tipo naturale, il che consente al compilatore di inferire il tipo di ritorno e i tipi dei parametri dell'espressione lambda.
Assegnazione definita
Le espressioni lambda sono oggetti dichiarati e assegnati in fase di esecuzione. Affinché un'espressione lambda venga utilizzata, è necessario che sia assegnata in modo definitivo: la variabile Action
/Func
alla quale è assegnata deve essere dichiarata e l'espressione lambda assegnata ad essa. Si noti che LambdaFactorial
deve dichiarare e inizializzare l'espressione lambda nthFactorial
prima di definirla. In caso contrario, si verifica un errore di compilazione per fare riferimento a nthFactorial
prima di assegnarla.
Le funzioni locali vengono definite in fase di compilazione. 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 prima o dopo l'istruzione return
e non attivare errori del compilatore.
Queste differenze fanno sì che gli algoritmi ricorsivi sino più facili da creare usando funzioni locali. È possibile dichiarare e definire una funzione locale che chiama se stessa. Le espressioni lambda devono essere dichiarate e assegnate un valore predefinito prima di poter essere riassegnate a un corpo che fa riferimento alla stessa espressione lambda.
Implementazione come delegato
Le espressioni lambda vengono convertite in delegati quando vengono dichiarate. Le funzioni locali sono più flessibili in quanto possono essere scritte come un metodo tradizionale o come delegato. Le funzioni locali vengono convertite in delegati solo quando vengono usate come delegati.
Se si dichiara una funzione locale e la si richiama solo come un metodo, non verrà convertita in un delegato.
Acquisizione di variabili
Le regole di assegnazione definita si applicano anche a qualunque variabile acquisita dalla funzione locale o dall'espressione lambda. Il compilatore può eseguire un'analisi statica che consente alle funzioni locali di assegnare in modo certo variabili acquisite nell'ambito che le contiene. Si consideri questo esempio:
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. Poiché LocalFunction
viene chiamata prima dell'istruzione return
, y
viene assegnata in modo certo in corrispondenza dell'istruzione return
.
Quando una funzione locale acquisisce variabili nell'ambito circostante, viene implementata usando una chiusura, così come avviene per i tipi delegati.
Allocazioni di heap
A seconda del loro uso, le funzioni locali possono evitare le allocazioni di heap, che sono sempre necessarie per le espressioni lambda. Se una funzione locale non viene mai convertita in 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.
Si consideri questo esempio asincrono:
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
. Per le funzioni locali, l'oggetto che implementa la chiusura può essere un tipo struct
. Tale tipo struct verrebbe passato per riferimento alla funzione locale. Questa differenza di implementazione consentirebbe di risparmiare un'allocazione.
La creazione di istanze necessaria per le espressioni lambda comporta allocazioni di memoria aggiuntive, che potrebbero essere un fattore che influisce sulle prestazioni nei percorsi di codice critici per le tempistiche. Le funzioni locali non comportano questo sovraccarico.
Se si sa che la funzione locale non verrà convertita in un delegato e che 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 l'allocazione nell'heap dichiarandola come funzione locale static
.
Suggerimento
Abilitare la regola di stile del codice .NET IDE0062 per assicurarsi che le funzioni locali siano sempre contrassegnate come static
.
Nota
La funzione locale equivalente di questo metodo usa anche una classe per la chiusura. Se la chiusura di una funzione locale viene implementata come class
o struct
non ha molta importanza. Una funzione locale può usare struct
mentre un'espressione lambda userà sempre 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.";
}
}
Uso della parola chiave yield
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.
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'istruzione yield return
non è consentita nelle espressioni lambda. Per altre informazioni, vedere l'errore del compilatore CS1621.
Anche se le funzioni locali possono sembrare ridondanti per le espressioni lambda, in realtà servono scopi diversi e hanno usi diversi. Le funzioni locali sono più efficienti nel caso si voglia scrivere una funzione che viene chiamata solo dal contesto di un altro metodo.
Specifiche del linguaggio C#
Per altre informazioni, vedere la sezione Dichiarazioni di funzioni locali della Specifica del linguaggio C#.