Last active
November 11, 2019 07:22
-
-
Save daiplusplus/aa07ceda23b2ca03184722c3f63ce7ca to your computer and use it in GitHub Desktop.
EnumTraits<T> - Common operations on enums with C#'s `Enum` type constraint. Uses emited IL for fast enum operations without boxing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Reflection.Emit; | |
namespace Jehoel | |
{ | |
/// <summary>Derived from https://devblogs.microsoft.com/premier-developer/dissecting-new-generics-constraints-in-c-7-3/</summary> | |
public static class EnumTraits<TEnum> | |
where TEnum : struct, Enum | |
{ | |
private static readonly String[] _names; | |
private static readonly TEnum[] _values; | |
private static readonly HashSet<TEnum> _valuesSet; | |
private static readonly Func<TEnum,SByte> _toS8 = EnumTraitsIL.CreateEnumToValueCast<TEnum,SByte>(); | |
private static readonly Func<TEnum,Byte> _toU8 = EnumTraitsIL.CreateEnumToValueCast<TEnum,Byte>(); | |
private static readonly Func<TEnum,Int16> _toS16 = EnumTraitsIL.CreateEnumToValueCast<TEnum,Int16>(); | |
private static readonly Func<TEnum,UInt16> _toU16 = EnumTraitsIL.CreateEnumToValueCast<TEnum,UInt16>(); | |
private static readonly Func<TEnum,Int32> _toS32 = EnumTraitsIL.CreateEnumToValueCast<TEnum,Int32>(); | |
private static readonly Func<TEnum,UInt32> _toU32 = EnumTraitsIL.CreateEnumToValueCast<TEnum,UInt32>(); | |
private static readonly Func<TEnum,Int64> _toS64 = EnumTraitsIL.CreateEnumToValueCast<TEnum,Int64>(); | |
private static readonly Func<TEnum,UInt64> _toU64 = EnumTraitsIL.CreateEnumToValueCast<TEnum,UInt64>(); | |
private static readonly Func<Int64,TEnum> _fromS64 = EnumTraitsIL.CreateFromInt64<TEnum>(); | |
private static readonly Func<TEnum,TEnum,Boolean> _hasFlags = EnumTraitsIL.CreateHasFlags<TEnum>(); | |
private static readonly Func<TEnum,TEnum,Boolean> _equals = EnumTraitsIL.CreateEquals<TEnum>(); | |
private static readonly Func<TEnum,Int64,Boolean> _equalsS64 = EnumTraitsIL.CreateEqualsInt64<TEnum>(); | |
private static readonly Func<TEnum,TEnum,TEnum> _bitwiseAnd = EnumTraitsIL.CreateAnd<TEnum>(); | |
private static readonly Func<TEnum,TEnum,TEnum> _bitwiseOr = EnumTraitsIL.CreateOr <TEnum>(); | |
private static readonly Func<TEnum,TEnum,TEnum> _bitwiseXor = EnumTraitsIL.CreateXor<TEnum>(); | |
private static readonly Func<TEnum,TEnum> _bitwiseNeg = EnumTraitsIL.CreateComplement<TEnum>(); | |
static EnumTraits() | |
{ | |
Type type = typeof(TEnum); | |
IsFlags = type.GetCustomAttributes( typeof(FlagsAttribute), inherit: false ).Any(); | |
_names = Enum.GetNames( type ); | |
_values = (TEnum[])Enum.GetValues( type ); // Returns non-distinct values that correspond to `Enum.GetNames` at the same index. | |
System.Diagnostics.Debug.Assert( condition: _names.Length == _values.Length, "Names and Values arrays have different length." ); // Sanity check, this should never happen. | |
_valuesSet = new HashSet<TEnum>( _values ); // Idea: could `_valuesSet` be replaced with an IL-emit `switch` method? | |
Int64 min = Int64.MaxValue; | |
Int64 max = Int64.MinValue; | |
for( Int32 i = 0; i < _values.Length; i++ ) | |
{ | |
Int64 v = _toS64( _values[i] ); | |
AllFlags |= v; | |
min = v < min ? v : min; | |
max = v > max ? v : max; | |
} | |
MinValue = min; | |
MaxValue = max; | |
} | |
public static Boolean IsEmpty => _values.Length == 0; | |
public static Boolean IsFlags { get; } | |
// TODO: Change these properties to use the original TEnum type? | |
public static Int64 MinValue { get; } | |
public static Int64 MaxValue { get; } | |
public static Int64 AllFlags { get; } | |
/// <summary>Returns a new enumerator (using <c>yield return</c>) over all named members of <typeparamref name="TEnum"/>. All names will be distinct, but values may be repeated if the enum has different names with the same value.</summary> | |
public static IEnumerable<(String name, TEnum value)> GetMembers() | |
{ | |
for( Int32 i = 0; i < _names.Length; i++ ) | |
{ | |
yield return ( _names[i], _values[i] ); | |
} | |
} | |
/// <summary>Returns <c>true</c> when <paramref name="integerValue"/> is a defined enum member (by integer value, not a composite of flag values) and sets <paramref name="enumValue"/>. Otherwise returns <c>false</c> and <paramref name="enumValue"/> is undefined.</summary> | |
public static Boolean TryConvertToEnum( Int64 integerValue, out TEnum enumValue ) | |
{ | |
TEnum e = FromInt64( integerValue ); | |
if( IsDefined( e ) ) | |
{ | |
enumValue = e; | |
return true; | |
} | |
else | |
{ | |
enumValue = default; | |
return false; | |
} | |
} | |
/// <summary> | |
/// <para>Returns <c>true</c> if every bitwise value in <typeparamref name="TEnum"/> can be represented by <typeparamref name="TDestinationEnumType"/> (i.e. that every <typeparamref name="TEnum"/> value can be unambigously represented by the underlying type of <typeparamref name="TDestinationEnumType"/>).</para> | |
/// <para>IMPORTANT: This method currently throws <see cref="NotImplementedException"/> if either <typeparamref name="TEnum"/> or <typeparamref name="TDestinationEnumType"/> is a Flags enum, as more research is needed.</para> | |
/// </summary> | |
/// <exception cref="NotImplementedException"/> | |
public static Boolean CanCastTo<TDestinationEnumType>() | |
where TDestinationEnumType : struct, Enum | |
{ | |
// Casting is only possible when TDestinationEnumType's underlying integer type is wide enough to accomodate TEnum's minimum and maximum values (so signed-to-unsigned, and unsigned-to-signed casts are valid as they preserve information). | |
// So it is legal to cast this: `enum U64Enum : UInt64 { Foo = 1 }` to `enum S8Enum : SByte { Bar = 1 }`. | |
// (It's legal to cast a UInt64.MaxValue or Int64.MaxValue enum to SByte and back too, y'know... not that you'd ever want to do that (it only works if the 64-bit value is -1 due to sign-extension magic, otherwise there's information-loss. | |
// This can be done in O(1) time when you know the maximum value of an enum, however doing a simple range-check of the MaxValue DOES NOT WORK because of how Sign-Extension works. | |
// So a simpler, if more expensive approach, is to test if every defined value in source can be casted to dest, and then cast it back and verify the value is preserved. | |
// NOTE: I think this method will return a false-positive (i.e. a `true` value when it should be `false`) for an Int64 Flags enum being cast to an Int32 if one of the flags will work due to sign-extension but other flags won't... I think? | |
// For now, return false. | |
if( IsFlags || EnumTraits<TDestinationEnumType>.IsFlags ) throw new NotImplementedException( "The safe cast-check for Flags enums is not yet implemented." ); | |
// weird, `#if DEBUG` wasn't working... | |
const Boolean debugMode = false; | |
if( debugMode ) | |
{ | |
List<TEnum> source = _valuesSet.ToList(); | |
List<TDestinationEnumType> casted = source.Select( EnumCaster<TEnum,TDestinationEnumType>.Cast ).ToList(); | |
List<TEnum> castedBack = casted.Select( EnumCaster<TDestinationEnumType,TEnum>.Cast ).ToList(); | |
return Enumerable.SequenceEqual( source, castedBack ); | |
} | |
else | |
{ | |
foreach( TEnum value in _valuesSet ) | |
{ | |
TDestinationEnumType casted = EnumCaster<TEnum,TDestinationEnumType>.Cast( value ); | |
TEnum castedBack = EnumCaster<TDestinationEnumType,TEnum>.Cast( casted ); | |
if( !_equals( value, castedBack ) ) return false; | |
} | |
return true; | |
} | |
} | |
public static TOtherEnum CastTo<TOtherEnum>( TEnum value ) | |
where TOtherEnum : struct, Enum | |
{ | |
return EnumCaster<TEnum,TOtherEnum>.Cast( value ); | |
} | |
public static TEnum CastFrom<TOtherEnum>( TOtherEnum value ) | |
where TOtherEnum : struct, Enum | |
{ | |
return EnumCaster<TOtherEnum,TEnum>.Cast( value ); | |
} | |
#region IsSupersetOf / IsSubsetOf | |
/// <summary>Indicates that this enum (specified by <typeparamref name="TEnum"/>) has identically-valued members as <typeparamref name="TOtherEnum"/>. This method only considers the distinct values of <typeparamref name="TOtherEnum"/> (<see cref="DistinctValues"/>, so this method does not compare their underlying types, member names nor compare the presence (or lack-of) <see cref="FlagsAttribute"/>). The comparison is performed by using <see cref="HashSet{T}"/>'s set operations after casting <typeparamref name="TOtherEnum"/> values to <typeparamref name="TEnum"/>.</summary> | |
public static Boolean HasValuesEquivalentTo<TOtherEnum>() where TOtherEnum : struct, Enum | |
{ | |
// Unfortunately, HashSet's set operations require identical T, so we can't just do this: | |
// return _valuesSet.SetEquals( EnumTraits<TOtherEnum>._valuesSet ); | |
// One option is to convert both to Int64: | |
#if ATTEMPT_1 | |
HashSet<Int64> selfInt64 = new HashSet<Int64>( _valuesSet.Select( _toInt64 ) ); | |
HashSet<Int64> otherInt64 = new HashSet<Int64>( EnumTraits<TOtherEnum>._valuesSet.Select( EnumTraits<TOtherEnum>._toInt64 ) ); | |
return selfInt64.SetEquals( otherInt64 ); | |
#else | |
// Another approach is to cast the foreign TOtherEnum values to TEnum, then let HashSet<TEnum> compare them. This also means we can have some shortcuts. | |
if( _valuesSet.Count == EnumTraits<TOtherEnum>._valuesSet.Count && MaxValue == EnumTraits<TOtherEnum>.MaxValue && MinValue == EnumTraits<TOtherEnum>.MinValue && AllFlags == EnumTraits<TOtherEnum>.AllFlags ) | |
{ | |
if( EnumTraits<TOtherEnum>.CanCastTo<TEnum>() ) | |
{ | |
HashSet<TEnum> other = new HashSet<TEnum>( EnumTraits<TOtherEnum>._valuesSet.Select( EnumCaster<TOtherEnum,TEnum>.Cast ) ); | |
return _valuesSet.SetEquals( other ); | |
} | |
else | |
{ | |
return false; | |
} | |
} | |
else | |
{ | |
return false; | |
} | |
#endif | |
} | |
/// <summary>Indicates that every distinct value in <typeparamref name="TEnum"/> exists in <typeparamref name="TOtherEnum"/>. Note that <typeparamref name="TEnum"/> may or may not have other values not in <typeparamref name="TOtherEnum"/>.</summary> | |
public static Boolean IsValueSupersetOf<TOtherEnum>() where TOtherEnum : struct, Enum | |
{ | |
HashSet<TEnum> other = new HashSet<TEnum>( EnumTraits<TOtherEnum>._valuesSet.Select( EnumCaster<TOtherEnum,TEnum>.Cast ) ); | |
return _valuesSet.IsSupersetOf( other ); | |
} | |
/// <summary>(The terms "Proper Subset" and "Strict Subset" are identical) Indicates that <typeparamref name="TEnum"/> has every distinct value in <typeparamref name="TOtherEnum"/> AND that <typeparamref name="TEnum"/> has other values not in <typeparamref name="TOtherEnum"/>.</summary> | |
public static Boolean IsValueProperSupersetOf<TOtherEnum>() where TOtherEnum : struct, Enum | |
{ | |
HashSet<TEnum> other = new HashSet<TEnum>( EnumTraits<TOtherEnum>._valuesSet.Select( EnumCaster<TOtherEnum,TEnum>.Cast ) ); | |
return _valuesSet.IsProperSupersetOf( other ); | |
} | |
/// <summary>Indicates that every distinct value in <typeparamref name="TEnum"/> exists in <typeparamref name="TOtherEnum"/>. Note that <typeparamref name="TOtherEnum"/> may or may not have other values not in <typeparamref name="TEnum"/>.</summary> | |
public static Boolean IsValueSubsetOf<TOtherEnum>() where TOtherEnum : struct, Enum | |
{ | |
HashSet<TEnum> other = new HashSet<TEnum>( EnumTraits<TOtherEnum>._valuesSet.Select( EnumCaster<TOtherEnum,TEnum>.Cast ) ); | |
return _valuesSet.IsSubsetOf( other ); | |
} | |
/// <summary>(The terms "Proper Subset" and "Strict Subset" are identical) Indicates that every distinct value in <typeparamref name="TEnum"/> exists in <typeparamref name="TOtherEnum"/> AND that <typeparamref name="TOtherEnum"/> has other values not in <typeparamref name="TEnum"/>.</summary> | |
public static Boolean IsValueProperSubsetOf<TOtherEnum>() where TOtherEnum : struct, Enum | |
{ | |
HashSet<TEnum> other = new HashSet<TEnum>( EnumTraits<TOtherEnum>._valuesSet.Select( EnumCaster<TOtherEnum,TEnum>.Cast ) ); | |
return _valuesSet.IsProperSubsetOf( other ); | |
} | |
// TOOD: Methods that ensure that when values match, so do the names - that's how you can tell *intentional* enum subsets. | |
// TODO: Ooooh, what if there was also a method to map enum values by name? | |
// e.g. `enum Foo { Red = 1, Green = 2 } enum Bar { Red = 2, Green = 10 }`, `EnumTraits<Bar>. (Bar.Red).Cast | |
// | |
#endregion | |
/// <summary>It is claimed this method is almost an order of magnitude faster then <see cref="System.Enum.IsDefined(Type, object)"/> (probably due to caching?).</summary> | |
public static Boolean IsDefined( Int64 integerValue ) | |
{ | |
TEnum enumValue = FromInt64( integerValue ); | |
return _valuesSet.Contains( enumValue ); | |
} | |
/// <summary>It is claimed this method is almost an order of magnitude faster then <see cref="System.Enum.IsDefined(Type, object)"/> (probably due to caching?).</summary> | |
public static Boolean IsDefined( TEnum value ) | |
{ | |
return _valuesSet.Contains( value ); | |
} | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> from <see cref="Int64"/> to <typeparamref name="TEnum"/> without boxing.</summary> | |
public static TEnum FromInt64( Int64 value ) | |
{ | |
return _fromS64( value ); | |
} | |
#region Boxing-free casts | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> to <see cref="SByte"/> without boxing.</summary> | |
[CLSCompliant( isCompliant: false )] | |
public static SByte ToSByte ( TEnum value ) => _toS8 ( value ); | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> to <see cref="Byte"/> without boxing.</summary> | |
public static Byte ToByte ( TEnum value ) => _toU8 ( value ); | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> to <see cref="Int16"/> without boxing.</summary> | |
public static Int16 ToInt16 ( TEnum value ) => _toS16( value ); | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> to <see cref="UInt16"/> without boxing.</summary> | |
[CLSCompliant( isCompliant: false )] | |
public static UInt16 ToSUInt16( TEnum value ) => _toU16( value ); | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> to <see cref="Int32"/> without boxing.</summary> | |
public static Int32 ToInt32 ( TEnum value ) => _toS32( value ); | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> to <see cref="UInt32"/> without boxing.</summary> | |
[CLSCompliant( isCompliant: false )] | |
public static UInt32 ToUInt32 ( TEnum value ) => _toU32( value ); | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> to <see cref="Int64"/> without boxing.</summary> | |
public static Int64 ToInt64 ( TEnum value ) => _toS64( value ); | |
/// <summary>Performs an unchecked cast of <paramref name="value"/> to <see cref="UInt64"/> without boxing.</summary> | |
[CLSCompliant( isCompliant: false )] | |
public static UInt64 ToUInt64 ( TEnum value ) => _toU64( value ); | |
#endregion | |
#region Operators | |
public static Boolean Equals( TEnum left, TEnum right ) | |
{ | |
return _equals( left, right ); | |
} | |
public static Boolean NotEqual( TEnum left, TEnum right ) | |
{ | |
return !_equals( left, right ); | |
} | |
public static Boolean Equals( TEnum left, Int64 right ) | |
{ | |
return _equalsS64( left, right ); | |
} | |
public static TEnum Or( TEnum left, TEnum right ) | |
{ | |
return _bitwiseOr( left, right ); | |
} | |
public static TEnum And( TEnum left, TEnum right ) | |
{ | |
return _bitwiseAnd( left, right ); | |
} | |
public static TEnum Negate( TEnum value ) | |
{ | |
return _bitwiseNeg( value ); | |
} | |
public static TEnum Xor( TEnum left, TEnum right ) | |
{ | |
return _bitwiseXor( left, right ); | |
} | |
#endregion | |
/// <summary> | |
/// <para>When <typeparamref name="TEnum"/> is a flag enum (see <see cref="IsFlags"/>) then it verifies that all of the bits of <paramref name="value"/> can be obtained by ORing all defined members of <typeparamref name="TEnum"/>. This method returns <c>true</c> if <paramref name="value"/> is zero.</para> | |
/// <para>When <typeparamref name="TEnum"/> is NOT a flag enum (see <see cref="IsFlags"/>) then it verifies that <paramref name="value"/> is an explicit defined value by calling <see cref="IsDefined(TEnum)"/>. If <paramref name="value"/> is zero-valued, then this method will return <c>true</c> only if <typeparamref name="TEnum"/> explicitly defines a zero-valued member.</para> | |
/// <para>In both cases, this method runs in constant-time <c>O(1)</c> (and is considerably faster than <see cref="Enum.IsDefined(Type, object)"/>)</para> | |
/// </summary> | |
public static Boolean IsValid( TEnum value ) | |
{ | |
if( IsFlags ) | |
{ | |
Int64 value64 = _toS64( value ); // TODO: How does this work with strange sign-extension scenarios? | |
return ( value64 & AllFlags ) == value64; | |
} | |
else | |
{ | |
return IsDefined( value ); | |
} | |
} | |
/// <summary>Returns <c>true</c> when <paramref name="value"/> is a composite (bitwise OR) of other distinct flag values in <typeparamref name="TEnum"/>.</summary> | |
public static Boolean IsComposite( TEnum value ) | |
{ | |
Int64 value64 = _toS64( value ); | |
Int64 bits = value64; | |
foreach( TEnum definedValue in DistinctValues ) | |
{ | |
Int64 definedValue64 = _toS64( definedValue ); | |
if( value64 == definedValue64 || definedValue64 == 0 ) | |
{ | |
continue; // Skip own values and zeroes. | |
} | |
else | |
{ | |
if( _hasFlags( value, definedValue ) ) | |
{ | |
// Only remove the bits if all of the flags are present in the original value. | |
bits = bits & ~definedValue64; | |
// If all of the bits are now zero then `value` can be defined by all defined enum values. | |
if( bits == 0 ) return true; | |
} | |
} | |
} | |
return false; | |
} | |
/// <summary>Faster than <see cref="Enum.HasFlag(Enum)"/>.</summary> | |
public static Boolean HasFlags( TEnum value, TEnum flags ) | |
{ | |
//return ( _toInt64( value ) & _toInt64( flags ) ) != 0; | |
return _hasFlags( value, flags ); | |
} | |
/// <summary>Each member of the enum's name. Each element in this list has its corresponding value at the same index in <see cref="Values"/> (both lists always have the same length).</summary> | |
public static IReadOnlyList<String> Names => _names; | |
/// <summary>Each member of the enum's value. Each element in this list has its corresponding value at the same index in <see cref="Names"/> (both lists always have the same length). This list will contain duplicate values when the enum's definition includes differently-named members with the same numeric value.</summary> | |
public static IReadOnlyList<TEnum> Values => _values; | |
/// <summary>Contains the distinct values of <typeparamref name="TEnum"/> (the "Name" of the value is determined by the CLR). This DOES include any zero values.</summary> | |
public static IEnumerable<TEnum> DistinctValues => _valuesSet; | |
} | |
internal static class EnumCaster<TEnumSource,TEnumDest> | |
where TEnumSource : struct, Enum | |
where TEnumDest : struct, Enum | |
{ | |
private static readonly Func<TEnumSource,TEnumDest> _func = EnumTraitsIL.CreateEnumToEnumCast<TEnumSource,TEnumDest>(); | |
public static TEnumDest Cast( TEnumSource value ) | |
{ | |
return _func( value ); | |
} | |
} | |
internal static class EnumTraitsIL | |
{ | |
private static Func<TArg0,TReturn> CreateEnumFunc<TArg0,TReturn>( Type enumTraitsType, String name, IEnumerable<OpCode> ops ) | |
{ | |
DynamicMethod method = new DynamicMethod( | |
name : typeof(TArg0).Name + "_" + name, | |
returnType : typeof(TReturn), | |
parameterTypes: new[] { typeof(TArg0) }, | |
m : enumTraitsType.Module, | |
skipVisibility: true | |
); | |
ILGenerator ilGen = method.GetILGenerator(); | |
foreach( OpCode op in ops ) ilGen.Emit( op ); | |
return (Func<TArg0,TReturn>)method.CreateDelegate( typeof(Func<TArg0,TReturn>) ); | |
} | |
private static Func<TArg0,TArg1,TReturn> CreateEnumFunc<TArg0,TArg1,TReturn>( Type enumTraitsType, String name, IEnumerable<OpCode> ops ) | |
{ | |
DynamicMethod method = new DynamicMethod( | |
name : typeof(TArg0).Name + "_" + name, | |
returnType : typeof(TReturn), | |
parameterTypes: new[] { typeof(TArg0), typeof(TArg1) }, | |
m : enumTraitsType.Module, | |
skipVisibility: true | |
); | |
ILGenerator ilGen = method.GetILGenerator(); | |
foreach( OpCode op in ops ) ilGen.Emit( op ); | |
return (Func<TArg0,TArg1,TReturn>)method.CreateDelegate( typeof(Func<TArg0,TArg1,TReturn>) ); | |
} | |
// https://devblogs.microsoft.com/premier-developer/dissecting-new-generics-constraints-in-c-7-3/ | |
public static Func<Int64,TEnum> CreateFromInt64<TEnum>() | |
where TEnum : struct, Enum | |
{ | |
Type underlyingType = Enum.GetUnderlyingType( typeof(TEnum) ); | |
// Protip: write snippets in LinqPad and use the IL output (with optimizations enabled) to see the generated IL. | |
// Then you know what opcodes to use to when using IL-emit. | |
List<OpCode> ops = new List<OpCode>(); | |
ops.Add(OpCodes.Ldarg_0); | |
if( underlyingType == typeof(SByte) ) | |
{ | |
ops.Add(OpCodes.Conv_I1); | |
} | |
else if( underlyingType == typeof(Byte) ) | |
{ | |
ops.Add(OpCodes.Conv_U1); | |
} | |
else if( underlyingType == typeof(Int16) ) | |
{ | |
ops.Add(OpCodes.Conv_I2); | |
} | |
else if( underlyingType == typeof(UInt16) ) | |
{ | |
ops.Add(OpCodes.Conv_U2); | |
} | |
else if( underlyingType == typeof(Int32) ) | |
{ | |
ops.Add(OpCodes.Conv_I4); | |
} | |
else if( underlyingType == typeof(UInt32) ) | |
{ | |
ops.Add(OpCodes.Conv_U4); | |
} | |
else if( underlyingType == typeof(Int64) || underlyingType == typeof(UInt64) ) | |
{ | |
// no Conv step needed, regardless of UInt64 vs Int64 | |
} | |
else | |
{ | |
throw new InvalidOperationException( "Unsupported Enum Base type: " + underlyingType.FullName ); | |
} | |
ops.Add(OpCodes.Ret); | |
return CreateEnumFunc<Int64,TEnum>( typeof(EnumTraits<TEnum>), name: "_ConvertFromInt64", ops ); | |
} | |
public static Func<TEnum,TEnum,Boolean> CreateHasFlags<TEnum>() | |
where TEnum : struct, Enum | |
{ | |
OpCode[] ops = new[] | |
{ | |
OpCodes.Ldarg_0, | |
OpCodes.Ldarg_1, | |
OpCodes.And, // x := args[0] & args[1] | |
OpCodes.Ldarg_1, | |
OpCodes.Ceq, // y := x == args[1] | |
OpCodes.Ret // return y | |
}; | |
return CreateEnumFunc<TEnum,TEnum,Boolean>( typeof(EnumTraits<TEnum>), "HasFlags", ops ); | |
} | |
public static Func<TEnum,TEnum,Boolean> CreateEquals<TEnum>() | |
where TEnum : struct, Enum | |
{ | |
OpCode[] ops = new[] | |
{ | |
OpCodes.Ldarg_0, | |
OpCodes.Ldarg_1, | |
OpCodes.Ceq, // x := args[0] == args[1] | |
OpCodes.Ret // return y | |
}; | |
return CreateEnumFunc<TEnum,TEnum,Boolean>( typeof(EnumTraits<TEnum>), "Equals", ops ); | |
} | |
public static Func<TEnum,Int64,Boolean> CreateEqualsInt64<TEnum>() | |
where TEnum : struct, Enum | |
{ | |
OpCode[] ops = new[] | |
{ | |
OpCodes.Ldarg_0, | |
OpCodes.Conv_I8, | |
OpCodes.Ldarg_1, | |
OpCodes.Ceq, // x := args[0] == args[1] | |
OpCodes.Ret // return x | |
}; | |
return CreateEnumFunc<TEnum,Int64,Boolean>( typeof(EnumTraits<TEnum>), "EqualsInt64", ops ); | |
} | |
private static Func<TEnum,TEnum,TEnum> CreateBinaryOp<TEnum>( OpCode op ) | |
where TEnum : struct, Enum | |
{ | |
OpCode[] ops = new[] | |
{ | |
OpCodes.Ldarg_0, | |
OpCodes.Ldarg_1, | |
op, | |
OpCodes.Ret | |
}; | |
return CreateEnumFunc<TEnum,TEnum,TEnum>( typeof(EnumTraits<TEnum>), name: op.ToString(), ops ); | |
} | |
public static Func<TEnum,TEnum,TEnum> CreateOr <TEnum>() where TEnum : struct, Enum => CreateBinaryOp<TEnum>( OpCodes.Or ); | |
public static Func<TEnum,TEnum,TEnum> CreateAnd<TEnum>() where TEnum : struct, Enum => CreateBinaryOp<TEnum>( OpCodes.And ); | |
public static Func<TEnum,TEnum,TEnum> CreateXor<TEnum>() where TEnum : struct, Enum => CreateBinaryOp<TEnum>( OpCodes.Xor ); | |
public static Func<TEnum,TEnum> CreateComplement<TEnum>() | |
where TEnum : struct, Enum | |
{ | |
Type underlyingType = Enum.GetUnderlyingType( typeof(TEnum) ); | |
List<OpCode> ops = new List<OpCode>(); | |
ops.Add( OpCodes.Ldarg_0 ); | |
ops.Add( OpCodes.Not ); | |
if( underlyingType == typeof(SByte) ) | |
{ | |
ops.Add( OpCodes.Conv_I1 ); | |
} | |
else if( underlyingType == typeof(Byte) ) | |
{ | |
ops.Add( OpCodes.Conv_U1 ); | |
} | |
else if( underlyingType == typeof(Int16) ) | |
{ | |
ops.Add( OpCodes.Conv_I2 ); | |
} | |
else if( underlyingType == typeof(UInt16) ) | |
{ | |
ops.Add( OpCodes.Conv_U2 ); | |
} | |
else if( underlyingType == typeof(Int32) || underlyingType == typeof(UInt32) || underlyingType == typeof(Int64) || underlyingType == typeof(UInt64) ) | |
{ | |
// This is weird - LinqPad shows that when an enum is SByte, Byte, Int16 or UInt16 then it has a `conv.` instruction. | |
// But when it's Int32, UInt32, Int64 or UInt64 then there's no conversion step, weird. | |
} | |
else | |
{ | |
throw new InvalidOperationException( "Unsupported Enum Base type: " + underlyingType.FullName ); | |
} | |
ops.Add( OpCodes.Ret ); | |
return CreateEnumFunc<TEnum,TEnum>( typeof(EnumTraits<TEnum>), name: "Complement", ops ); | |
} | |
/// <summary>Creates an unchecked cast.</summary> | |
public static Func<TEnumSource,TDest> CreateEnumToValueCast<TEnumSource,TDest>() | |
where TEnumSource : struct, Enum | |
where TDest : unmanaged | |
{ | |
Type sourceType = typeof(TEnumSource); | |
Type destType = typeof(TDest); | |
Type sourceUnderlyingType = Enum.GetUnderlyingType( sourceType ); | |
OpCode? castOp = GetCastOp( sourceType: sourceUnderlyingType, destType: destType ); | |
OpCode[] ops; | |
if( castOp.HasValue ) | |
{ | |
ops = new[] | |
{ | |
OpCodes.Ldarg_0, | |
castOp.Value, | |
OpCodes.Ret | |
}; | |
} | |
else | |
{ | |
ops = new[] | |
{ | |
OpCodes.Ldarg_0, | |
OpCodes.Ret | |
}; | |
} | |
return CreateEnumFunc<TEnumSource,TDest>( typeof(EnumTraits<TEnumSource>), "Cast_" + sourceType.Name + "_to_" + destType.Name, ops ); | |
} | |
public static Func<TEnumSource,TEnumDest> CreateEnumToEnumCast<TEnumSource,TEnumDest>() | |
where TEnumSource : struct, Enum | |
where TEnumDest : struct, Enum | |
{ | |
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum | |
// > "Every enumeration type has an underlying type, which can be any integral numeric type. The char type cannot be an underlying type of an enum." | |
// The above text links to https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/integral-numeric-types ... | |
// ...which lists the 8 types used here. | |
Type sourceType = typeof(TEnumSource); | |
Type destType = typeof(TEnumDest); | |
Type sourceUnderlyingType = Enum.GetUnderlyingType( sourceType ); // TODO: How does `Enum.GetUnderlyingType` compare to `Type::GetEnumUnderlyingType`? | |
Type destUnderlyingType = Enum.GetUnderlyingType( destType ); | |
OpCode? castOp = GetCastOp( sourceType: sourceUnderlyingType, destType: destUnderlyingType ); | |
OpCode[] ops; | |
if( castOp.HasValue ) | |
{ | |
ops = new[] | |
{ | |
OpCodes.Ldarg_0, | |
castOp.Value, | |
OpCodes.Ret | |
}; | |
} | |
else | |
{ | |
ops = new[] | |
{ | |
OpCodes.Ldarg_0, | |
OpCodes.Ret | |
}; | |
} | |
return CreateEnumFunc<TEnumSource,TEnumDest>( typeof(EnumTraits<TEnumDest>), "Cast_" + sourceType.Name + "_to_" + destType.Name, ops ); | |
} | |
private static OpCode? GetCastOp( Type sourceType, Type destType ) | |
{ | |
if ( destType == typeof(SByte) ) return GetCastOpToS8 ( sourceType ); | |
else if( destType == typeof(Byte) ) return GetCastOpToU8 ( sourceType ); | |
else if( destType == typeof(Int16) ) return GetCastOpToS16( sourceType ); | |
else if( destType == typeof(UInt16) ) return GetCastOpToU16( sourceType ); | |
else if( destType == typeof(Int32) ) return GetCastOpToS32( sourceType ); | |
else if( destType == typeof(UInt32) ) return GetCastOpToU32( sourceType ); | |
else if( destType == typeof(Int64) ) return GetCastOpTo64 ( sourceType ); | |
else if( destType == typeof(UInt64) ) return GetCastOpTo64 ( sourceType ); | |
else throw new ArgumentOutOfRangeException( paramName: nameof(sourceType), actualValue: destType, message: "Unsupported destination enum type." ); | |
} | |
private static OpCode? GetCastOpToS8( Type sourceType ) | |
{ | |
if ( sourceType == typeof(SByte) ) return null; | |
else if( sourceType == typeof(Byte) ) return OpCodes.Conv_I1; | |
else if( sourceType == typeof(Int16) ) return OpCodes.Conv_I1; | |
else if( sourceType == typeof(UInt16) ) return OpCodes.Conv_I1; | |
else if( sourceType == typeof(Int32) ) return OpCodes.Conv_I1; | |
else if( sourceType == typeof(UInt32) ) return OpCodes.Conv_I1; | |
else if( sourceType == typeof(Int64) ) return OpCodes.Conv_I1; | |
else if( sourceType == typeof(UInt64) ) return OpCodes.Conv_I1; | |
else throw new ArgumentOutOfRangeException( paramName: nameof(sourceType), actualValue: sourceType, message: "Unsupported source enum type." ); | |
} | |
private static OpCode? GetCastOpToU8( Type sourceType ) | |
{ | |
if ( sourceType == typeof(SByte) ) return OpCodes.Conv_U1; | |
else if( sourceType == typeof(Byte) ) return null; | |
else if( sourceType == typeof(Int16) ) return OpCodes.Conv_U1; | |
else if( sourceType == typeof(UInt16) ) return OpCodes.Conv_U1; | |
else if( sourceType == typeof(Int32) ) return OpCodes.Conv_U1; | |
else if( sourceType == typeof(UInt32) ) return OpCodes.Conv_U1; | |
else if( sourceType == typeof(Int64) ) return OpCodes.Conv_U1; | |
else if( sourceType == typeof(UInt64) ) return OpCodes.Conv_U1; | |
else throw new ArgumentOutOfRangeException( paramName: nameof(sourceType), actualValue: sourceType, message: "Unsupported source enum type." ); | |
} | |
private static OpCode? GetCastOpToS16( Type sourceType ) | |
{ | |
if ( sourceType == typeof(SByte) ) return null; | |
else if( sourceType == typeof(Byte) ) return null; | |
else if( sourceType == typeof(Int16) ) return null; | |
else if( sourceType == typeof(UInt16) ) return OpCodes.Conv_I2; | |
else if( sourceType == typeof(Int32) ) return OpCodes.Conv_I2; | |
else if( sourceType == typeof(UInt32) ) return OpCodes.Conv_I2; | |
else if( sourceType == typeof(Int64) ) return OpCodes.Conv_I2; | |
else if( sourceType == typeof(UInt64) ) return OpCodes.Conv_I2; | |
else throw new ArgumentOutOfRangeException( paramName: nameof(sourceType), actualValue: sourceType, message: "Unsupported source enum type." ); | |
} | |
private static OpCode? GetCastOpToU16( Type sourceType ) | |
{ | |
if ( sourceType == typeof(SByte) ) return OpCodes.Conv_U2; | |
else if( sourceType == typeof(Byte) ) return null; | |
else if( sourceType == typeof(Int16) ) return OpCodes.Conv_U2; | |
else if( sourceType == typeof(UInt16) ) return null; | |
else if( sourceType == typeof(Int32) ) return OpCodes.Conv_U2; | |
else if( sourceType == typeof(UInt32) ) return OpCodes.Conv_U2; | |
else if( sourceType == typeof(Int64) ) return OpCodes.Conv_U2; | |
else if( sourceType == typeof(UInt64) ) return OpCodes.Conv_U2; | |
else throw new ArgumentOutOfRangeException( paramName: nameof(sourceType), actualValue: sourceType, message: "Unsupported source enum type." ); | |
} | |
private static OpCode? GetCastOpToS32( Type sourceType ) | |
{ | |
if ( sourceType == typeof(SByte) ) return null; | |
else if( sourceType == typeof(Byte) ) return null; | |
else if( sourceType == typeof(Int16) ) return null; | |
else if( sourceType == typeof(UInt16) ) return null; | |
else if( sourceType == typeof(Int32) ) return null; | |
else if( sourceType == typeof(UInt32) ) return null; | |
else if( sourceType == typeof(Int64) ) return OpCodes.Conv_I4; | |
else if( sourceType == typeof(UInt64) ) return OpCodes.Conv_I4; | |
else throw new ArgumentOutOfRangeException( paramName: nameof(sourceType), actualValue: sourceType, message: "Unsupported source enum type." ); | |
} | |
private static OpCode? GetCastOpToU32( Type sourceType ) | |
{ | |
if ( sourceType == typeof(SByte) ) return null; | |
else if( sourceType == typeof(Byte) ) return null; | |
else if( sourceType == typeof(Int16) ) return null; | |
else if( sourceType == typeof(UInt16) ) return null; | |
else if( sourceType == typeof(Int32) ) return null; | |
else if( sourceType == typeof(UInt32) ) return null; | |
else if( sourceType == typeof(Int64) ) return OpCodes.Conv_U4; | |
else if( sourceType == typeof(UInt64) ) return OpCodes.Conv_U4; | |
else throw new ArgumentOutOfRangeException( paramName: nameof(sourceType), actualValue: sourceType, message: "Unsupported source enum type." ); | |
} | |
// Casting to Int64 and UInt64 enums from other enums of these underlying types has identical opcodes - weird. | |
private static OpCode? GetCastOpTo64( Type sourceType ) | |
{ | |
if ( sourceType == typeof(SByte) ) return OpCodes.Conv_I8; | |
else if( sourceType == typeof(Byte) ) return OpCodes.Conv_U8; // That's interesting - why U8 here but I8 above? | |
else if( sourceType == typeof(Int16) ) return OpCodes.Conv_I8; | |
else if( sourceType == typeof(UInt16) ) return OpCodes.Conv_U8; | |
else if( sourceType == typeof(Int32) ) return OpCodes.Conv_I8; | |
else if( sourceType == typeof(UInt32) ) return OpCodes.Conv_U8; | |
else if( sourceType == typeof(Int64) ) return null; | |
else if( sourceType == typeof(UInt64) ) return null; | |
else throw new ArgumentOutOfRangeException( paramName: nameof(sourceType), actualValue: sourceType, message: "Unsupported source enum type." ); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment