Introduzione
Il metodo ToString()
di un enum
Γ¨ implementato in maniera molto discutibile: Γ¨ lento e inoltre porta a delle inutili allocazioni di memoria che dovranno essere eliminati dal GC.
Andiamo a vedere sotto il cofano come funziona il metodo ToString()
; dopo qualche metodo interno otteniamo:
private static string? GetEnumName(EnumInfo enumInfo, ulong ulValue)
{
int index = Array.BinarySearch(enumInfo.Values, ulValue);
if (index >= 0)
{
return enumInfo.Names[index];
}
return null; // return null so the caller knows to .ToString() the input
}
Come si nota vi Γ¨ un binary search su tutti i valori dellβenum e inoltre un accesso tramite indice allβarray contenente i nomi.
Gli algoritmi di ricerca binaria hanno performance di O(logn)
, quindi piΓΉ sono i valori dellβenum maggiore sarΓ il tempo impiegato dallβalgoritmo a trovare il valore corretto.
Questa complicazione puΓ² essere completamente evitata dato che il ToString()
di un Enum
Γ¨, di fatto, il suo nameof
: il ToString()
di Color.Aquamarine
Γ¨ esattamente nameof(Color.Aquamarine)
Questa chiamata Γ¨ incredibilmente piΓΉ veloce, non dipende dalla dimensione dellβenum e inoltre non alloca memoria.
Il problema Γ¨ che, per ogni enum da velocizzare, servirebbe che ci sia un metodo con allβinterno un gigantesco switch case che mappa ogni valore dellβenum al suo nameof
.
Il primo approccio Γ¨ fare tutto a mano ma, grazie ai Source Generators, Γ¨ possibile automatizzare il lavoro.
Andrew Lock, nel suo blog .NET Escapades, ha creato un comodo pacchetto nuget per automatizzare la generazione di codice veloce per ogni enum che si vuole. Il progetto Γ¨ open source e disponibile qui: EnumGenerators.
Una volta importato il pacchetto nuget Γ¨ solo necessario aggiungere lβattributo [EnumExtensions]
sopra lβenum da velocizzare e verranno generati degli extension methods automaticamente.
Test
Oltre a ToString()
ho testato Enum.IsDefined
e Enum.TryParse
utilizzando BenchmarkDotNet confrontando le loro performance con i metodi classici.
[Benchmark]
public string EnumToString()
{
return EnumColor.Aquamarine.ToString();
}
[Benchmark]
public string EnumToStringFast()
{
return EnumColor.Aquamarine.ToStringFast();
}
[Benchmark]
public bool EnumIsDefined()
{
return Enum.IsDefined(typeof(EnumColor), 48);
}
[Benchmark]
public bool EnumIsDefinedFast()
{
return EnumColorExtensions.IsDefined((EnumColor)48);
}
[Benchmark]
public (bool, EnumColor) EnumTryParse()
{
var couldParse = Enum.TryParse("Aquamarine", false, out EnumColor value);
return (couldParse, value);
}
[Benchmark]
public (bool, EnumColor) EnumTryParseFast()
{
var couldParse = EnumColorExtensions.TryParse("Aquamarine", false, out var value);
return (couldParse, value);
}
Ecco i risultati:
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
---|---|---|---|---|---|---|
EnumToString | 55.941 ns | 66.2153 ns | 3.6295 ns | 0.0057 | 0.0002 | 24 B |
EnumToStringFast | 1.512 ns | 1.8909 ns | 0.1036 ns | - | - | - |
EnumIsDefined | 301.073 ns | 1,289.5731 ns | 70.6859 ns | 0.0057 | - | 24 B |
EnumIsDefinedFast | 1.813 ns | 0.3078 ns | 0.0169 ns | - | - | - |
EnumTryParse | 190.173 ns | 414.8240 ns | 22.7379 ns | - | - | - |
EnumTryParseFast | 20.076 ns | 21.5005 ns | 1.1785 ns | - | - | - |
Conclusione
Come si nota abbiamo circa due ordini di grandezza di velocitΓ e inoltre non abbiamo alcuna allocazione di memoria.
Analizzando lβextension ToStringFast()
notiamo che internamente ha lo switch case di cui parlavamo in precedenza che mappa ogni valore dellβenum nel suo nameof
.
Dei limiti degli enum e di questo pacchetto ne ha parlato anche Nick Chapsas qui e qui.
NetEscapades.EnumGenerators Γ¨ ancora in beta ed Γ¨ richiede almeno .NET 6 SDK.