通用枚举到int的C#非装箱转换?


68

给定通用参数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上得到了广泛处理,但我找不到针对此特定情况的讨论。


您是否查看过IL作为示例?有拳击吗?
德鲁·诺阿克斯

1
我编辑了您的问题,以证明您的猜测确实正确-确实发生了拳击。
德鲁·诺阿克斯

我通过Reflector确认了装箱/拆箱。在看到您这样做之前,我还编辑了问题。抱歉,您要覆盖您的编辑。
杰夫·夏普

没问题。抱歉,我无法给您答案,但是在看了几分钟这个问题之后,我不确定C#中您想要什么。
德鲁·诺阿克斯

16
不幸的是,这种抖动并没有将完全安全的紧随其后的拆箱操作变成无操作操作。在几种情况下,C#编译器被迫生成此类代码以使验证程序满意。这只是其中之一。抖动团队已经意识到了这个问题,我希望以后的抖动版本能够很好地优化这种模式。
埃里克·利珀特

Answers:


19

我不确定如果不使用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)对原始问题的评论-他说,抖动目前并未实现这种优化。

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


非常感谢。我将最终考虑这样做。现在,我将拳击转换保留在原位。
杰夫·夏普

Int32可以将采用类型的函数绑定到采用解析为枚举类型的委托Int32(对于其他数字类型也是如此)。如果有人有一堆重载函数将Byte,SByte,Int16,UInt16等转换为an ,则在第一次尝试将其转换为时Int64,可以使用反射将a绑定Func<T,Int64>到这些函数之一。此后每次都使用缓存的委托。比拳击快得多(扩展方法的速度约为扩展方法的30倍)TInt64HasAny<T>Enum.HasFlag(Enum)
supercat 2012年

54

这与此处发布的答案类似,但是使用表达式树发出il以在类型之间进行转换。Expression.Convert绝招。编译的委托(广播程序)由内部静态类缓存。由于可以从参数推断出源对象,因此我猜它可以提供更清晰的调用。例如,通用上下文:

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

您可以将casterfunc替换为其他实现。我将比较一些性能:

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. intint

    对象投射-> 42毫秒
    caster1-> 102毫秒
    caster2-> 102毫秒
    caster3-> 90毫秒
    caster4-> 101毫秒

  2. intint?

    对象投射-> 651毫秒
    Caster1->失败
    Caster2->失败
    Caster3-> 109毫秒
    Caster4->失败

  3. int?int

    对象投射-> 1957 ms
    caster1->失败
    caster2->失败
    caster3-> 124 ms
    caster4->失败

  4. enumint

    对象投射-> 405毫秒
    caster1->失败的
    caster2-> 102毫秒
    caster3-> 78毫秒
    caster4->失败

  5. intenum

    对象投射-> 370毫秒
    caster1->失败
    投射器2-> 93毫秒
    caster3-> 87毫秒
    caster4->失败

  6. int?enum

    对象投射-> 2340毫秒
    Caster1->失败
    Caster2->失败
    Caster3-> 258毫秒
    Caster4->失败

  7. enum?int

    对象投射-> 2776毫秒
    Caster1->失败
    Caster2->失败
    Caster3-> 131毫秒
    Caster4->失败


Expression.Convert将直接类型转换从源类型转换为目标类型,因此它可以计算出显式​​和隐式转换(更不用说参考类型转换)。因此,这为处理转换提供了方法,否则只有在未装箱的情况下才可能进行转换(即,如果您使用(TTarget)(object)(TSource)未进行身份转换(如上一节中所述)或引用转换(如下一节中所示),它将爆炸) ))。因此,我将它们包含在测试中。

非盒装演员表:

  1. intdouble

    对象施法->失败
    施法者1->失败
    施法者2->失败
    施法者3-> 109毫秒
    施法者4-> 118毫秒

  2. enumint?

    对象投射->失败的
    Caster1->失败的
    Caster2->失败的
    Caster3-> 93 ms的
    Caster4->失败

  3. intenum?

    对象投射->失败的
    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. PrintStringPropertystring(表示形式更改)

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

  2. stringobject(表示形式保留参考转换)

    对象投射-> 78毫秒
    Caster1->失败
    Caster2->失败
    Caster3-> 322毫秒
    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. 我的估计是,除非您至少运行十万次,否则这样做是不值得的,并且您几乎不必担心拳击。请注意,您缓存代表对内存的影响很大。但是超出这个限制,速度的提高是非常重要的,特别是涉及可空值的转换时

  2. 但是,CastTo<T>该类的真正优势在于,它允许进行非盒装转换,例如(int)double在通用上下文中。因此(int)(object)double在这些情况下失败。

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

  4. 如果从TSourceto不存在强制转换TTarget,则在编译委托时会引发异常。如果您想要其他行为,例如获得默认值TTarget,则可以在编译委托之前使用反射检查类型兼容性。您可以完全控制所生成的代码。它的将是非常棘手,虽然,你必须检查参考兼容性(IsSubClassOfIsAssignableFrom),转换运营商的存在(将是哈克),甚至对于一些建在原始类型之间的类型可兑换。会变得非常hacky。更容易捕获异常并基于返回默认值委托ConstantExpression。仅说明您可以模仿as不抛出关键字的行为的可能性。最好远离它并坚持惯例。


