There are many answers to this question. I was interested in which solution works exactly the same as (int)enumValue
and which is the fastest.
So I wrote a benchmark using BenchmarkDotNet and checked also the results of used methods.
Performance Benchmark
Method |
Mean |
Error |
StdDev |
Median |
Gen 0 |
Allocated |
Convert_ToInt32 |
20.4430 ns |
0.1278 ns |
0.1067 ns |
20.4343 ns |
0.0003 |
48 B |
CastTo_Object_Int |
8.3970 ns |
0.0664 ns |
0.0621 ns |
8.3929 ns |
0.0002 |
24 B |
ByPointers_Switch_Byte |
0.4515 ns |
0.0035 ns |
0.0029 ns |
0.4512 ns |
- |
- |
ByPointers_Switch_SByte |
0.4664 ns |
0.0069 ns |
0.0064 ns |
0.4687 ns |
- |
- |
CompiledLambdaFunc |
0.2236 ns |
0.0055 ns |
0.0051 ns |
0.2230 ns |
- |
- |
Unsafe_As |
0.0045 ns |
0.0050 ns |
0.0047 ns |
0.0034 ns |
- |
- |
GetHashCode |
0.0034 ns |
0.0036 ns |
0.0034 ns |
0.0019 ns |
- |
- |
ByPointers_DirectInt |
0.0030 ns |
0.0051 ns |
0.0048 ns |
0.0000 ns |
- |
- |
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 9 5900HX with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.300
Consistency with (int)enumValue
Method |
IntEnum |
UIntEnum |
LongEnum |
ULongEnum |
ByteEnum |
SByteEnum |
Convert_ToInt32 |
✓ same |
throws |
throws |
throws |
✓ same |
✓ same |
CastTo_Object_Int |
✓ same |
throws |
throws |
throws |
throws |
throws |
ByPointers_Switch_Byte |
✓ same |
✓ same |
✓ same |
✓ same |
✓ same |
differs* |
ByPointers_Switch_SByte |
✓ same |
✓ same |
✓ same |
✓ same |
differs* |
✓ same |
CompiledLambdaFunc |
✓ same |
✓ same |
✓ same |
✓ same |
✓ same |
✓ same |
Unsafe_As |
✓ same |
✓ same |
✓ same |
✓ same |
✓ same |
✓ same |
GetHashCode |
✓ same |
✓ same |
differs |
differs |
✓ same |
✓ same |
ByPointers_DirectInt |
✓ same |
✓ same |
✓ same |
✓ same |
✓ same |
✓ same |
Legend:
✓ same: gives the same results
throws: throws an exception at least for some cases
differs: gives different results at least for some cases
differs*: gives different results in Debug, same results in Release
A question regarding differs*
: about *(byte*)(&sbyteValue)
inconsistency in the Release mode.
Results
Two methods were both fastest and consistent with (int)enumValue
:
public static int Unsafe_As<TEnum>(TEnum enumValue)
where TEnum : struct, Enum
{
return Unsafe.As<TEnum, int>(ref enumValue);
}
public static unsafe int ByPointers_DirectInt<TEnum>(TEnum enumValue)
where TEnum : unmanaged, Enum
{
return *(int*)(&enumValue);
}
The methods shall be safe and work well for TEnum : int, uint, long or ulong
.
Caution: for TEnum : byte, sbyte, short or ushort
these methods access more memory than is the native size of TEnum. If the extra memory is all '0', then we get the right results. But if not, then results end up corrupted.
I tried to make a failing example, but I failed. Anybody's suggestion or explanation is welcome.
Fastest among safe: The next fastest method gives exactly same results as (int)enumValue
since it is a dynamically compiled version of this cast. If we consider full safety, then the CompiledLambdaFunc
is the winner:
public static int CompiledLambdaFunc<TEnum>(TEnum value)
where TEnum : struct, Enum
{
return StaticGenericCache<TEnum>.TheFunc(value);
}
private static class StaticGenericCache<T>
where T : struct, Enum
{
public static Func<T, int> TheFunc = GenerateFunc<T>();
}
private static Func<TEnum, int> GenerateFunc<TEnum>()
where TEnum : struct, Enum
{
var inputParameter = Expression.Parameter(typeof(TEnum));
var body = Expression.Convert(inputParameter, typeof(int)); // means: (int)input;
var lambda = Expression.Lambda<Func<TEnum, int>>(body, inputParameter);
var func = lambda.Compile();
return func;
}
The Benchmark Code
[MemoryDiagnoser]
public class EnumToIntBenchmark
{
[Benchmark]
public int Convert_ToInt32() => Methods.Convert_ToInt32(SpecificEnum.TheValue);
[Benchmark]
public int CastTo_Object_Int() => Methods.CastTo_Object_Int(SpecificEnum.TheValue);
[Benchmark]
public int ByPointers_Switch_Byte() => Methods.ByPointers_Switch_Byte(SpecificEnum.TheValue);
[Benchmark]
public int ByPointers_Switch_SByte() => Methods.ByPointers_Switch_SByte(SpecificEnum.TheValue);
[Benchmark]
public int CompiledLambdaFunc() => Methods.CompiledLambdaFunc(SpecificEnum.TheValue);
[Benchmark]
public int Unsafe_As() => Methods.Unsafe_As(SpecificEnum.TheValue);
[Benchmark]
public new int GetHashCode() => Methods.GetHashCode(SpecificEnum.TheValue);
[Benchmark]
public int ByPointers_DirectInt() => Methods.ByPointers_DirectInt(SpecificEnum.TheValue);
private enum SpecificEnum
{
None = 0,
TheValue,
}
public static class Methods
{
public static int Convert_ToInt32<TEnum>(TEnum value)
where TEnum : struct, Enum
{
return Convert.ToInt32(value);
}
public static int CastTo_Object_Int<TEnum>(TEnum value)
where TEnum : struct, Enum
{
return (int)(object)value;
}
public static unsafe int ByPointers_Switch_Byte<TEnum>(TEnum enumValue)
where TEnum : unmanaged, Enum
{
switch (sizeof(TEnum))
{
case 1: return *(byte*)(&enumValue);
case 2: return *(short*)(&enumValue);
case 4: return *(int*)(&enumValue);
case 8: return (int)*(long*)(&enumValue);
default: throw new NotImplementedException($"Not implemented for size: {sizeof(TEnum)}");
}
}
public static unsafe int ByPointers_Switch_SByte<TEnum>(TEnum enumValue)
where TEnum : unmanaged, Enum
{
switch (sizeof(TEnum))
{
case 1: return *(sbyte*)(&enumValue);
case 2: return *(short*)(&enumValue);
case 4: return *(int*)(&enumValue);
case 8: return (int)*(long*)(&enumValue);
default: throw new NotImplementedException($"Not implemented for size: {sizeof(TEnum)}");
}
}
public static unsafe int ByPointers_DirectInt<TEnum>(TEnum enumValue)
where TEnum : unmanaged, Enum
{
return *(int*)(&enumValue);
}
public static int Unsafe_As<TEnum>(TEnum enumValue)
where TEnum : struct, Enum
{
return Unsafe.As<TEnum, int>(ref enumValue);
}
public static int GetHashCode<TEnum>(TEnum value)
where TEnum : struct, Enum
{
return value.GetHashCode();
}
public static int CompiledLambdaFunc<TEnum>(TEnum value)
where TEnum : struct, Enum
{
return StaticGenericCache<TEnum>.TheFunc(value);
}
private static class StaticGenericCache<T>
where T : struct, Enum
{
public static Func<T, int> TheFunc = GenerateFunc<T>();
}
private static Func<TEnum, int> GenerateFunc<TEnum>()
where TEnum : struct, Enum
{
var inputParameter = Expression.Parameter(typeof(TEnum));
var body = Expression.Convert(inputParameter, typeof(int)); // means: (int)input;
var lambda = Expression.Lambda<Func<TEnum, int>>(body, inputParameter);
var func = lambda.Compile();
return func;
}
}
}
The Consistency Check Table Code
class Program
{
static void Main()
{
var table = GenerateConsistencyTable();
Console.WriteLine(table);
}
private static string GenerateConsistencyTable()
{
var sb = new StringBuilder();
sb.AppendLine(GenerateHeader());
sb.AppendLine(GenerateUnderHeader());
foreach (var methodName in _methodNames)
{
sb.AppendLine(CheckAllEnumsForMethod(methodName));
}
return sb.ToString().Trim();
}
private static readonly string[] _methodNames = new string[]
{
nameof(EnumToIntBenchmark.Methods.Convert_ToInt32),
nameof(EnumToIntBenchmark.Methods.CastTo_Object_Int),
nameof(EnumToIntBenchmark.Methods.ByPointers_Switch_Byte),
nameof(EnumToIntBenchmark.Methods.ByPointers_Switch_SByte),
nameof(EnumToIntBenchmark.Methods.CompiledLambdaFunc),
nameof(EnumToIntBenchmark.Methods.Unsafe_As),
nameof(EnumToIntBenchmark.Methods.GetHashCode),
nameof(EnumToIntBenchmark.Methods.ByPointers_DirectInt),
};
private static readonly Type[] _allEnumTypes = new Type[] { typeof(IntEnum), typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(ByteEnum), typeof(SByteEnum) };
private static string GenerateHeader()
{
var line = $"| {"Method",30} ";
foreach (var enumType in _allEnumTypes)
{
line += $"| {enumType.Name,10} ";
}
line += '|';
return line;
}
private static string GenerateUnderHeader()
{
var line = '|' + new string('-', 32);
foreach (var enumType in _allEnumTypes)
{
line += '|' + new string('-', 11) + ':';
}
line += '|';
return line;
}
private static string CheckAllEnumsForMethod(string methodName)
{
var line = $"| {methodName,30} ";
foreach (var enumType in _allEnumTypes)
{
line += $"| {CheckMethodAndEnum(enumType, methodName),10} ";
}
line += '|';
return line;
}
private static string CheckMethodAndEnum(Type enumType, string methodName)
{
var methodsClassType = typeof(EnumToIntBenchmark.Methods);
var methodInfoGeneric = methodsClassType.GetMethods().Single(method => method.Name == methodName && method.IsGenericMethodDefinition);
var methodInfoSpecific = methodInfoGeneric!.MakeGenericMethod(enumType);
var funcType = typeof(Func<,>).MakeGenericType(enumType, typeof(int));
var methodFuncDelegate = Delegate.CreateDelegate(funcType, methodInfoSpecific);
var methodFunc = Convert.ChangeType(methodFuncDelegate, funcType);
var checkMethodGeneric = typeof(Program).GetMethod(nameof(CheckMethodAndEnumCore), BindingFlags.Static | BindingFlags.NonPublic);
var checkMethod = checkMethodGeneric!.MakeGenericMethod(enumType);
return (string)checkMethod.Invoke(null, new object?[] { methodFunc })!;
}
private static string CheckMethodAndEnumCore<TEnum>(Func<TEnum, int> method)
where TEnum : struct, Enum
{
bool anyIsDifferent = false;
try
{
var allEnumValues = Enum.GetValues<TEnum>();
foreach (var enumValue in allEnumValues)
{
var expected = RealCastToInt(enumValue);
var actual = method(enumValue);
if (expected != actual)
{
anyIsDifferent = true;
}
}
}
catch (Exception e)
{
return "throws";
}
return anyIsDifferent ? "differs" : "\u2713 same";
}
private static int RealCastToInt<TEnum>(TEnum enumValue)
where TEnum : struct, Enum
{
switch (enumValue)
{
case IntEnum typedValue: return (int)typedValue;
case LongEnum typedValue: return (int)typedValue;
case UIntEnum typedValue: return (int)typedValue;
case ULongEnum typedValue: return (int)typedValue;
case ByteEnum typedValue: return (int)typedValue;
case SByteEnum typedValue: return (int)typedValue;
default: throw new NotImplementedException($"Not implemented for type: {typeof(TEnum)}");
}
}
enum IntEnum : int
{
None = 0,
One = 1,
MinusOne = -1,
MinValue = int.MinValue,
MaxValue = int.MaxValue,
}
enum LongEnum : long
{
None = 0,
One = 1,
MinusOne = -1,
MinValue = long.MinValue,
MaxValue = long.MaxValue,
}
enum UIntEnum : uint
{
None = 0,
One = 1,
MinValue = uint.MinValue,
MaxValue = uint.MaxValue,
}
enum ULongEnum : ulong
{
None = 0,
One = 1,
MinValue = ulong.MinValue,
MaxValue = ulong.MaxValue,
}
enum ByteEnum : byte
{
None = 0,
One = 1,
MinValue = byte.MinValue,
MaxValue = byte.MaxValue,
}
enum SByteEnum : sbyte
{
None = 0,
One = 1,
MinusOne = -1,
MinValue = sbyte.MinValue,
MaxValue = sbyte.MaxValue,
}
}