web-dev-qa-db-ja.com

ジェネリック列挙型からintへのC#非ボックス化変換?

常に列挙型になるジェネリックパラメーター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で広範囲に扱われましたが、この特定のケースに対処する議論は見つかりませんでした。

59
Jeff Sharp

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は馬鹿ではなく、あなたのためにこれを処理するかもしれないことを忘れないでください。 ([〜#〜] edit [〜#〜]元の質問に対するEric Lippertのコメントを参照してください-現在、ジッターはこの最適化を実行していないと述べています。

すべてのパフォーマンス関連の問題と同様に、測定、測定、測定!

17
Drew Noakes

これはここに投稿された回答に似ていますが、式ツリーを使用してilを出力し、型間でキャストします。 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 ms
    caster1-> 102 ms
    キャスター2-> 102ミリ秒
    キャスター3-> 90ミリ秒
    キャスター4-> 101ミリ秒

  2. intからint?

    オブジェクトのキャスト-> 651 ms
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 109ミリ秒
    caster4->失敗

  3. int?からint

    オブジェクトのキャスト-> 1957ミリ秒
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 124ミリ秒
    caster4->失敗

  4. enumからint

    オブジェクトのキャスト-> 405 ms
    caster1->失敗
    キャスター2-> 102ミリ秒
    キャスター3-> 78ミリ秒
    caster4->失敗

  5. intからenum

    オブジェクトのキャスト-> 370 ms
    caster1->失敗
    キャスター2-> 93ミリ秒
    キャスター3-> 87ミリ秒
    caster4->失敗

  6. int?からenum

    オブジェクトのキャスト-> 2340 ms
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 258ミリ秒
    caster4->失敗

  7. enum?からint

    オブジェクトのキャスト-> 2776 ms
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 131ミリ秒
    caster4->失敗


Expression.Convertは、ソース型からターゲット型への直接キャストを行うため、明示的および暗黙的なキャストを実行できます(参照キャストは言うまでもありません)。したがって、これはキャストを処理する方法を提供しますが、これは、ボックス化されていない場合にのみ可能です(つまり、ジェネリックメソッドで(TTarget)(object)(TSource)を実行すると、アイデンティティ変換(前のセクションのように)または参照変換でない場合に爆発します(参照変換(後のセクションに示すように))。だから私はそれらをテストに含めます。

非箱型キャスト:

  1. intからdouble

    オブジェクトのキャスト->失敗
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 109ミリ秒
    キャスター4-> 118ミリ秒

  2. enumからint?

    オブジェクトのキャスト->失敗
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 93ミリ秒
    caster4->失敗

  3. intからenum?

    オブジェクトのキャスト->失敗
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 93ミリ秒
    caster4->失敗

  4. enum?からint?

    オブジェクトのキャスト->失敗
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 121ミリ秒
    caster4->失敗

  5. int?からenum?

    オブジェクトのキャスト->失敗
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 120ミリ秒
    caster4->失敗

それを楽しむために、いくつかの参照型変換をテストしました:

  1. PrintStringPropertyからstring(表現の変更)

    オブジェクトのキャスト->失敗します(元の型にキャストされないため、明らかです)
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 315ミリ秒
    caster4->失敗

  2. stringからobject(参照変換を保持する表現)

    オブジェクトのキャスト-> 78 ms
    caster1->失敗
    キャスター2->失敗
    キャスター3-> 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. 私の推定では、これを少なくとも10万回実行しない限り、その価値はなく、ボクシングについて心配する必要はほとんどありません。デリゲートをキャッシュすることは、メモリに影響を与えることに注意してください。ただし、その制限を超えると、特にnullableを使用したキャストの場合、速度が大幅に向上します。

  2. ただし、CastTo<T>クラスの本当の利点は、一般的なコンテキストでの(int)doubleのように、非ボックス化が可能なキャストを許可する場合です。したがって、これらのシナリオでは(int)(object)doubleは失敗します。

  3. Expression.ConvertCheckedの代わりにExpression.Convertを使用して、算術オーバーフローとアンダーフローがチェックされるようにしました(つまり、例外が発生します)。 ilは実行時に生成され、チェックされた設定はコンパイル時のものであるため、呼び出しコードのチェックされたコンテキストを知る方法はありません。これはあなたが自分で決めなければならないことです。 1つを選択するか、両方にオーバーロードを提供します(より良い)。

  4. TSourceからTTargetへのキャストが存在しない場合、デリゲートのコンパイル中に例外がスローされます。 TTargetのデフォルト値を取得するなど、別の動作が必要な場合は、デリゲートをコンパイルする前にリフレクションを使用して型の互換性を確認できます。生成されるコードを完全に制御できます。ただし、非常にトリッキーになります。参照の互換性(IsSubClassOfIsAssignableFrom)、変換演算子の存在(ハックになる)、および組み込みの型間の互換性についても確認する必要があります。プリミティブ型。非常にハックになるつもりです。より簡単なのは、例外をキャッチし、ConstantExpressionに基づいてデフォルト値のデリゲートを返すことです。スローしないasキーワードの動作を模倣できる可能性を述べているだけです。それから離れて慣習に固執する方が良い。

50
nawfal

パーティーに遅れるのはわかっていますが、このような安全なキャストを行う必要がある場合は、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は、基本的にこれを戻り値(int)valueに変換するように最適化されています。

あなたはデリゲートを使用するオーバーヘッドを払っていますが、それは確かにボクシングよりも優れています。

32
Michael B

...私も「後で」:)

しかし、前の投稿(Michael B)を拡張するだけです。

そして、ジェネリックケースのラッパーを作ることに興味を持ってもらいました(ジェネリックを実際に列挙型にキャストしたい場合)

...そして少し最適化されました...(注:主なポイントは、代わりにFunc <>/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();
    }
}

...(int)の場合-(int)relのみ

4
NSGaga

検証できない場合もありますが、System.Reflection.Emitを使用して動的メソッドを作成し、ボクシングを行わずにこれを行う命令を発行することができると思います。

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に最適なソリューションです。

コメント投稿者「クリストフ」:
すべての詳細を記入しなかったのは残念です。

2
MooLim Lee

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.MemoryCopy dahallの提案からキャストされた単純なポインタ。

0
Eideren