题 C#非通用枚举的非拳击转换为int?


给定一个通用参数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上得到广泛处理,但我无法找到解决此特定情况的讨论。


51
2017-07-27 16:18


起源


您是否以IL为例看过IL?它有没有拳击? - Drew Noakes
我编辑了你的问题以证明你的猜测确实是正确的 - 拳击确实发生了。 - Drew Noakes
我通过Reflector确认了装箱/拆箱。在看到你这样做之前我也编辑了这个问题。我很抱歉要覆盖你的编辑。 - Jeff Sharp
没问题。对不起,我不能给你一个答案,但看了几分钟这个问题后,我不确定你想要什么是可能的C#。 - Drew Noakes
不幸的是,抖动并没有变得非常安全,随后立即通过unbox变成无操作。我们有几种情况,C#编译器被迫生成这样的代码以使验证者满意;这只是其中之一。抖动团队意识到了这个问题,我希望未来版本的抖动可以很好地优化这种模式。 - Eric Lippert


答案:


我不确定在没有使用Reflection.Emit的情况下C#中是否可行。如果使用Reflection.Emit,则可以将枚举的值加载到堆栈中,然后将其视为int。

你必须编写相当多的代码,所以你要检查一下你是否真的会在这方面获得任何性能。

我相信等效的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位整数。)

编辑

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

另外,不要忘记JIT并不笨,可能会为您解决这个问题。 (编辑  请参阅Eric Lippert对原始问题的评论 - 他说抖动目前没有执行此优化。

与所有与绩效相关的问题:衡量,衡量,衡量!


17
2017-07-27 16:36



非常感谢。我最终会看到这样做。现在我将把拳击转换留在原地。 - Jeff Sharp
一个带类型的函数 Int32 可以绑定到采用解析为的枚举类型的委托 Int32 (对于其他数字类型也是如此)。如果有一堆重载函数将Byte,SByte,Int16,UInt16等转换为 Int64,人们可以用反射来绑定一个 Func<T,Int64> 第一次尝试转换时,其中一个功能 T 到了 Int64,然后每次使用缓存的委托。比拳击快得多(a HasAny<T> 扩展方法的速度约为30倍 Enum.HasFlag(Enum) 扩展方法) - supercat


这类似于此处发布的答案,但使用表达式树来发出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毫秒
      caster1 - > 102 ms
      caster2 - > 102 ms
      caster3 - > 90 ms
      caster4 - > 101 ms

  2. int 至 int?

    对象转换 - > 651 ms
      caster1 - >失败
      caster2 - >失败
      caster3 - > 109毫秒
      caster4 - >失败

  3. int? 至 int

    对象强制转换 - > 1957 ms
      caster1 - >失败
      caster2 - >失败
      caster3 - > 124 ms
      caster4 - >失败

  4. enum 至 int

    对象转换 - > 405毫秒
      caster1 - >失败
      caster2 - > 102 ms
      caster3 - > 78 ms
      caster4 - >失败

  5. int 至 enum

    对象转换 - > 370毫秒
      caster1 - >失败
      caster2 - > 93 ms
      caster3 - > 87 ms
      caster4 - >失败

  6. int? 至 enum

    对象强制转换 - > 2340 ms
      caster1 - >失败
      caster2 - >失败
      caster3 - > 258 ms
      caster4 - >失败

  7. enum? 至 int

    对象转换 - > 2776毫秒
      caster1 - >失败
      caster2 - >失败
      caster3 - > 131 ms
      caster4 - >失败


Expression.Convert 将源类型的直接强制转换为目标类型,因此它可以计算显式和隐式强制转换(更不用说参考强制转换)。因此,这为处理铸造提供了方法,否则只有在非盒装时才能进行铸造(例如,如果你这样做,则采用通用方法) (TTarget)(object)(TSource)如果它不是身份转换(如上一节)或引用转换(如后面部分所示),它将会爆炸)。所以我会将它们包含在测试中。

非盒装演员表: 

  1. int 至 double

    对象转换 - >失败
      caster1 - >失败
      caster2 - >失败
      caster3 - > 109毫秒
      caster4 - > 118 ms

  2. enum 至 int?

    对象转换 - >失败
      caster1 - >失败
      caster2 - >失败
      caster3 - > 93 ms
      caster4 - >失败

  3. int 至 enum?

    对象转换 - >失败
      caster1 - >失败
      caster2 - >失败
      caster3 - > 93 ms
      caster4 - >失败

  4. enum? 至 int?

    对象转换 - >失败
      caster1 - >失败
      caster2 - >失败
      caster3 - > 121 ms
      caster4 - >失败

  5. int? 至 enum?

    对象转换 - >失败
      caster1 - >失败
      caster2 - >失败
      caster3 - > 120 ms
      caster4 - >失败

为了它的乐趣,我测试了一个 几个参考类型转换:

  1. PrintStringProperty 至 string (代表性变化)

    对象转换 - >失败(非常明显,因为它不会被转换回原始类型)
      caster1 - >失败
      caster2 - >失败
      caster3 - > 315 ms
      caster4 - >失败

  2. string 至 object (表示保留参考转换)

    对象转换 - > 78毫秒
      caster1 - >失败
      caster2 - >失败
      caster3 - > 322 ms
      caster4 - >失败

