WORDS
C# 7: Lokala funktioner
En lokal funktion är som en metod inuti en annan metod, och kan ibland vara ett alternativ till privata metoder som bara anropas från ett ställe. Dom är särskilt användbara när man behöver en hjälpfunktion och kan tydliggöra att en funktion bara används inom en annan metods kontext. Ett annat alternativ är lambdauttryck, men dom fungerar sällan som hjälpfunktioner.
Fördelar gentemot privata metoder:
- Tydliggör att funktionen bara kan anropas inom ett begränsat scope.
- Kan fånga (eng. capture) variabler från utanförvarande scope. Detta skapar ett så kallat closure.
Fördelar gentemot lambdauttryck:
- Kompilatorn kan bättre analysera anropen, vilket bl.a. underlättar i rekursion och undviker lagring på heapen.
- Har stöd för
yield return
.
I den här artikeln går vi igenom ett par exempel där lokala funktioner är särskilt användbara.
Iterator-metoder med yield return
Låt oss börja med en iterator-metod:
public static IEnumerable<char> AlphabetSubset(char start, char end)
{
if (start < 'a' || start > 'z' || end < 'a' || end > 'z')
throw new ArgumentException($"Both {nameof(end)} and {nameof(start)} must be letters");
if (end <= start)
throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
for (var c = start; c <= end; c++)
yield return c;
}
Följande kod anropar metoden felaktigt:
var result = AlphabetSubset('f', 'a');
Console.WriteLine("Skapade iterator");
foreach (var x in result)
{
Console.WriteLine($"Fick {x}");
}
Ett exception kastas först när result
itereras i foreach
, inte när resultatet skapades. I detta minimala exempel går det lätt att hitta problemet, men i en större kodbas kanske resultatet inte itereras direkt efter att det hämtas. Där skulle det vara bättre att få exception redan vid anropet.
Det kan man implementera genom att lägga loopen i en egen metod:
public static IEnumerable<char> AlphabetSubset2(char start, char end)
{
if (start < 'a' || start > 'z' || end < 'a' || end > 'z')
throw new ArgumentException($"Both {nameof(end)} and {nameof(start)} must be letters");
if (end <= start)
throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
return AlphabetSubsetImplementation(start, end);
}
private static IEnumerable<char> AlphabetSubsetImplementation(char start, char end)
{
for (var c = start; c <= end; c++)
yield return c;
}
Detta kan tydligare implementeras som en lokal funktion. Konventionen är att lägga lokala funktioner efter return
.
public static IEnumerable<char> AlphabetSubset3(char start, char end)
{
if (start < 'a' || start > 'z' || end < 'a' || end > 'z')
throw new ArgumentException($"Both {nameof(end)} and {nameof(start)} must be letters");
if (end <= start)
throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
return implementation();
IEnumerable<char> implementation()
{
for (var c = start; c <= end; c++)
yield return c;
}
}
Lokala funktioner är som en blandning av metoder och lambdauttryck, med några av fördelarna från vardera.
Asynkrona metoder
Precis som för iterator-metoder, så kan ett exception från anrop till en asynkron metod kastas senare än man förväntar sig. Genom att lägga den asynkrona implementationen i en lokal funktion, säkerställer man att exception kastas omedelbart vid ett felaktigt anrop. Observera att den yttre metoden inte använder async
.
public Task<string> PerformLongRunningWork(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address) || string.IsNullOrWhiteSpace(name))
throw new ArgumentException("An address and name are required");
if (index < 0)
throw new ArgumentOutOfRangeException("The index must be non-negative");
return implementation();
async Task<string> implementation()
{
var interimResult = await FirstStep(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}.";
}
}
Rekursiva metoder
Som exempel på rekursiv funktion, så beräknar vi n-fakultet. Här börjar vi med en lokal funktion:
public static int Factorial(int n)
{
if (n < 0)
throw new ArgumentOutOfRangeException("n must be non-negative");
return nthFactorial(n);
int nthFactorial(int number) =>
(number < 2)
? 1
: number * nthFactorial(number - 1);
}
En implementation med lambdauttryck blir lite krångligare:
public static int Factorial2(int n)
{
if (n < 0)
throw new ArgumentOutOfRangeException("n must be non-negative");
Func<int, int> nthFactorial = default(Func<int, int>);
nthFactorial = number =>
(number < 2)
? 1
: number * nthFactorial(number - 1);
return nthFactorial(n);
}
Här måste deklarationen av lambdauttryckets variabel göras först, innan man tilldelar variabeln det lambdauttryck vi vill ha. Detta på grund av att kompilatorn inte tillåter att ett lambdauttryck refererar till en variabel som inte ännu har tilldelats.