给定一个通用参数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上得到广泛处理,但我无法找到解决此特定案例的讨论。
答案 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>));
}
盒装演员:
int
至int
对象投射 - &gt; 42毫秒
caster1 - &gt; 102毫秒
caster2 - &gt; 102毫秒
caster3 - &gt; 90毫秒
caster4 - &gt; 101毫秒
int
至int?
对象投射 - &gt; 651毫秒
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 109毫秒
caster4 - &gt;失败
int?
至int
对象投射 - &gt; 1957 ms
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 124毫秒
caster4 - &gt;失败
enum
至int
对象投射 - &gt; 405毫秒
caster1 - &gt;失败
caster2 - &gt; 102毫秒
caster3 - &gt; 78毫秒
caster4 - &gt;失败
int
至enum
对象投射 - &gt; 370毫秒
caster1 - &gt;失败
caster2 - &gt; 93毫秒
caster3 - &gt; 87毫秒
caster4 - &gt;失败
int?
至enum
对象投射 - &gt; 2340毫秒 caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 258毫秒
caster4 - &gt;失败
enum?
至int
对象投射 - &gt; 2776毫秒 caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 131毫秒
caster4 - &gt;失败
Expression.Convert
将源类型的直接强制转换为目标类型,因此它可以计算显式和隐式强制转换(更不用说引用强制转换)。因此,这为处理转换提供了方法,否则只有在非盒装的情况下才可行(例如,如果你(TTarget)(object)(TSource)
,如果它不是标识转换(如前一节)或参考转换那么它将会爆炸(如后面部分所示))。所以我会把它们包括在测试中。
非盒装演员:
int
至double
对象投射 - &gt;失败
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 109毫秒
caster4 - &gt; 118毫秒
enum
至int?
对象投射 - &gt;失败
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 93毫秒
caster4 - &gt;失败
int
至enum?
对象投射 - &gt;失败
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 93毫秒
caster4 - &gt;失败
enum?
至int?
对象投射 - &gt;失败
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 121毫秒
caster4 - &gt;失败
int?
至enum?
对象投射 - &gt;失败
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 120毫秒
caster4 - &gt;失败
为了好玩,我测试了几个参考类型转换:
PrintStringProperty
至string
(代表更改)
对象投射 - &gt;失败(很明显,因为它没有回归到原始类型)
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 315毫秒
caster4 - &gt;失败
string
至object
(表示保留参考转换)
对象投射 - &gt; 78毫秒
caster1 - &gt;失败
caster2 - &gt;失败
caster3 - &gt; 322毫秒
caster4 - &gt;失败
像这样测试:
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);
}
注意:
我的估计是,除非你运行至少十万次,否则它不值得,你几乎没有什么可担心拳击。请注意,缓存代表会记住内存。但是超出这个限制,速度的提升是显着的,特别是涉及到涉及nullables 的铸造时。
但CastTo<T>
类的真正优势在于它允许非通用的转换,例如通用上下文中的(int)double
。因此(int)(object)double
在这些情况下失败。
我使用Expression.ConvertChecked
而不是Expression.Convert
,以便检查算术溢出和下溢(即导致异常)。由于il是在运行时生成的,并且检查的设置是编译时间,因此您无法知道调用代码的已检查上下文。这是你必须自己决定的事情。选择一个,或为两者提供超载(更好)。
如果从TSource
到TTarget
的转换不存在,则在编译委托时抛出异常。如果您想要一个不同的行为,比如获取默认值TTarget
,您可以在编译委托之前使用反射检查类型兼容性。您可以完全控制生成的代码。它会变得非常棘手,你必须检查参考兼容性(IsSubClassOf
,IsAssignableFrom
),转换运算符是否存在(将成为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中。
我无法找到任何理由不接受这种方法(这个类将位于堆中而不是堆栈上,这个速度较慢,但它值得)
请告诉我你的想法。