常に列挙型となるジェネリック パラメーター 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 で広範囲に扱われていますが、この特定のケースを扱った議論は見つかりませんでした。
ベストアンサー1
これはここに投稿された回答と似ていますが、式ツリーを使用して 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
。いくつかのパフォーマンスを比較してみます。
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 ミリ秒
キャスター1 -> 102 ミリ秒
キャスター2 -> 102 ミリ秒
キャスター3 -> 90 ミリ秒
キャスター4 -> 101 ミリ秒int
にint?
オブジェクトキャスト -> 651 ミリ秒
キャスター1 -> 失敗
キャスター2 -> 失敗
キャスター3 -> 109 ミリ秒
キャスター4 -> 失敗int?
にint
オブジェクトキャスト -> 1957 ミリ秒
キャスター1 -> 失敗
キャスター2 -> 失敗
キャスター3 -> 124 ミリ秒
キャスター4 -> 失敗enum
にint
オブジェクトキャスト -> 405 ミリ秒
キャスター1 -> 失敗
キャスター2 -> 102 ミリ秒
キャスター3 -> 78 ミリ秒
キャスター4 -> 失敗int
にenum
オブジェクトキャスト -> 370 ミリ秒
キャスター1 -> 失敗
キャスター2 -> 93 ミリ秒
キャスター3 -> 87 ミリ秒
キャスター4 -> 失敗int?
にenum
オブジェクトキャスト -> 2340 ミリ秒
キャスター1 -> 失敗
キャスター2 -> 失敗
キャスター3 -> 258 ミリ秒
キャスター4 -> 失敗enum?
にint
オブジェクトキャスト -> 2776 ミリ秒
キャスター1 -> 失敗
キャスター2 -> 失敗
キャスター3 -> 131 ミリ秒
キャスター4 -> 失敗
Expression.Convert
ソース タイプからターゲット タイプへの直接キャストを配置し、明示的および暗黙的なキャスト (参照キャストは言うまでもありません) を処理できるようにします。これにより、非ボックス化の場合にのみ可能なキャストの処理が可能になります (つまり、ジェネリック メソッドで(TTarget)(object)(TSource)
これを行うと、アイデンティティ変換 (前のセクションのように) または参照変換 (後のセクションに示すように) でない場合は爆発します)。そのため、テストにそれらを含めます。
非箱入りキャスト:
int
にdouble
オブジェクトキャスト -> 失敗
キャスター1 -> 失敗
キャスター2 -> 失敗
キャスター3 -> 109 ミリ秒
キャスター4 -> 118 ミリ秒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 -> 失敗
楽しみのために、私は参照型変換は少ない:
PrintStringProperty
string
(表現の変更)オブジェクトキャスト -> 失敗 (元の型にキャストされていないので明らか)
キャスター1 -> 失敗
キャスター2 -> 失敗
キャスター3 -> 315 ミリ秒
キャスター4 -> 失敗string
object
(表現保存参照変換)オブジェクトキャスト -> 78 ミリ秒
キャスター1 -> 失敗
キャスター2 -> 失敗
キャスター3 -> 322 ミリ秒
キャスター4 -> 失敗
次のようにテストしました:
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);
}
注記:
私の推測では、少なくとも10万回実行しない限り、価値はなく、ボックス化について心配する必要はほとんどありません。デリゲートをキャッシュするとメモリに負荷がかかります。しかし、その制限を超えると、特にnullablesを含むキャストに関しては速度が大幅に向上します。。
しかし、このクラスの本当の利点は、ジェネリック コンテキストの
CastTo<T>
ように、ボックス化されていないキャストを許可する場合です。そのため、このようなシナリオでは失敗します。(int)double
(int)(object)double
Expression.ConvertChecked
代わりにを使用するExpression.Convert
ことで、算術オーバーフローとアンダーフローがチェックされます (つまり、例外が発生します)。 il は実行時に生成され、チェックされた設定はコンパイル時のものなので、呼び出しコードのチェックされたコンテキストを知る方法はありません。 これは自分で決める必要があります。 1 つを選択するか、両方にオーバーロードを提供します (より良い)。TSource
からへのキャストが存在しない場合はTTarget
、デリゲートのコンパイル中に例外がスローされます。 のデフォルト値を取得するなど、異なる動作が必要な場合はTTarget
、デリゲートをコンパイルする前にリフレクションを使用して型の互換性を確認できます。生成されるコードを完全に制御できます。ただし、参照の互換性 (IsSubClassOf
、IsAssignableFrom
)、変換演算子の存在 (ハッキングになります)、さらにはプリミティブ型間の組み込みの型変換可能性も確認する必要があるため、非常に注意が必要です。非常にハッキングになります。より簡単なのは、例外をキャッチし、 に基づいてデフォルト値のデリゲートを返すことですConstantExpression
。スローされないキーワードの動作を模倣できる可能性があることを述べているだけですas
。それを避けて、規則に従う方がよいでしょう。