3
+1,我喜欢这种方法。CreateDelegate对我来说,这似乎是一个hack。实际上,在单声道中,该CreateDelegate方法失败了,这种方法一直有效。
Erti-Chris Eelmaa 2014年


34

我知道我参加晚会很晚,但是如果您只需要进行这样的安全操作,则可以使用以下命令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转换为enum,而无需装箱或拆箱。请注意,TEnum此处必须具有int否则将引发异常,表明无法绑定。

编辑:另一种方法也有效,可能写起来少一些。。。

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

这可以将您的32位或更小的枚举从TEnum转换为int。并非相反。在.Net 3.5+中,EnumEqualityComparer优化后的基本上可以将其转化为回报(int)value

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


2
在我客户的代码库中,这种情况再次出现。这次,我最终使用了此解决方案。谢谢!
杰夫·夏普

4

...我什至“以后” :)

但只是为了扩展前一篇文章(Michael 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();
    }
}

...对于(int)-just(int)rel


1
这似乎不起作用。 Func<TEnum, TEnum>不能强制转换为aFunc<T, TEnum>或a Func<Enum, T>as运算符的结果为null。如果您改用传统的强制类型转换,则编译器将抱怨。
杰夫·夏普

1
杰夫,是的,正如我提到的那样,这是不允许的(请仔细阅读“注释”)-您需要使用“身份”,而是使用委托-完全像我所做的那样(FamilyRelation rel = Identity<FamilyRelation, P>.Cast(value);)。该示例按“原样”运行(经过编译和测试,我正在几个项目中使用它,尚未尝试使用新的.NET / C#/ VS)。请注意,这不是对您Q的确切答案,而是“详尽”。希望对某人有用。我正在使用拳击,但无关,不是为了枚举。您可能需要插件,制定具体细节。
NSGaga-2012年

...因为您已经在使用委托了-您可能正在尝试插入'int'并将其枚举在一起-这是行不通的。这是为了将“开放”枚举类型简化为实数/封闭式枚举-在大多数情况下更有用。然后,您可以进行投射(int)Identity<FamilyRelation, P>.Cast(value)。但是,如果您使用的是“未知”枚举类型(例如,您想将许多不同的枚举混合并序列化为一个“ int”,例如db字段),那将无济于事,尽管这种情况很少见(设计考虑),但在某些情况下是合理的。
NSGaga-几乎未激活,

1
一个小的建议,是否会宣布它Identity<T, TEnum>更有意义?
nawfal 2013年

1
哼,您可以使用Cast的存在来测试可铸性:if(Identity<T,P>.Cast!=null) return Identity<T,P>.Cast(value);
Rbjz

3

我想您总是可以使用System.Reflection.Emit创建一个动态方法,并发出无需装箱的说明,尽管这可能无法验证。


1
快点 顺便说一句,在这种情况下,您可以使用Reflection.Emit创建可验证的IL。
德鲁·诺阿克斯

3

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

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”:
很抱歉,我没有写所有细节。


有趣的方法,但是我不知道它是否真的是最快的。使用LayoutKind.Explicit强制这种骇客行为时,我看到了有害的性能曲线。与您在此线程中的替代方法相比,我希望您展示一些时间安排。
亚伯

不起作用 x中发生了类型为'System.TypeLoadException'的未处理异常。其他信息:无法从程序集x,Version = 1.0.0.0,Culture = neutral,PublicKeyToken = null中加载类型'EnumUnion32`1',因为泛型类型不能具有显式布局
克里斯托夫

2

这是使用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.MemoryCopydahall建议的简单指针替换。


如果您已经拥有或可以拥有不安全的项目,我会喜欢这种方法。在那种情况下,您是否也不能仅使用指针来完成MemoryCopy的工作? return *(TEnum*)(void*)&value;
dahall

@dahall总的来说,我当时对指针并不十分熟悉,后期编辑。
Eideren '19

0

如果您想加快转换速度,只能使用不安全的代码并且不能发出IL,则可以考虑将通用类作为抽象并在派生类中实现转换。例如,当您为Unity引擎编码时,您可能想构建与emit不兼容的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;
    }
}

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


-1

我希望我还不算太晚...

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

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

还有其他优点,例如使该类成为部分类,将使您能够在一个以上的文件/ dll中定义相同的枚举,这使得可以将值添加到通用dll中而无需重新编译它。

我找不到任何不采用这种方法的充分理由(此类将位于堆中,而不是位于堆栈中,这较慢,但这是值得的)

请让我知道你在想什么。


序列化是这种方法的问题。
Timbo

不能对输出进行位屏蔽是另一个问题。
Alxwest的
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.