通用枚举的C#非装箱转换为int?(C# non-boxing conversion of gene

2019-06-26 22:56发布

给定一个泛型参数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
}

枚举转化已被广泛地处理上如此,但我无法找到一个解决讨论这个特定的情况下。

Answer 1:

我不知道这是在C#中无需使用Reflection.Emit的。 如果您使用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不哑,可能照顾这对你。 ( 编辑 看到在原来的问题埃里克利珀的评论-他说,抖动目前不执行该优化。)

如同所有的性能相关的问题:测量,测量,测量!



Answer 2:

这类似于在此处发布答案,但使用表达式树发出金正日类型之间进行铸造。 Expression.Convert的伎俩。 编译后的委托(脚轮)由内静态类缓存。 由于源对象可以通过以下参数来推断,我猜它提供了更清洁的呼叫。 对于如一般情况下:

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. int ,以int

    对象铸造 - > 42毫秒
    caster1 - > 102毫秒
    caster2 - > 102毫秒
    caster3 - > 90毫秒
    caster4 - > 101毫秒

  2. int ,以int?

    对象铸造 - > 651毫秒
    caster1 - >失败
    caster2 - >失败
    caster3 - > 109毫秒
    caster4 - >失败

  3. int?int

    对象铸造 - > 1957年毫秒
    caster1 - >失败
    caster2 - >失败
    caster3 - > 124毫秒
    caster4 - >失败

  4. enumint

    对象铸造 - > 405毫秒
    caster1 - >失败
    caster2 - > 102毫秒
    caster3 - > 78毫秒
    caster4 - >失败

  5. int ,以enum

    对象铸造 - > 370毫秒
    caster1 - >失败
    caster2 - > 93毫秒
    caster3 - > 87毫秒
    caster4 - >失败

  6. int?enum

    对象铸造 - > 2340毫秒
    caster1 - >失败
    caster2 - >失败
    caster3 - > 258毫秒
    caster4 - >失败

  7. enum?int

    对象铸造 - > 2776毫秒
    caster1 - >失败
    caster2 - >失败
    caster3 - > 131毫秒
    caster4 - >失败


Expression.Convert把直接投从源类型到目标类型,因此它可以制定出明确的和隐式类型转换(更不用说引用类型转换)。 所以,这给处理铸造这是其他可能,只有当非盒装(即,在一个通用的方法,如果你做的方式(TTarget)(object)(TSource)它会如果它不是身份转换爆炸(如在上一节)或引用转换(如在后面的部分示出))。 因此,我将它们包括在测试中。

非盒装强制转换:

  1. intdouble

    对象铸造 - >失败
    caster1 - >失败
    caster2 - >失败
    caster3 - > 109毫秒
    caster4 - > 118毫秒

  2. enumint?

    对象铸造 - >失败
    caster1 - >失败
    caster2 - >失败
    caster3 - > 93毫秒
    caster4 - >失败

  3. int ,以enum?

    对象铸造 - >失败
    caster1 - >失败
    caster2 - >失败
    caster3 - > 93毫秒
    caster4 - >失败

  4. enum?int?

    对象铸造 - >失败
    caster1 - >失败
    caster2 - >失败
    caster3 - > 121毫秒
    caster4 - >失败

  5. int?enum?

    对象铸造 - >失败
    caster1 - >失败
    caster2 - >失败
    caster3 - > 120毫秒
    caster4 - >失败

对于它的乐趣,我测试了几个引用类型转换:

  1. PrintStringPropertystring (表示变化)的

    对象铸造 - >失败(很明显的,因为它不转换回原来的类型)
    caster1 - >失败
    caster2 - >失败
    caster3 - > 315毫秒
    caster4 - >失败

  2. stringobject (表示保持基准转换)

    对象铸造 - > 78毫秒
    caster1 - >失败
    caster2 - >失败
    caster3 - > 322毫秒
    caster4 - >失败

经测试是这样的:

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 ),转换运营商的存在(将是哈克),甚至对于一些建在原始类型之间的类型可兑换。 将是非常哈克。 更简单的是赶上基于异常和返回默认值代表ConstantExpression 。 只是说,你可以模仿的行为可能as其犯规throw关键字。 它能够更好地远离它,并坚持公约。



Answer 3:

我知道我这样迟到了,但如果你只是需要做一个安全的投这样你可以使用以下使用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 ;

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



Answer 4:

......我甚至“后”:)

但只是为了延长在以前的帖子(迈克尔·B),它所做的一切有趣的工作

而我感兴趣到制作包装为一个通用的情况下(如果你想投的通用实际ENUM)

...和优化的位...(注:主要的一点是使用“为”上Func键<> /代表代替 - 如枚举,值类型不允许它)

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

...为(INT) - 刚(INT)相对。



Answer 5:

我想你可以随时使用System.Reflection.Emit创建一个动态方法,放出的是这样做没有拳击的说明,尽管它可能是无法证实的。



Answer 6:

这是一个最简单和最快的方式。
(有一点限制。:-))

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

限制:
这个工作在单声道。 (EX。Unity3D)

关于Unity3D的更多信息:
ErikE的CastTo类是解决这个问题的一个非常整洁的方式。
但它不能直接使用在Unity3D

首先,它必须是固定的像下面。
(因为单编译器不能编译原码)

public class CastTo {
    protected static class Cache<TTo, TFrom> {
        public static readonly Func<TFrom, TTo> Caster = Get();

        static Func<TFrom, TTo> Get() {
            var p = Expression.Parameter(typeof(TFrom), "from");
            var c = Expression.ConvertChecked(p, typeof(TTo));
            return Expression.Lambda<Func<TFrom, TTo>>(c, p).Compile();
        }
    }
}

public class ValueCastTo<TTo> : ValueCastTo {
    public static TTo From<TFrom>(TFrom from) {
        return Cache<TTo, TFrom>.Caster(from);
    }
}

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

要评论者克里斯托弗“:
我很抱歉,我没有写的所有细节。



Answer 7:

下面是用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 );


Answer 8:

我希望我不是太晚了?

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

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

有喜欢做那类的部分,这将使你在一个以上的文件/ DLL这使得可以将值添加到一个共同的DLL而无需重新编译它定义相同的枚举等优点。

我找不到任何好的理由不采取这种方法(这个类将位于堆而不是堆栈,它是比较慢的,但它是值得的)

请让我知道你在想什么。



文章来源: C# non-boxing conversion of generic enum to int?