像这样测试:

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. 我的估计是,除非你运行至少十万次,否则它不值得,你几乎没有什么可担心拳击。请注意,缓存代表会对内存产生影响。但超出这个限制, 速度的提升是显着的,特别是涉及到涉及nullables的铸造

  2. 但真正的优势 CastTo<T> class是什么时候它允许可能非盒装的强制转换,比如 (int)double 在一般情况下。因此 (int)(object)double 在这些情况下失败。

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

  4. 如果不存在演员表 TSource 至 TTarget,编译委托时抛出异常。如果您想要一个不同的行为,比如获取默认值 TTarget,您可以在编译委托之前使用反射检查类型兼容性。您可以完全控制生成的代码。它会变得非常棘手,你必须检查参考兼容性(IsSubClassOfIsAssignableFrom),转换运算符存在(将成为hacky),甚至对于原始类型之间的某些内置类型可转换性。会非常hacky。更容易捕获异常并返回基于的默认值委托 ConstantExpression。只是说明你可以模仿行为的可能性 as 不扔的关键字。最好远离它并坚持惯例。


38
2018-04-30 15:09



+1,我喜欢这种方法。该 CreateDelegate 对我来说似乎是个黑客。事实上,在单声道, CreateDelegate 接近失败,这一个继续工作。 - Erti-Chris Eelmaa
哦,我的上帝。你太棒了。 - AgentFire
你是我的英雄。谢谢。 - Pavel Tupitsyn
这应该是公认的答案。 - 0b101010
编辑有什么意义? - nawfal


我知道我迟到了,但是如果你只是需要像这样安全演员,你可以使用以下方法 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。不是相反。在.Net 3.5+中 EnumEqualityComparer优化基本上把它变成一个回报 (int)value;

你正在支付使用委托的开销,但它肯定会比拳击更好。


30
2017-10-26 18:10



在我的客户端代码库中再次出现这种情况。这一次,我最终使用了这个解决方案。谢谢! - Jeff Sharp
很好,谢谢! - Lea Hayes


......我甚至'后来':)

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

并让我有兴趣为一般案例制作一个包装器(如果你想实际上将通用转换为枚举)

...并优化了一下...... (注意:重点是在Func <> / delegates上使用'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


4
2018-03-02 19:35



这似乎不起作用。 Func<TEnum, TEnum> 无法投入任何一个 Func<T, TEnum> 或者a Func<Enum, T>。结果 as 运营商是 null。如果您使用传统的强制转换,编译器会抱怨。 - Jeff Sharp
杰夫,是的,这是不允许的,正如我所提到的那样(请阅读'注释'abovve) - 你需要使用'身份',而不是扮演代表 - 就像我做的那样(FamilyRelation rel = Identity<FamilyRelation, P>.Cast(value);)。该示例“按原样”工作(编译和测试,我在几个项目中使用它,还没有尝试过新的.NET / C#/ VS)。请注意,这不是您的确切答案,而是“详细说明”。它有希望对某人有用。而我正在使用拳击但不相关,不是为了枚举。您可能需要插件,制定具体细节。 - NSGaga
...因为你已经在委托代表 - 你可能试图插入'int'并一起枚举 - 这是行不通的。这是为了将你的'开放'枚举类型降低到真实/封闭的枚举 - 这在大多数情况下更有用。然后你可以施展 (int)Identity<FamilyRelation, P>.Cast(value)。但是,如果你有一个'未知'的枚举类型(即你想要将许多不同的枚举混合并序列化为一个'int',例如db字段),它将无济于事,尽管这种情况很少(设计考虑因素)但在某些情况下似乎是合理的。 - NSGaga
一个小建议,不会宣布它 Identity<T, TEnum> 更有意义吗? - nawfal
嗯,你可以使用Cast的存在来测试可铸性: if(Identity<T,P>.Cast!=null) return Identity<T,P>.Cast(value); - Rbjz


我猜你总是可以使用System.Reflection.Emit来创建一个动态方法并发出执行此操作而无需装箱的指令,尽管它可能无法验证。


3
2017-07-27 16:33



捕捉。顺便说一下,您可以使用Reflection.Emit为此方案创建可验证的IL。 - Drew Noakes


这是一种最简单,最快捷的方式。
(有一点限制。:-))

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中使用

首先,它必须如下修复。
(因为单声道编译器无法编译原始代码)

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);
    }
}

其次,ErikE的代码不能在AOT平台上使用。
所以,我的代码是Mono的最佳解决方案。

评论'Kristof':
对不起,我没有写完所有细节。


2
2017-07-27 07:10



有趣的方法,但它是否真的是我不知道的最快。我在使用时看到了有害的性能曲线 LayoutKind.Explicit 强迫这种黑客攻击。我对你展示一些与这个帖子中的替代品相比的时间感兴趣。 - Abel
不起作用。 x中发生未处理的类型'System.TypeLoadException'的异常附加信息:无法从程序集x,版本= 1.0.0.0,Culture = neutral,PublicKeyToken = null'加载类型'EnumUnion32`1',因为 泛型类型不能具有显式布局。 - Kristof


我希望我不会太迟......

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

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

还有其他一些优点,例如使该类成为部分,这使您能够在多个文件/ dll中定义相同的枚举,从而可以在不重新编译的情况下将值添加到公共dll。

我找不到任何理由不采取这种方法(这个类将位于堆中而不是堆栈上,这是慢的,但它是值得的)

请让我知道你在想什么。


-1
2018-05-19 07:25



序列化是这种方法的一个问题。 - Timbo
另一个是无法对输出进行位屏蔽。 - Alxwest