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

时间:2009-07-27 16:18:55

标签: c# .net enums boxing

给定一个通用参数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
}

Enum转换已在SO上得到广泛处理,但我无法找到解决此特定案例的讨论。

9 个答案:

答案 0 :(得分:44)

这类似于此处发布的答案,但使用表达式树来发出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. intint

      

    对象投射 - &gt; 42毫秒
      caster1 - &gt; 102毫秒
      caster2 - &gt; 102毫秒
      caster3 - &gt; 90毫秒
      caster4 - &gt; 101毫秒

  2. intint?

      

    对象投射 - &gt; 651毫秒
      caster1 - &gt;失败
      caster2 - &gt;失败
      caster3 - &gt; 109毫秒
      caster4 - &gt;失败

  3. int?int

      

    对象投射 - &gt; 1957 ms
      caster1 - &gt;失败
      caster2 - &gt;失败
      caster3 - &gt; 124毫秒
      caster4 - &gt;失败

  4. enumint

      

    对象投射 - &gt; 405毫秒
      caster1 - &gt;失败
      caster2 - &gt; 102毫秒
      caster3 - &gt; 78毫秒
      caster4 - &gt;失败

  5. intenum

      

    对象投射 - &gt; 370毫秒
      caster1 - &gt;失败
      caster2 - &gt; 93毫秒
      caster3 - &gt; 87毫秒
      caster4 - &gt;失败

  6. int?enum

      

    对象投射 - &gt; 2340毫秒   caster1 - &gt;失败
      caster2 - &gt;失败
      caster3 - &gt; 258毫秒
      caster4 - &gt;失败

  7. enum?int

      

    对象投射 - &gt; 2776毫秒   caster1 - &gt;失败
      caster2 - &gt;失败
      caster3 - &gt; 131毫秒
      caster4 - &gt;失败


  8. Expression.Convert将源类型的直接强制转换为目标类型,因此它可以计算显式和隐式强制转换(更不用说引用强制转换)。因此,这为处理转换提供了方法,否则只有在非盒装的情况下才可行(例如,如果你(TTarget)(object)(TSource),如果它不是标识转换(如前一节)或参考转换那么它将会爆炸(如后面部分所示))。所以我会把它们包括在测试中。

    非盒装演员:

    1. intdouble

        

      对象投射 - &gt;失败
        caster1 - &gt;失败
        caster2 - &gt;失败
        caster3 - &gt; 109毫秒
        caster4 - &gt; 118毫秒

    2. enumint?

        

      对象投射 - &gt;失败
        caster1 - &gt;失败
        caster2 - &gt;失败
        caster3 - &gt; 93毫秒
        caster4 - &gt;失败

    3. intenum?

        

      对象投射 - &gt;失败
        caster1 - &gt;失败
        caster2 - &gt;失败
        caster3 - &gt; 93毫秒
        caster4 - &gt;失败

    4. enum?int?

        

      对象投射 - &gt;失败
        caster1 - &gt;失败
        caster2 - &gt;失败
        caster3 - &gt; 121毫秒
        caster4 - &gt;失败

    5. int?enum?

        

      对象投射 - &gt;失败
        caster1 - &gt;失败
        caster2 - &gt;失败
        caster3 - &gt; 120毫秒
        caster4 - &gt;失败

    6. 为了好玩,我测试了几个参考类型转换:

      1. PrintStringPropertystring(代表更改)

          

        对象投射 - &gt;失败(很明显,因为它没有回归到原始类型)
          caster1 - &gt;失败
          caster2 - &gt;失败
          caster3 - &gt; 315毫秒
          caster4 - &gt;失败

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

          

        对象投射 - &gt; 78毫秒
          caster1 - &gt;失败
          caster2 - &gt;失败
          caster3 - &gt; 322毫秒
          caster4 - &gt;失败

      3. 像这样测试:

        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>类的真正优势在于它允许非通用的转换,例如通用上下文中的(int)double。因此(int)(object)double在这些情况下失败。

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

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

答案 1 :(得分:31)

我知道我迟到了,但如果您只是需要像这样安全演员,可以使用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;

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

答案 2 :(得分:17)

我不确定在没有使用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对原始问题的评论 - 他说抖动目前没有执行此优化。

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

答案 3 :(得分:4)

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

但只是为了扩展上一篇文章(Michael B),它完成了所有有趣的工作

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

...并优化了一下...... (注意:重点是在Func&lt;&gt; / 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 :(得分:3)

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

答案 5 :(得分:2)

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

    modified:   gradle/wrapper/gradle-wrapper.jar
    modified:   gradle/wrapper/gradle-wrapper.properties
    modified:   gradlew
    modified:   gradlew.bat

限制:
这适用于Mono。 (例如Unity3D)

有关Unity3D的更多信息:
ErikE的CastTo课程是解决这个问题的一种非常巧妙的方法 但它不能在Unity3D中使用

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

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

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

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

答案 6 :(得分:0)

这是使用C#7.3的非托管通用类型约束的非常简单的解决方案:

using System;
public static class EnumExtensions<TEnum> where TEnum : unmanaged, Enum
{
    /// <summary>
    /// Will fail if <see cref="TResult"/>'s type is smaller than <see cref="TEnum"/>'s underlying type
    /// </summary>
    public static TResult To<TResult>( TEnum value ) where TResult : unmanaged
    {
        unsafe
        {
            TResult outVal = default;
            Buffer.MemoryCopy( &value, &outVal, sizeof(TResult), sizeof(TEnum) );
            return outVal;
        }
    }

    public static TEnum From<TSource>( TSource value ) where TSource : unmanaged
    {
        unsafe
        {
            TEnum outVal = default;
            long size = sizeof(TEnum) < sizeof(TSource) ? sizeof(TEnum) : sizeof(TSource);
            Buffer.MemoryCopy( &value, &outVal, sizeof(TEnum), size );
            return outVal;
        }
    }
}

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

用法:

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

答案 7 :(得分: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倍。

答案 8 :(得分:-1)

我希望我不会太晚......

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

如果您将使用这种方法,您将拥有一个&#34;感觉&#34;就像一个Enum但你会拥有一个类的所有灵活性,这意味着你可以覆盖任何运算符。

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

我无法找到任何理由不接受这种方法(这个类将位于堆中而不是堆栈上,这个速度较慢,但​​它值得)

请告诉我你的想法。