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:

MethodMeanErrorStdDevGen 0Gen 1Allocated
EnumToString55.941 ns66.2153 ns3.6295 ns0.00570.000224 B
EnumToStringFast1.512 ns1.8909 ns0.1036 ns---
EnumIsDefined301.073 ns1,289.5731 ns70.6859 ns0.0057-24 B
EnumIsDefinedFast1.813 ns0.3078 ns0.0169 ns---
EnumTryParse190.173 ns414.8240 ns22.7379 ns---
EnumTryParseFast20.076 ns21.5005 ns1.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.