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

Jef*_*arp 62 .net c# enums boxing

给定一个通用参数TEnum,它总是一个枚举类型,有没有办法从TEnum转换为int而不用装箱/拆箱?

请参阅此示例代码.这将不必要地装箱/取消装箱.

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

上面的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
}
Run Code Online (Sandbox Code Playgroud)

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

naw*_*fal 52

这类似于此处发布的答案,但使用表达式树来发出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;
}
Run Code Online (Sandbox Code Playgroud)

班级:

/// <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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以将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>));
}
Run Code Online (Sandbox Code Playgroud)

盒装演员:

  1. intint

    对象强制转换 - > 42 ms
    caster1 - > 102 ms
    caster2 - > 102 ms
    caster3 - > 90 ms
    caster4 - > 101 ms

  2. intint?

    对象转换 - > 651 ms
    caster1 - > fail
    caster2 - > fail
    caster3 - > 109 ms
    caster4 - > fail

  3. int?int

    对象转换 - > 1957 ms
    caster1 - > fail
    caster2 - > fail
    caster3 - > 124 ms
    caster4 - > fail

  4. enumint

    对象转换 - > 405 ms
    caster1 - > fail
    caster2 - > 102 ms
    caster3 - > 78 ms
    caster4 - > fail

  5. intenum

    对象转换 - > 370 ms
    caster1 - > fail
    caster2 - > 93 ms
    caster3 - > 87 ms
    caster4 - > fail

  6. int?enum

    对象转换 - > 2340 ms
    caster1 - > fail
    caster2 - > fail
    caster3 - > 258 ms
    caster4 - > fail

  7. enum?int

    对象转换 - > 2776 ms
    caster1 - > fail
    caster2 - > fail
    caster3 - > 131 ms
    caster4 - > fail


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

非盒装演员表:

  1. intdouble

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

  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 - > fail
    caster2 - > fail
    caster3 - > 315 ms
    caster4 - > fail

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

    对象转换 - > 78 ms
    caster1 - > fail
    caster2 - > fail
    caster3 - > 322 ms
    caster4 - > fail

像这样测试:

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);
}
Run Code Online (Sandbox Code Playgroud)

注意:

  1. 我的估计是,除非你运行至少十万次,否则它不值得,你几乎没有什么可担心的拳击.请注意,缓存代表会记住内存.但是超出这个限制,速度提升是显着的,特别是涉及到涉及nullables的铸造.

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

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

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

  • +1,我喜欢这种方法.`CreateDelegate`对我来说似乎是一个黑客攻击.事实上,在单声道中,`CreateDelegate`方法失败了,这个方法继续工作. (3认同)
  • 哦,我的上帝.你太棒了. (3认同)

Mic*_*l B 33

我知道我迟到了,但是如果你只是需要像这样安全演员,你可以使用以下方法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>
Run Code Online (Sandbox Code Playgroud)

现在没有写入Reflection.Emit或表达式树,你有一个方法,可以在没有装箱或拆箱的情况下将int转换为枚举.请注意,TEnum此处必须具有基础类型,int否则将抛出异常,表示无法绑定.

编辑:另一种方法也有效,可能会少写...

Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;
Run Code Online (Sandbox Code Playgroud)

这可以将32位或更少的枚举从TEnum转换为int.不是相反.在.Net 3.5+中,EnumEqualityComparer经过优化,基本上将其转化为回报(int)value;

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

  • 在我客户的代码库中,这种情况再次出现.这一次,我最终使用了这个解决方案.谢谢! (2认同)

Dre*_*kes 18

我不确定在没有使用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
}
Run Code Online (Sandbox Code Playgroud)

请注意,如果您的枚举派生自long(64位整数),则会失败.

编辑

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

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

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