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

给定一个通用参数TEnum,它总是一个枚举types,有没有什么办法从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的转换已经被广泛的处理,但是我找不到针对这个具体情况的讨论。

我不确定这是不可能在C#中使用Reflection.Emit。 如果使用Reflection.Emit,则可以将枚举的值加载到堆栈上,然后将其视为int。

尽pipe如此,你还是需要编写很多代码,所以你需要检查一下你是否真的会获得任何性能。

我相信等效的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可以创build上面的方法,但是绑定到它的唯一方法是通过虚拟调用(即它实现一个编译时间的已知接口/摘要,您可以调用)或间接调用(即通过委托调用)。 我想这两种情况都会比装箱/取消装箱的开销慢。

另外,不要忘记,JIT不是愚蠢的,可能为你照顾这个。 ( 编辑 请参阅Eric Lippert对原始问题的评论 – 他说抖动目前不执行此优化。

与所有绩效相关的问题一样:措施,措施,措施!

这类似于这里发布的答案,但是使用expression式树来发射il来在types之间进行转换。 Expression.Convert做的伎俩。 已编译的委托(脚本)由内部静态类进行caching。 由于源对象可以从参数推断,我想它提供了更清晰的调用。 例如,一个通用的上下文:

 static int Generic<T>(T t) { int variable = -1; // may be a type check - if(... variable = CastTo<int>.From(t); return variable; } 

class上:

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

你可以用其他的实现来代替casterfunction。 我会比较几个performance:

 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 ms
    脚轮1 – > 102毫秒
    脚轮2 – > 102毫秒
    脚轮3 – > 90毫秒
    脚轮4 – > 101毫秒

  2. intint?

    对象投射 – > 651毫秒
    脚轮1 – >失败
    脚轮2 – >失败
    脚轮3 – > 109毫秒
    脚轮4 – >失败

  3. int? int

    对象铸造 – > 1957毫秒
    脚轮1 – >失败
    脚轮2 – >失败
    脚轮3 – > 124 ms
    脚轮4 – >失败

  4. enumint

    对象投射 – > 405毫秒
    脚轮1 – >失败
    脚轮2 – > 102毫秒
    脚轮3 – > 78毫秒
    脚轮4 – >失败

  5. intenum

    对象投射 – > 370 ms
    脚轮1 – >失败
    脚轮2 – > 93毫秒
    脚轮3 – > 87毫秒
    脚轮4 – >失败

  6. int? enum

    对象投射 – > 2340毫秒
    脚轮1 – >失败
    脚轮2 – >失败
    脚轮3 – > 258毫秒
    脚轮4 – >失败

  7. enum? int

    对象投射 – > 2776毫秒
    脚轮1 – >失败
    脚轮2 – >失败
    脚轮3 – > 131毫秒
    脚轮4 – >失败


Expression.Convert将源types直接转换为目标types,因此可以进行显式和隐式转换(更不用说引用转换)。 所以这给了处理(TTarget)(object)(TSource)方式,否则只有当非盒装(即在一个通用的方法,如果你做(TTarget)(object)(TSource)它会爆炸,如果它不是身份转换(如上一节)或参考转换(如后面部分所示))。 所以我会把他们包括在testing中。

非盒装演员:

  1. int double

    对象投射 – >失败
    脚轮1 – >失败
    脚轮2 – >失败
    脚轮3 – > 109毫秒
    脚轮4 – > 118 ms

  2. enumint?

    对象投射 – >失败
    脚轮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 – >失败

为了好玩,我testing了一些引用types转换:

  1. PrintStringProperty string (表示法更改)

    对象投射 – >失败(相当明显,因为它没有转回到原始types)
    脚轮1 – >失败
    脚轮2 – >失败
    脚轮3 – > 315 ms
    脚轮4 – >失败

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

    对象投射 – > 78毫秒
    脚轮1 – >失败
    脚轮2 – >失败
    脚轮3 – > 322毫秒
    脚轮4 – >失败

像这样testing:

 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. 我的估计是,除非你运行这个至less十万次,这是不值得的,你几乎没有什么可担心的拳击。 请注意,caching代表对内存有很大的影响。 但超出这个极限, 速度的提高是显着的,特别是涉及到可空的铸件时

  2. CastTo<T>类的真正优点是它允许可能非盒装的转换,比如(int)double在通用上下文中是(int)double 。 因为这样(int)(object)double在这些情况下失败。

  3. 我已经使用Expression.ConvertChecked而不是Expression.Convert以便检查算术溢出和下溢(即导致exception)。 由于il是在运行时生成的,检查的设置是编译时的事情,所以你无法知道调用代码的检查上下文。 这是你必须自己决定的事情。 select一个,或提供两个(更好)超负荷。

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

我知道我晚了晚会,但如果你只是需要做这样的安全演员,你可以使用以下使用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或expression式树,你有一个方法,将int转换为枚举而无需装箱或拆箱。 请注意,这里的TEnum必须有一个基本types的int否则这将抛出一个exception,说它不能被绑定。

编辑:另一种方法,也可能有点less写…

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

这工作将您的32位或更less枚举从TEnum转换为int。 而不是相反。 在.net 3.5+中, EnumEqualityComparer被优化,基本上把它变成一个return (int)value ;

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

…我甚至'以后':)

但只是扩大了前一个职位(迈克尔B),做了所有有趣的工作

并让我有兴趣为一个通用的情况下做一个包装(如果你想要generics实际枚举)

…并优化了一点…(注意:主要是使用'as'在Func <> /委托 – 作为枚举,值types不允许它)

 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

我想你总是可以使用System.Reflection.Emit来创build一个dynamic的方法,并发出这样做的指示,没有装箱,虽然它可能是无法validation的。

这是一个最简单,最快捷的方法。 (几乎没有限制:-))

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

我希望我还不迟

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

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

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

我找不到任何理由不采取这种做法(这个类将被放置在堆中,而不是在速度较慢的堆栈上,但这是值得的)

请让我知道你在想什么。