72

给定一个始终是枚举类型的泛型参数 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
}

枚举转换已在 SO 上得到广泛处理,但我找不到针对此特定情况的讨论。

4

9 回答 9

61

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

您可以将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>));
}

盒装铸件

  1. intint

    对象投射 -> 42 ms
    caster1 -> 102 ms
    caster2 -> 102 ms
    caster3 -> 90 ms
    caster4 -> 101 ms

  2. intint?

    对象投射 -> 651 ms
    caster1 -> 失败
    caster2 -> 失败
    caster3 -> 109 ms
    caster4 -> 失败

  3. int?int

    对象投射 -> 1957 ms
    caster1 -> 失败
    caster2 -> 失败
    caster3 -> 124 ms
    caster4 -> 失败

  4. enumint

    对象投射 -> 405 ms
    caster1 -> 失败
    caster2 -> 102 ms
    caster3 -> 78 ms
    caster4 -> 失败

  5. intenum

    对象投射 -> 370 ms
    caster1 -> 失败
    caster2 -> 93 ms
    caster3 -> 87 ms
    caster4 -> 失败

  6. int?enum

    对象投射 -> 2340 ms
    caster1 -> 失败
    caster2 -> 失败
    caster3 -> 258 ms
    caster4 -> 失败

  7. enum?int

    对象投射 -> 2776 ms
    caster1 -> 失败
    caster2 -> 失败
    caster3 -> 131 ms
    caster4 -> 失败


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

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

    对象投射 -> 78 ms
    caster1 -> 失败
    caster2 -> 失败
    caster3 -> 322 ms
    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.ConvertCheckedExpression.Convertnot 来检查算术上溢和下溢(即导致异常)。由于 il 是在运行时生成的,并且检查的设置是编译时的事情,因此您无法知道调用代码的检查上下文。这是你必须自己决定的事情。选择一个,或为两者提供过载(更好)。

  4. TSource如果从to不存在强制转换TTarget,则在编译委托时会引发异常。如果您想要不同的行为,例如获取默认值TTarget,您可以在编译委托之前使用反射检查类型兼容性。您可以完全控制正在生成的代码。不过,这将非常棘手,您必须检查引用兼容性(IsSubClassOf, IsAssignableFrom)、转换运算符的存在(将是 hacky),甚至是原始类型之间的某些内置类型可转换性。会非常hacky。更容易的是捕获异常并基于ConstantExpression. 只是说明您可以模仿as不抛出的关键字的行为的可能性。最好远离它并坚持惯例。

于 2014-04-30T15:09:08.827 回答
37

我知道我参加聚会已经很晚了,但是如果您只需要像这样进行安全演员表,则可以使用以下命令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 转换为 enum 的方法。请注意,TEnum这里必须有一个基础类型,int否则会抛出一个异常,说它不能被绑定。

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

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

这可以将您的 32 位或更少的枚举从 TEnum 转换为 int。不是反过来。在 .Net 3.5+ 中,EnumEqualityComparer优化了基本上把它变成了 return (int)value

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

这已经相当老了,但是如果您仍然回到这里寻找适用于 .net 5/.Net 核心(或带有不安全包的 netfx)并保持最佳状态的解决方案......

[JitGeneric(typeof(StringComparison), typeof(int))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryConvert<TEnum, T>(this TEnum @enum, out T val)
    where TEnum : struct, Enum
    where T : struct, IConvertible, IFormattable, IComparable
{
    if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<TEnum>())
    {
        val = Unsafe.As<TEnum, T>(ref @enum);
        return true;
    }
    val = default;
    return false;
}

一个示例用法可能是这样的::

public static int M(MethodImplOptions flags) => flags.TryConvert(out int v) ? v : 0;

在这里我们可以在sharplab上看到,这个方法完全被内联了:: https://sharplab.io/#gist:802b8d21ee1de26e791294ba48f69d97

于 2010-10-26T18:10:33.090 回答
19

我不确定在不使用 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 对原始问题的评论——他说抖动目前没有执行这种优化。

与所有与性能相关的问题一样:测量、测量、测量!

于 2009-07-27T16:36:56.310 回答
4

...我什至“迟到”:)

