Skip to content

Instantly share code, notes, and snippets.

@daiplusplus
Last active November 11, 2019 07:22
Show Gist options
  • Save daiplusplus/aa07ceda23b2ca03184722c3f63ce7ca to your computer and use it in GitHub Desktop.
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.
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 &quot;Proper Subset&quot; and &quot;Strict Subset&quot; 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 &quot;Proper Subset&quot; and &quot;Strict Subset&quot; 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 &quot;Name&quot; 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