泛型枚举到整型的 C # 非装箱转换?

给定一个总是为枚举类型的泛型参数 TEnum,有没有办法在不装箱/取消装箱的情况下将 TEnum 强制转换为 int?

查看这个示例代码。这将不必要地打开/取消打开值。

private int Foo<TEnum>(TEnum value)
where TEnum : struct  // C# does not allow enum constraint
{
return (int) (ValueType) value;
}

上面的 C # 是编译成以下 IL (注释装箱和取消装箱操作码)的发布模式:

.method public hidebysig instance int32  Foo<valuetype
.ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
.maxstack  8
IL_0000:  ldarg.1
IL_0001:  box        !!TEnum
IL_0006:  unbox.any  [mscorlib]System.Int32
IL_000b:  ret
}

枚举转换已经在 SO 上得到了广泛的处理,但是我找不到针对这个特定情况的讨论。

24528 次浏览

我想你可以一直使用系统。反射。发出以创建动态方法,并发出不装箱的指令,尽管这可能是无法验证的。

如果不使用反射,我不确定这在 C # 中是否可行。发射。如果你使用反射。发出,您可以将枚举的值加载到堆栈上,然后将其视为整型。

但是,您必须编写相当多的代码,因此需要检查在执行此操作时是否真正获得了任何性能。

我相信相应的 IL 将是:

.method public hidebysig instance int32  Foo<valuetype
.ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
.maxstack  8
IL_0000:  ldarg.1
IL_000b:  ret
}

请注意,如果您的枚举是从 long(一个64位整数)派生的,则此操作将失败

剪辑

关于这个方法的另一个想法。反射。Emit 可以创建上面的方法,但是绑定到它的唯一方法是通过一个虚拟调用(即它实现了一个可以调用的编译时已知的接口/抽象)或者一个间接调用(即通过一个委托调用)。我想这两种情况都比装箱/拆箱的开销要慢。

另外,不要忘记 JIT 并不笨,它可以为你解决这个问题。(剪辑 看看埃里克 · 利伯特对最初问题的评论——他说,抖动目前并没有执行这种优化。)

与所有与性能相关的问题一样: 度量、度量、度量!

我知道我来晚了,但是如果你只是需要这样一个安全的铸造,你可以使用以下 Delegate.CreateDelegate:

public static int Identity(int x){return x;}
// later on..
Func<int,int> identity = Identity;
Delegate.CreateDelegate(typeof(Func<int,TEnum>),identity.Method) as Func<int,TEnum>

现在不需要编写 Reflection.Emit或表达式树,您就有了一个方法,可以在不装箱或解箱的情况下将 int 转换为枚举。请注意,这里的 TEnum必须有一个基础类型 int,否则将抛出一个异常,说明它不能被绑定。

编辑: 另一种方法也可以,而且可能写得少一点..。

Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;

这样可以将32位 或者更少枚举从 TEnum 转换为 int。而不是反过来。进去。净3.5 + ,EnumEqualityComparer的优化,基本上把这个返回 (int)value;

您正在支付使用委托的开销,但它肯定比装箱好。

这是相当古老的,但如果你仍然回到这里寻找一个解决方案,工程。净额5/。Net 核心(或带有不安全软件包的 netfx) ,并保持最优..。

[JitGeneric(typeof(StringComparison), typeof(int))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryConvert<TEnum, T>(this TEnum @enum, out T val)
where TEnum : struct, Enum
where T : struct, IConvertible, IFormattable, IComparable
{
if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<TEnum>())
{
val = Unsafe.As<TEnum, T>(ref @enum);
return true;
}
val = default;
return false;
}

一个例子的用法可能是这样的:

public static int M(MethodImplOptions flags) => flags.TryConvert(out int v) ? v : 0;

在 sharplab 上我们可以看到,这个方法被完全内联了: Https://sharplab.io/#gist:802b8d21ee1de26e791294ba48f69d97

... 我甚至“晚了”:)

但是只是延伸到上一篇文章(迈克尔 B) ,它做了所有有趣的工作

并使我对为通用情况制作包装器感兴趣(如果您想实际将通用转换为枚举)

优化了一点。 (注意: 主要是在 Func < >/committee 上使用‘ as’-作为 Enum,值类型不允许这样做)

