如何将泛型枚举转换为 int?

我有一个小方法,看起来像这样:

public void SetOptions<T>() where T : Enum
{
int i = 0;
foreach (T obj in Enum.GetValues(typeof(T)))
{
if (i == 0)
DefaultOption = new ListItem(obj.Description(), obj.ToString());
i++;
DropDownList.Items.Add(new ListItem(obj.Description(), obj.ToString()));
}
}

基本上,我从枚举填充下拉列表。Description()实际上是枚举的扩展方法,所以 T肯定是 enum

但是,我想把 obj强制转换为任何枚举到它的索引,就像这个 (int)obj一样,但是我得到一个错误,说我不能把 T 转换为 int。有办法吗?

45913 次浏览

I'm surprised your code works at all. Enum.GetValues returns an array of integers - which are the values you're looking for. And, as others have mentioned, you can't constrain your generics to an enum.

Instead, you should probably call your Description method as a regular static method and not an extension method.

try this,

public void SetOptions<T>()
{
Type genericType = typeof(T);
if (genericType.IsEnum)
{
foreach (T obj in Enum.GetValues(genericType))
{
Enum test = Enum.Parse(typeof(T), obj.ToString()) as Enum;
int x = Convert.ToInt32(test); // x is the integer value of enum
..........
..........
}
}
}

Can you abuse GetHashCode for this?

public enum MyEnum
{
Foo = 100,
Bar = 200,
Fizz = 0
}


static void Main(string[] args)
{
var i1 = MyEnum.Foo.GetHashCode();  // i1 = 100
var i2 = MyEnum.Bar.GetHashCode();  // i2 = 200
var i3 = MyEnum.Fizz.GetHashCode(); // i3 = 0
}

Please note: "GetHashCode() is by design useful for only one thing: putting an object in a hash table. Hence the name." - E. Lippert

To expand on Jan's answer concerning generics and enum values:

void MyFunc<T>(T value)
{
Type t = typeof(T);
if(t.IsEnum)
{
int valueAsInt = value.GetHashCode(); // returns the integer value
}
}

Using LINQ this can be done elegantly:

public static void SetOptions<T>(this DropDownList dropDownList)
{
if (!typeof(T).IsEnum)
{
throw new ArgumentException("Type must be an enum type.");
}


dropDownList.Items.AddRange(Enum
.GetValues(typeof(T))
.Cast<Enum>()
.Select(x => new ListItem(x.ToString(), Convert.ToInt32(x).ToString()))
.ToArray());
}

This one is working with any underlying type

Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()))

For example when you want add a value to SqlCommand, which converts enums to 0 and you have to explicitly cast it to matching type. But we can write following extension:

public static void AddEnum(this SqlParameterCollection parameters, string parameterName, Enum value)
{
parameters.AddWithValue(parameterName, Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType())));
}

Which is doing everything for us.

Here's a simpler way.

Since Enum implements IConvertible we can use ToInt32(..).

int? value = (enumCandidate as IConvertible)?.ToInt32(CultureInfo.InvariantCulture.Numberformat);

Or if you'd like a general purpose method for generic enums:

public static int GetEnumValue<T>(T inputEnum) where T: struct, IConvertible
{
Type t = typeof(T);
if (!t.IsEnum)
{
throw new ArgumentException("Input type must be an enum.");
}


return inputEnum.ToInt32(CultureInfo.InvariantCulture.NumberFormat);


}

Or yet more general:

public static int GetEnumValue(object enumInput)
{
Type t = enumInput.GetType();
if (!t.IsEnum)
{
throw new ArgumentException("Input type must be an enum.");
}


return ((IConvertible)inputEnum).ToInt32(CultureInfo.InvariantCulture.NumberFormat);


}

You could also cast your value to object first and then to int.

###C# 7.3 and above

With the Enum generic constraint.

public static int EnumToInt<TValue>(this TValue value) where TValue : Enum
=> (int)(object)value;

###Below C# 7.3

Without the Enum generic constraint.

public static int EnumToInt<TValue>(this TValue value)  where TValue : struct, IConvertible
{
if(!typeof(TValue).IsEnum)
{
throw new ArgumentException(nameof(value));
}


return (int)(object)value;
}

If your enum inherits from other types for example from byte the cast to int will throw an InvalidCastException.

You could either check if the base type of the enum is an integer.

public static int EnumToInt<TValue>(this TValue value) where TValue : Enum
{
if (!typeof(int).IsAssignableFrom(Enum.GetUnderlyingType(typeof(TValue))))
throw new ArgumentException(nameof(TValue));


return (int)(object)value;
}

Or you if you use Convert.ToInt32 it will use the IConvertible interface of int32 to convert the incompatible types.

public static int EnumToInt<TValue>(this TValue value) where TValue : Enum
=> Convert.ToInt32(value);

Just be aware the converting uint to int and signed/unsigned pairs can cause unintended behavior. (Boxing to IConvertible and the converting is less performant than just unboxing.)

Try this: (assuming that the TEnum has a numeration from 0 to n)

public void SetOptions<TEnum>() where TEnum : Enum
{
foreach (TEnum obj in Enum.GetValues(typeof(TEnum)))
{
var i = (int)(object)obj;
if (i == 0) DefaultOption = new ListItem(obj.Description(), obj.ToString());
DropDownList.Items.Add(new ListItem(obj.Description(), obj.ToString()));
}
}

If you are targeting .NET Core, you can utilize Unsafe.As<TFrom, TTo> in the System.Runtime.CompilerServices namespace, as explained on MSDN. The advantage here is there will be no boxing done, which is the only real performance cost in the other answers here.

private static int EnumToInt<TEnum>(TEnum enumValue) where TEnum : Enum
{
return Unsafe.As<TEnum, int>(ref enumValue);
}

Note that this approach suffers from the safe issue as other existing answers do: there is not a guarantee that the given enum is a compatible int type, which is likely the same reason this functionality is not baked-in. If you are using this approach internally where you can be sure that any enum passed to it is a compatible type, then this is likely the most efficient approach.

Here is a link to an issue on dotnet's GitHub page where where this issue was raised, and some of the developers elaborated a bit on this approach if you would like to learn more.

if you restict the generic T to be an Enum using

where T: Enum

you can then use the one-liner below

public static int GetIndexFromEnum<T>(T enumValue) where T : Enum {
int index = Convert.ToInt32(enumValue);
return index;
}

This seems like the simplest solution, as long as you can guarantee T is going to be an Enum.

Here's my solution for C# 7.3 and up. Not an exact match to OP's question, but probably useful for people finding this from Google. The main advantage over the other answers is it returns a ulong, which means any of the permissible enum types will fit in it. I also made a comparison of the machine code for this and a few of the other answers. Yes, I was bored and in the mood for a little premature optimization.

private static unsafe ulong EnumAsUInt64<T>(T item) where T : unmanaged, Enum
{
ulong x;
if (sizeof(T) == 1)
x = *(byte*)(&item);
else if (sizeof(T) == 2)
x = *(ushort*)(&item);
else if (sizeof(T) == 4)
x = *(uint*)(&item);
else if (sizeof(T) == 8)
x = *(ulong*)(&item);
else
throw new ArgumentException("Argument is not a usual enum type; it is not 1, 2, 4, or 8 bytes in length.");
return x;
}

Just cast the generic T to object first

T value;
int int_value = (int)(object)value;

That's it.

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,
}
}