As Jason Tyler said in a comment, this has a problem that [Flags]
enumerations won't be properly handled.
I ran this in C# interactive, and it threw an exception (as expected, but not desired) for a valid [Flags]
value:
> public static T ToEnum<T>(this int value)
. {
. Type type = typeof(T);
.
. if (!type.IsEnum)
. {
. throw new ArgumentException($"{type} is not an enum.");
.
. }
.
. if (!type.IsEnumDefined(value))
. {
. throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
. }
.
. return (T)Enum.ToObject(type, value);
. }
> [Flags] public enum Test { Flag1 = 0x01, Flag2 = 0x02 }
> ToEnum<Test>(3);
3 is not a valid ordinal of type Submission#1+Test.
+ Submission#0.ToEnum<T>(int)
So, we might have to look at a different way of going about this.
There's an answer on Stack Overflow, but I don't like it. They use string comparison and validation, and it just feels like a hack.
So what can we do?
Well, we have Enum.GetValues
, I bet we can come up with a solution there, assuming we interpret all the values as a numeric type.
The hard part is picking the type. Enums can be any numeric type, so we have to decide on one.
Fun fact: every numeric type can be cast to the others in C#, there just might be overflow issues. So if we look at all the numeric types, we can find that we should always be able to cast them to a long
or ulong
, depending on whether they're signed or not.
OK, so how do we figure out what number type it is?
There's another magic function in Enum
: Enum.GetUnderlyingType
, I think that'll do.
Of course, this also means we can no longer take an int
input, because we want to support any number. We could take in an object
, but then we have to deal with boxing/unboxing, and that can get prohibitively expensive with frequent use, but we'll work that out-of-the-way later.
We can use some bit-fiddling to find out if a value is only composed of valid Enum values. Basically, we'll do the following:
- Loop through the valid flags options:
- If
providedValue ∧ enumValue > 0
, then providedValue = providedValue ⊕ enumValue
- If
providedValue > 0
then it is not composed strictly of the valid enum options
For those unfamiliar with boolean algebra:
∧
= "AND", take all bits of both values and compare them such that the result value only contains those that are 1
in each value, this is &
in C#;
⊕
= "XOR", take all bits of both values and compare them such that the result value only contains those that are 1
in one value, and 0
in another, this is ^
in C#;
So, if we rewrite this, it gets a bit ugly, but we'll clean some of that up:
public static TEnum ToEnum<TEnum>(this object value)
{
Type type = typeof(TEnum);
if (!type.IsEnum)
{
throw new ArgumentException($"{type} is not an enum.");
}
if (type.GetCustomAttributes(typeof(FlagsAttribute), true).Length > 0)
{
var values = Enum.GetValues(type);
switch (Enum.GetUnderlyingType(type).FullName)
{
case "System.Byte":
{
var myVal = (byte)value;
foreach (byte val in values)
{
if ((myVal & val) > (byte)0)
{
myVal ^= val;
}
}
if (myVal > (byte)0)
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
break;
case "System.SByte":
{
var myVal = (sbyte)value;
foreach (sbyte val in values)
{
if ((myVal & val) > (sbyte)0)
{
myVal ^= val;
}
}
if (myVal > (sbyte)0)
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
break;
case "System.UInt16":
{
var myVal = (ushort)value;
foreach (ushort val in values)
{
if ((myVal & val) > (ushort)0)
{
myVal ^= val;
}
}
if (myVal > (ushort)0)
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
break;
case "System.Int16":
{
var myVal = (short)value;
foreach (short val in values)
{
if ((myVal & val) > (short)0)
{
myVal ^= val;
}
}
if (myVal > (short)0)
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
break;
case "System.UInt32":
{
var myVal = (uint)value;
foreach (uint val in values)
{
if ((myVal & val) > (uint)0)
{
myVal ^= val;
}
}
if (myVal > (uint)0)
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
break;
case "System.Int32":
{
var myVal = (int)value;
foreach (int val in values)
{
if ((myVal & val) > (int)0)
{
myVal ^= val;
}
}
if (myVal > (int)0)
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
break;
case "System.UInt64":
{
var myVal = (ulong)value;
foreach (ulong val in values)
{
if ((myVal & val) > (ulong)0)
{
myVal ^= val;
}
}
if (myVal > (ulong)0)
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
break;
case "System.Int64":
{
var myVal = (long)value;
foreach (long val in values)
{
if ((myVal & val) > (long)0)
{
myVal ^= val;
}
}
if (myVal > (long)0)
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
break;
default:
throw new ArgumentException($"{type} does not have a valid backing type ({underlyingType}).");
break;
}
}
else
{
if (!type.IsEnumDefined(value))
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
return (TEnum)Enum.ToObject(type, value);
}
This will work as expected on [Flags]
enumerations, which is great! But, there's some improvement to be made.
First and foremost: how do we get rid of that object
parameter?
Well, we have a few options, but the easiest is to redefine our ToEnum
on each numeric type, and create a version that takes a dynamic
. It's ugly, but works:
public static TEnum ToEnum<TEnum>(this byte value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this sbyte value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this ushort value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this short value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this uint value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this int value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this ulong value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this long value) => DynamicToEnum<TEnum>(value);
private static TEnum DynamicToEnum<TEnum>(dynamic value)
{
...
For each repeated case
body, there's no good way to do this. With the use dynamic
, we lose all safety of our types. There's no way to constrain it generically, because we can't say "it can be any number."
The best we could do would be a local function something like the following:
void testEnum<T>(T myVal, Func<T, bool> aboveZero, Func<T, T, T> and, Func<T, T, T> xor)
{
foreach (T val in values)
{
if (aboveZero(and(myVal, val)))
{
myVal = xor(myVal, val);
}
}
if (aboveZero(myVal))
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
Which means, we can use it to create a more readable version of our function:
public static TEnum ToEnum<TEnum>(this byte value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this sbyte value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this ushort value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this short value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this uint value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this int value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this ulong value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this long value) => DynamicToEnum<TEnum>(value);
private static TEnum DynamicToEnum<TEnum>(dynamic value)
{
Type type = typeof(TEnum);
if (!type.IsEnum)
{
throw new ArgumentException($"{type} is not an enum.");
}
if (type.GetCustomAttributes(typeof(FlagsAttribute), true).Length > 0)
{
var values = Enum.GetValues(type);
void testEnum<T>(T myVal, Func<T, bool> aboveZero, Func<T, T, T> and, Func<T, T, T> xor)
{
foreach (T val in values)
{
if (aboveZero(and(myVal, val)))
{
myVal = xor(myVal, val);
}
}
if (aboveZero(myVal))
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
var underlyingType = Enum.GetUnderlyingType(type).FullName;
switch (underlyingType)
{
case "System.Byte":
testEnum((Byte)value, (v) => v > (Byte)0, (v1, v2) => (Byte)(v1 & v2), (v1, v2) => (Byte)(v1 ^ v2));
break;
case "System.SByte":
testEnum((SByte)value, (v) => v > (SByte)0, (v1, v2) => (SByte)(v1 & v2), (v1, v2) => (SByte)(v1 ^ v2));
break;
case "System.UInt16":
testEnum((UInt16)value, (v) => v > (UInt16)0, (v1, v2) => (UInt16)(v1 & v2), (v1, v2) => (UInt16)(v1 ^ v2));
break;
case "System.Int16":
testEnum((Int16)value, (v) => v > (Int16)0, (v1, v2) => (Int16)(v1 & v2), (v1, v2) => (Int16)(v1 ^ v2));
break;
case "System.UInt32":
testEnum((UInt32)value, (v) => v > (UInt32)0, (v1, v2) => (UInt32)(v1 & v2), (v1, v2) => (UInt32)(v1 ^ v2));
break;
case "System.Int32":
testEnum((Int32)value, (v) => v > (Int32)0, (v1, v2) => (Int32)(v1 & v2), (v1, v2) => (Int32)(v1 ^ v2));
break;
case "System.UInt64":
testEnum((UInt64)value, (v) => v > (UInt64)0, (v1, v2) => (UInt64)(v1 & v2), (v1, v2) => (UInt64)(v1 ^ v2));
break;
case "System.Int64":
testEnum((Int64)value, (v) => v > (Int64)0, (v1, v2) => (Int64)(v1 & v2), (v1, v2) => (Int64)(v1 ^ v2));
break;
default:
throw new ArgumentException($"{type} does not have a valid backing type ({underlyingType}).");
break;
}
}
else
{
if (!type.IsEnumDefined(value))
{
throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
}
}
return (TEnum)Enum.ToObject(type, value);
}
Overboard? Probably. But does it work? Oh yeah.
Additionally, as Peter Taylor mentions, you can use where TEnum : struct
to help reduce some of the run-time issues, and bring them to the compile-type.
IsEnumDefined
. It won't work if usingFlags
and the value represents a combination. \$\endgroup\$where T : struct
could use a generic constraint. \$\endgroup\$where T : enum
could be used as a constraint now. \$\endgroup\$