但只是为了扩展上一篇文章(Michael B),它做了所有有趣的工作

并让我对为泛型案例制作包装器感兴趣(如果您实际上想将泛型转换为枚举)

...并进行了一些优化...(注意:重点是在 Func<>/delegates 上使用 'as' - 作为枚举,值类型不允许这样做)

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)rel

于 2012-03-02T19:35:21.123 回答
3

我想您总是可以使用 System.Reflection.Emit 创建一个动态方法并发出无需装箱即可执行此操作的指令,尽管它可能无法验证。

于 2009-07-27T16:33:17.520 回答
3

这是一种最简单最快的方法。
(有一点限制。:-))

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

限制:
这在 Mono 中有效。(例如 Unity3D)

有关 Unity3D 的更多信息:
ErikE 的 CastTo 类是解决此问题的一种非常巧妙的方法。
但它不能在 Unity3D 中使用

首先,它必须像下面这样固定。
(因为mono编译器无法编译原代码)

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平台上使用。
所以,我的代码是 Mono 的最佳解决方案。

致评论者“Kristof”:
很抱歉我没有写下所有细节。

于 2016-07-27T07:10:32.103 回答
3

这是使用 C# 7.3 的非托管泛型类型约束的非常直接的解决方案:

    using System;
    public static class EnumExtensions<TEnum> where TEnum : unmanaged, Enum
    {
        /// <summary>
        /// Converts a <typeparam name="TEnum"></typeparam> into a <typeparam name="TResult"></typeparam>
        /// through pointer cast.
        /// Does not throw if the sizes don't match, clips to smallest data-type instead.
        /// So if <typeparam name="TResult"></typeparam> is smaller than <typeparam name="TEnum"></typeparam>
        /// bits that cannot be captured within <typeparam name="TResult"></typeparam>'s size will be clipped.
        /// </summary>
        public static TResult To<TResult>( TEnum value ) where TResult : unmanaged
        {
            unsafe
            {
                if( sizeof(TResult) > sizeof(TEnum) )
                {
                    // We might be spilling in the stack by taking more bytes than value provides,
                    // alloc the largest data-type and 'cast' that instead.
                    TResult o = default;
                    *((TEnum*) & o) = value;
                    return o;
                }
                else
                {
                    return * (TResult*) & value;
                }
            }
        }

        /// <summary>
        /// Converts a <typeparam name="TSource"></typeparam> into a <typeparam name="TEnum"></typeparam>
        /// through pointer cast.
        /// Does not throw if the sizes don't match, clips to smallest data-type instead.
        /// So if <typeparam name="TEnum"></typeparam> is smaller than <typeparam name="TSource"></typeparam>
        /// bits that cannot be captured within <typeparam name="TEnum"></typeparam>'s size will be clipped.
        /// </summary>
        public static TEnum From<TSource>( TSource value ) where TSource : unmanaged
        {
            unsafe
            {

                if( sizeof(TEnum) > sizeof(TSource) )
                {
                    // We might be spilling in the stack by taking more bytes than value provides,
                    // alloc the largest data-type and 'cast' that instead.
                    TEnum o = default;
                    *((TSource*) & o) = value;
                    return o;
                }
                else
                {
                    return * (TEnum*) & value;
                }
            }
        }
    }

需要在项目配置中进行不安全切换。

用法:

int intValue = EnumExtensions<YourEnumType>.To<int>( yourEnumValue );

编辑:由Buffer.MemoryCopydahall 的建议中的简单指针替换。

于 2018-10-21T10:58:00.400 回答
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 倍。

于 2019-12-16T08:17:27.047 回答
-1

我希望我不会太晚...

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

如果您使用这种方法,您将拥有一个“感觉”像 Enum 的对象,但您将拥有类的所有灵活性,这意味着您可以覆盖任何运算符。

还有其他优点,例如使该类成为部分类,这将使您能够在多个文件/dll中定义相同的枚举,从而可以将值添加到通用dll而无需重新编译。

我找不到任何不采用这种方法的充分理由(这个类将位于堆中而不是堆栈中,虽然速度较慢但值得)

请让我知道你在想什么。

于 2015-05-19T07:25:50.237 回答