通用枚举的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(); } } }
你可以用其他的实现来代替caster
function。 我会比较几个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>)); }
盒装演员 :
-
int
到int
对象投射 – > 42 ms
脚轮1 – > 102毫秒
脚轮2 – > 102毫秒
脚轮3 – > 90毫秒
脚轮4 – > 101毫秒 -
int
为int?
对象投射 – > 651毫秒
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 109毫秒
脚轮4 – >失败 -
int?
int
对象铸造 – > 1957毫秒
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 124 ms
脚轮4 – >失败 -
enum
为int
对象投射 – > 405毫秒
脚轮1 – >失败
脚轮2 – > 102毫秒
脚轮3 – > 78毫秒
脚轮4 – >失败 -
int
来enum
对象投射 – > 370 ms
脚轮1 – >失败
脚轮2 – > 93毫秒
脚轮3 – > 87毫秒
脚轮4 – >失败 -
int?
enum
对象投射 – > 2340毫秒
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 258毫秒
脚轮4 – >失败 -
enum?
int
对象投射 – > 2776毫秒
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 131毫秒
脚轮4 – >失败
Expression.Convert
将源types直接转换为目标types,因此可以进行显式和隐式转换(更不用说引用转换)。 所以这给了处理(TTarget)(object)(TSource)
方式,否则只有当非盒装(即在一个通用的方法,如果你做(TTarget)(object)(TSource)
它会爆炸,如果它不是身份转换(如上一节)或参考转换(如后面部分所示))。 所以我会把他们包括在testing中。
非盒装演员:
-
int
double
对象投射 – >失败
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 109毫秒
脚轮4 – > 118 ms -
enum
为int?
对象投射 – >失败
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 93毫秒
脚轮4 – >失败 -
int
enum?
对象投射 – >失败
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 93毫秒
脚轮4 – >失败 -
enum?
为int?
对象投射 – >失败
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 121毫秒
脚轮4 – >失败 -
int?
enum?
对象投射 – >失败
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 120毫秒
脚轮4 – >失败
为了好玩,我testing了一些引用types转换:
-
PrintStringProperty
string
(表示法更改)对象投射 – >失败(相当明显,因为它没有转回到原始types)
脚轮1 – >失败
脚轮2 – >失败
脚轮3 – > 315 ms
脚轮4 – >失败 -
string
到object
(表示保留参考转换)对象投射 – > 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); }
注意:
-
我的估计是,除非你运行这个至less十万次,这是不值得的,你几乎没有什么可担心的拳击。 请注意,caching代表对内存有很大的影响。 但超出这个极限, 速度的提高是显着的,特别是涉及到可空的铸件时 。
-
但
CastTo<T>
类的真正优点是它允许可能非盒装的转换,比如(int)double
在通用上下文中是(int)double
。 因为这样(int)(object)double
在这些情况下失败。 -
我已经使用
Expression.ConvertChecked
而不是Expression.Convert
以便检查算术溢出和下溢(即导致exception)。 由于il是在运行时生成的,检查的设置是编译时的事情,所以你无法知道调用代码的检查上下文。 这是你必须自己决定的事情。 select一个,或提供两个(更好)超负荷。 -
如果从
TSource
到TTarget
不存在,编译委托时抛出exception。 如果你想要一个不同的行为,比如获取TTarget
的默认值,你可以在编译委托之前使用reflection来检查types兼容性。 您可以完全控制正在生成的代码。 它会非常棘手,但你必须检查参考兼容性(IsSubClassOf
,IsAssignableFrom
),转换运算符的存在(将是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中。
我找不到任何理由不采取这种做法(这个类将被放置在堆中,而不是在速度较慢的堆栈上,但这是值得的)
请让我知道你在想什么。