public static class Identity<TEnum, T>
{
public static readonly Func<T, TEnum> Cast = (Func<TEnum, TEnum>)((x) => x) as Func<T, TEnum>;
}

你可以像这样使用它。

enum FamilyRelation { None, Father, Mother, Brother, Sister, };
class FamilyMember
{
public FamilyRelation Relation { get; set; }
public FamilyMember(FamilyRelation relation)
{
this.Relation = relation;
}
}
class Program
{
static void Main(string[] args)
{
FamilyMember member = Create<FamilyMember, FamilyRelation>(FamilyRelation.Sister);
}
static T Create<T, P>(P value)
{
if (typeof(T).Equals(typeof(FamilyMember)) && typeof(P).Equals(typeof(FamilyRelation)))
{
FamilyRelation rel = Identity<FamilyRelation, P>.Cast(value);
return (T)(object)new FamilyMember(rel);
}
throw new NotImplementedException();
}
}

... for (int)-just (int) rel

这与此处发布的答案类似,但是使用表达式树发出 il 以在类型之间进行强制转换。Expression.Convert就行了。已编译的委托(caster)由内部静态类缓存。因为源对象可以从参数中推断出来,所以我猜它提供了更干净的调用。例如,通用上下文:

static int Generic<T>(T t)
{
int variable = -1;


// may be a type check - if(...
variable = CastTo<int>.From(t);


return variable;
}

课程:

/// <summary>
/// Class to cast to type <see cref="T"/>
/// </summary>
/// <typeparam name="T">Target type</typeparam>
public static class CastTo<T>
{
/// <summary>
/// Casts <see cref="S"/> to <see cref="T"/>.
/// This does not cause boxing for value types.
/// Useful in generic methods.
/// </summary>
/// <typeparam name="S">Source type to cast from. Usually a generic type.</typeparam>
public static T From<S>(S s)
{
return Cache<S>.caster(s);
}


private static class Cache<S>
{
public static readonly Func<S, T> caster = Get();


private static Func<S, T> Get()
{
var p = Expression.Parameter(typeof(S));
var c = Expression.ConvertChecked(p, typeof(T));
return Expression.Lambda<Func<S, T>>(c, p).Compile();
}
}
}

你可以用其他的实现来替换 caster的 func。我将比较一些实现的性能:

direct object casting, ie, (T)(object)S


caster1 = (Func<T, T>)(x => x) as Func<S, T>;


caster2 = Delegate.CreateDelegate(typeof(Func<S, T>), ((Func<T, T>)(x => x)).Method) as Func<S, T>;


caster3 = my implementation above


caster4 = EmitConverter();
static Func<S, T> EmitConverter()
{
var method = new DynamicMethod(string.Empty, typeof(T), new[] { typeof(S) });
var il = method.GetILGenerator();


il.Emit(OpCodes.Ldarg_0);
if (typeof(S) != typeof(T))
{
il.Emit(OpCodes.Conv_R8);
}
il.Emit(OpCodes.Ret);


return (Func<S, T>)method.CreateDelegate(typeof(Func<S, T>));
}

盒装石膏 :

  1. int呼叫 int

    物体铸造-> 42毫秒
    连铸机1-> 102毫秒
    连铸机2-> 102毫秒
    连铸机3-> 90毫秒
    连铸机4-> 101毫秒

  2. int呼叫 int?

    物体铸造-> 651毫秒
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 109毫秒
    卡斯特4-> 失败

  3. int?呼叫 int

    物体铸造-> 1957毫秒
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 124毫秒
    卡斯特4-> 失败

  4. enum呼叫 int

    物体铸造-> 405毫秒
    卡斯特1-> 失败
    连铸机2-> 102毫秒
    连铸机3-> 78毫秒
    卡斯特4-> 失败

  5. int呼叫 enum

    物体铸造-> 370毫秒
    卡斯特1-> 失败
    Caster2-> 93 ms
    连铸机3-> 87毫秒
    卡斯特4-> 失败

  6. int?呼叫 enum

    物体铸造-> 2340毫秒
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 258毫秒
    卡斯特4-> 失败

  7. enum?呼叫 int

    物体铸造-> 2776毫秒
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 131毫秒
    卡斯特4-> 失败


Expression.Convert将源类型直接转换为目标类型,因此它可以进行显式和隐式转换(更不用说引用转换)。因此,这给处理铸造的方式,否则是可能的,只有当非盒子(即,在一个通用的方法,如果你做 (TTarget)(object)(TSource),它将爆炸,如果它不是身份转换(如在前一节)或引用转换(如在后一节所示))。所以我会把它们纳入测试。

非盒装石膏:

  1. int呼叫 double

    对象强制转换-> 失败
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 109毫秒
    连铸机4-> 118毫秒

  2. enum呼叫 int?

    对象强制转换-> 失败
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 93毫秒
    卡斯特4-> 失败

  3. int呼叫 enum?

    对象强制转换-> 失败
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 93毫秒
    卡斯特4-> 失败

  4. enum?呼叫 int?

    对象强制转换-> 失败
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 121毫秒
    卡斯特4-> 失败

  5. int?呼叫 enum?

    对象强制转换-> 失败
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 120毫秒
    卡斯特4-> 失败

为了好玩,我测试了 少量引用类型转换:

  1. PrintStringPropertystring(表示变化)

    对象强制转换-> 失败(非常明显,因为它没有被强制转换回原始类型)
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 315毫秒
    卡斯特4-> 失败

  2. stringobject(保持表示的引用转换)

    物体铸造-> 78毫秒
    卡斯特1-> 失败
    卡斯特2-> 失败
    连铸机3-> 322毫秒
    卡斯特4-> 失败

像这样测试:

static void TestMethod<T>(T t)
{
CastTo<int>.From(t); //computes delegate once and stored in a static variable


int value = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++)
{
value = (int)(object)t;


// similarly value = CastTo<int>.From(t);


// etc
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}

注:

  1. 我的估计是,除非你运行这至少10万次,这是不值得的,你几乎没有什么可担心的拳击。请注意,缓存委托对内存有影响。但是超过这个限制,速度的提高是显著的,特别是当涉及铸造零件

  2. 但是,CastTo<T>类的真正优势在于它允许非装箱的强制转换,比如泛型上下文中的 (int)double。因此,(int)(object)double在这些场景中会失败。

  3. 我使用了 Expression.ConvertChecked而不是 Expression.Convert,以便检查算术溢出和下溢(即异常结果)。由于 il 是在运行时生成的,并且检查设置是在编译时进行的,因此您无法知道调用代码的检查上下文。这件事你得自己决定。选择一个,或者为两者提供过载(更好)。

  4. 如果从 TSourceTTarget不存在强制转换,则在编译委托时引发异常。如果需要不同的行为,比如获取 TTarget的默认值,可以在编译委托之前使用反射检查类型兼容性。您可以完全控制正在生成的代码。这将是非常棘手的,但是,您必须检查引用兼容性(IsSubClassOfIsAssignableFrom) ,转换操作符的存在(将是 hacky) ,甚至为一些内置的类型转换之间的基本类型。会非常恶心。基于 ConstantExpression捕获异常并返回默认值委托更容易。只是说明一种可能性,你可以模仿行为的 as关键字不抛出。最好远离它,坚持传统。

我希望我还来得及。

我认为您应该考虑用一种不同的方法来解决问题,而不是使用 Enums 尝试创建一个具有公共静态只读属性的类。

如果您使用这种方法,您将拥有一个“感觉上”像 Enum 的对象,但是您将拥有类的所有灵活性,这意味着您可以覆盖任何操作符。

还有一些其他的好处,比如使该类成为一个局部类,这将使您能够在多个文件/dll 中定义相同的枚举,这使得向一个公共 dll 添加值而不需要重新编译它成为可能。

我找不到任何不采用这种方法的好理由(这个类将位于堆中,而不是堆栈中,虽然堆栈速度较慢,但是值得一试)

请让我知道你的想法。

这里有一个最简单、最快捷的方法。
(有一点限制: ——)

public class BitConvert
{
[StructLayout(LayoutKind.Explicit)]
struct EnumUnion32<T> where T : struct {
[FieldOffset(0)]
public T Enum;


[FieldOffset(0)]
public int Int;
}


public static int Enum32ToInt<T>(T e) where T : struct {
var u = default(EnumUnion32<T>);
u.Enum = e;
return u.Int;
}


public static T IntToEnum32<T>(int value) where T : struct {
var u = default(EnumUnion32<T>);
u.Int = value;
return u.Enum;
}
}

限制:
这在 Mono 中可以工作。(如 Unity3D)

更多关于 Unity3D 的信息:
ErikE 的 CastTo 类是解决这个问题的一个非常简洁的方法。
但是它不能像 Unity3D 那样使用

首先,它必须像下面这样固定。
(因为 mono 编译器无法编译原始代码)

public class CastTo {
protected static class Cache<TTo, TFrom> {
public static readonly Func<TFrom, TTo> Caster = Get();


static Func<TFrom, TTo> Get() {
var p = Expression.Parameter(typeof(TFrom), "from");
var c = Expression.ConvertChecked(p, typeof(TTo));
return Expression.Lambda<Func<TFrom, TTo>>(c, p).Compile();
}
}
}


public class ValueCastTo<TTo> : ValueCastTo {
public static TTo From<TFrom>(TFrom from) {
return Cache<TTo, TFrom>.Caster(from);
}
}

其次,艾瑞克的代码不能用于 AOT 平台。
因此,我的代码是 Mono 的最佳解决方案。

评论“ Kristof”:
很抱歉我没有写下所有的细节。

下面是一个非常直接的解决方案,它使用了 C # 7.3的非托管泛型类型约束:

    using System;
public static class EnumExtensions<TEnum> where TEnum : unmanaged, Enum
{
/// <summary>
/// Converts a <typeparam name="TEnum"></typeparam> into a <typeparam name="TResult"></typeparam>
/// through pointer cast.
/// Does not throw if the sizes don't match, clips to smallest data-type instead.
/// So if <typeparam name="TResult"></typeparam> is smaller than <typeparam name="TEnum"></typeparam>
/// bits that cannot be captured within <typeparam name="TResult"></typeparam>'s size will be clipped.
/// </summary>
public static TResult To<TResult>( TEnum value ) where TResult : unmanaged
{
unsafe
{
if( sizeof(TResult) > sizeof(TEnum) )
{
// We might be spilling in the stack by taking more bytes than value provides,
// alloc the largest data-type and 'cast' that instead.
TResult o = default;
*((TEnum*) & o) = value;
return o;
}
else
{
return * (TResult*) & value;
}
}
}


/// <summary>
/// Converts a <typeparam name="TSource"></typeparam> into a <typeparam name="TEnum"></typeparam>
/// through pointer cast.
/// Does not throw if the sizes don't match, clips to smallest data-type instead.
/// So if <typeparam name="TEnum"></typeparam> is smaller than <typeparam name="TSource"></typeparam>
/// bits that cannot be captured within <typeparam name="TEnum"></typeparam>'s size will be clipped.
/// </summary>
public static TEnum From<TSource>( TSource value ) where TSource : unmanaged
{
unsafe
{


if( sizeof(TEnum) > sizeof(TSource) )
{
// We might be spilling in the stack by taking more bytes than value provides,
// alloc the largest data-type and 'cast' that instead.
TEnum o = default;
*((TSource*) & o) = value;
return o;
}
else
{
return * (TEnum*) & value;
}
}
}
}


在项目配置中需要不安全的切换。

用法:

int intValue = EnumExtensions<YourEnumType>.To<int>( yourEnumValue );

编辑: 取代 Buffer.MemoryCopy的简单指针从达霍尔的建议。

如果希望加快转换,限制使用不安全的代码,并且不能发出 IL,那么可以考虑将泛型类作为抽象类,并在派生类中实现转换。例如,当您为 Unity 引擎编写代码时,您可能希望构建与发出不兼容的 IL2CPP 目标。以下是如何实施的一个例子:

// Generic scene resolver is abstract and requires
// to implement enum to index conversion
public abstract class SceneResolver<TSceneTypeEnum> : ScriptableObject
where TSceneTypeEnum : Enum
{
protected ScenePicker[] Scenes;


public string GetScenePath ( TSceneTypeEnum sceneType )
{
return Scenes[SceneTypeToIndex( sceneType )].Path;
}


protected abstract int SceneTypeToIndex ( TSceneTypeEnum sceneType );
}


// Here is some enum for non-generic class
public enum SceneType
{
}


// Some non-generic implementation
public class SceneResolver : SceneResolver<SceneType>
{
protected override int SceneTypeToIndex ( SceneType sceneType )
{
return ( int )sceneType;
}
}


我测试了拳击 VS 虚拟方法,并在 macOS 上为 Mono 和 IL2CPP 目标提高了10倍的虚拟方法速度。