Etant donné un paramètre générique TEnum qui sera toujours un type enum, existe-t-il un moyen de convertir TEnum en int sans boxing/unboxing?
Voir cet exemple de code. Cela va box/unbox la valeur inutilement.
private int Foo<TEnum>(TEnum value)
where TEnum : struct // C# does not allow enum constraint
{
return (int) (ValueType) value;
}
Le C # ci-dessus est compilé en mode de libération pour l'IL suivant (opcodes de boxing et unboxing):
.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
}
La conversion Enum a été traitée de manière exhaustive sur SO, mais je n'ai trouvé aucune discussion concernant ce cas particulier.
Je ne suis pas sûr que cela soit possible en C # sans utiliser Reflection.Emit. Si vous utilisez Reflection.Emit, vous pouvez charger la valeur de l'énum sur la pile, puis la traiter comme s'il s'agissait d'un entier.
Cependant, vous devez écrire pas mal de code, vous voudrez donc vérifier si vous gagnerez réellement en performance.
Je crois que l'IL équivalent serait:
.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
}
Notez que cela échouera si votre énumération dérive de long
(un entier de 64 bits.)
MODIFIER
Une autre pensée sur cette approche. Reflection.Emit peut créer la méthode ci-dessus, mais la seule façon de la lier est via un appel virtuel (c’est-à-dire qu’il implémente une interface/un résumé connu au moment de la compilation que vous pouvez appeler) ou un appel indirect (c.-à-d. via une invocation de délégué). J'imagine que ces deux scénarios seraient plus lents que les frais généraux de la boxe/unboxing de toute façon.
N'oubliez pas non plus que l'EJE n'est pas idiote et qu'elle peut s'en occuper pour vous. (EDIT voir le commentaire d'Eric Lippert sur la question initiale - il dit que la gigue n'effectue pas actuellement cette optimisation. )
Comme pour tous les problèmes liés aux performances: mesurer, mesurer, mesurer!
Ceci est similaire aux réponses postées ici, mais utilise des arbres d’expression pour les émettre entre les types. Expression.Convert
fait le tour. Le délégué compilé (caster) est mis en cache par une classe statique interne. Puisque l'objet source peut être déduit de l'argument, je suppose qu'il offre un appel plus propre. Par exemple un contexte générique:
static int Generic<T>(T t)
{
int variable = -1;
// may be a type check - if(...
variable = CastTo<int>.From(t);
return variable;
}
La classe:
/// <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();
}
}
}
Vous pouvez remplacer la caster
func par d'autres implémentations. Je vais comparer les performances de quelques uns:
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>));
}
Incantations en boîte :
int
à int
casting d'objet -> 42 ms
lanceur1 -> 102 ms
lanceur2 -> 102 ms
caster3 -> 90 ms
caster4 -> 101 ms
int
à int?
casting d'objet -> 651 ms
lanceur1 -> échec
lanceur2 -> échec
caster3 -> 109 ms
caster4 -> fail
int?
à int
casting d'objet -> 1957 ms
lanceur1 -> échec
lanceur2 -> échec
caster3 -> 124 ms
caster4 -> fail
enum
à int
casting d'objet -> 405 ms
lanceur1 -> échec
lanceur2 -> 102 ms
lanceur3 -> 78 ms
caster4 -> fail
int
à enum
casting d'objet -> 370 ms
lanceur1 -> échec
lanceur2 -> 93 ms
lanceur3 -> 87 ms
caster4 -> fail
int?
à enum
casting d'objet -> 2340 ms
lanceur1 -> échec
lanceur2 -> échec
lanceur3 -> 258 ms
caster4 -> fail
enum?
à int
casting d'objet -> 2776 ms
lanceur1 -> échec
lanceur2 -> échec
caster3 -> 131 ms
caster4 -> fail
Expression.Convert
transforme directement le type source en type cible afin qu'il puisse traiter des conversions explicites et implicites (sans parler des conversions de référence). Cela laisse donc la place à la manipulation du casting qui n’est par ailleurs possible que si elle n’est pas encadrée (c’est-à-dire, dans une méthode générique si vous utilisez (TTarget)(object)(TSource)
, elle explose si ce n’est pas une conversion d’identité (comme dans la section précédente) ou une conversion de référence (comme indiqué plus loin). section)). Je vais donc les inclure dans les tests.
Incantations non emballées:
int
à double
casting d'objet -> échec
lanceur1 -> échec
lanceur2 -> échec
caster3 -> 109 ms
caster4 -> 118 ms
enum
à int?
casting d'objet -> échec
lanceur1 -> échec
lanceur2 -> échec
lanceur3 -> 93 ms
caster4 -> fail
int
à enum?
casting d'objet -> échec
lanceur1 -> échec
lanceur2 -> échec
lanceur3 -> 93 ms
caster4 -> fail
enum?
à int?
casting d'objet -> échec
lanceur1 -> échec
lanceur2 -> échec
lanceur3 -> 121 ms
caster4 -> fail
int?
à enum?
casting d'objet -> échec
lanceur1 -> échec
lanceur2 -> échec
lanceur3 -> 120 ms
caster4 -> fail
Pour le plaisir, j’ai testé a quelques conversions de types de référence:
PrintStringProperty
à string
(changement de représentation)
transtypage d'objet -> échec (assez évident, car il n'est pas renvoyé au type d'origine)
lanceur1 -> échec
lanceur2 -> échec
lanceur3 -> 315 ms
caster4 -> fail
string
à object
(représentation préservant la conversion de référence)
casting d'objet -> 78 ms
lanceur1 -> échec
lanceur2 -> échec
caster3 -> 322 ms
caster4 -> fail
Testé comme ça:
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);
}
Remarque:
Mon estimation est la suivante: à moins d’avoir couru au moins cent mille fois, cela ne vaut pas la peine et vous n’avez presque rien à craindre de la boxe. Notez que la mise en cache des délégués a un impact sur la mémoire. Mais au-delà de cette limite, l’amélioration de la vitesse est significative, notamment en ce qui concerne les conversions impliquant des nullables .
Mais le réel avantage de la classe CastTo<T>
réside dans le fait qu’elle autorise les conversions possibles non encadrées, comme (int)double
dans un contexte générique. En tant que tel, (int)(object)double
échoue dans ces scénarios.
J'ai utilisé Expression.ConvertChecked
au lieu de Expression.Convert
afin que les débordements et les sous-débits arithmétiques soient vérifiés (c'est-à-dire que les résultats sont une exception). Comme il est généré au moment de l'exécution et que les paramètres vérifiés sont des éléments de la compilation, il est impossible de connaître le contexte vérifié du code d'appel. C'est quelque chose que vous devez décider vous-même. Choisissez-en un ou créez une surcharge pour les deux (meilleur).
S'il n'existe pas de conversion de TSource
à TTarget
, une exception est levée pendant la compilation du délégué. Si vous souhaitez un comportement différent, comme obtenir une valeur par défaut de TTarget
, vous pouvez vérifier la compatibilité des types à l'aide de la réflexion avant la compilation du délégué. Vous avez le contrôle total du code généré. Cela va être extrêmement délicat, cependant, vous devez vérifier la compatibilité des références (IsSubClassOf
, IsAssignableFrom
), l'existence de l'opérateur de conversion (va être hacky), et même pour certains convertisseurs de types intégrés entre types primitifs. Va être extrêmement hacky. Plus facile consiste à intercepter une exception et à renvoyer un délégué de valeur par défaut basé sur ConstantExpression
. Juste en indiquant une possibilité que vous pouvez imiter le comportement du mot clé as
qui ne jette pas. Il vaut mieux rester à l'écart et rester fidèle à la convention.
Je sais que je suis bien en retard à la fête, mais si vous avez juste besoin de faire une distribution sécurisée comme celle-ci, vous pouvez utiliser ce qui suit en utilisant 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>
maintenant, sans écrire Reflection.Emit
ni les arbres d’expression, vous avez une méthode qui convertira int en enum sans boxing ou unboxing. Notez que TEnum
doit ici avoir un type sous-jacent de int
ou ceci lève une exception disant qu'il ne peut pas être lié.
Edit: Une autre méthode qui fonctionne aussi et qui pourrait être un peu moins à écrire ...
Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;
Cela fonctionne pour convertir votre 32bit ou moins enum d'un TEnum à un int. Pas l'inverse. Dans .Net 3.5+, la EnumEqualityComparer
est optimisée pour en faire un retour (int)value
;
Vous payez les frais généraux liés à l'utilisation d'un délégué, mais ce sera certainement mieux que la boxe.
... je suis même 'plus tard':)
mais juste pour prolonger le post précédent (Michael B), qui a fait tout le travail intéressant
et m'a intéressé à faire un wrapper pour un cas générique (si vous voulez convertir générique à enum en fait)
... et optimisé un peu ... (note: le but principal est d'utiliser 'comme' sur Func <>/delegues - comme Enum, les types de valeur ne le permettent pas)
public static class Identity<TEnum, T>
{
public static readonly Func<T, TEnum> Cast = (Func<TEnum, TEnum>)((x) => x) as Func<T, TEnum>;
}
... et vous pouvez l'utiliser comme ça ...
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();
}
}
... pour (int) - just (int) rel
Je suppose que vous pouvez toujours utiliser System.Reflection.Emit pour créer une méthode dynamique et émettre les instructions qui le font sans boxe, bien que cela puisse être invérifiable.
Voici un moyen le plus simple et le plus rapide.
(avec une petite restriction. :-))
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;
}
}
Restriction:
Cela fonctionne en Mono. (ex. Unity3D)
Plus d'informations sur Unity3D:
La classe CastTo d’ErikE est un moyen vraiment élégant de résoudre ce problème.
MAIS il ne peut pas être utilisé tel quel dans Unity3D
Tout d'abord, il doit être corrigé comme ci-dessous.
(parce que le compilateur mono ne peut pas compiler le code original)
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);
}
}
Deuxièmement, le code d'ErikE ne peut pas être utilisé sur la plateforme AOT.
Mon code est donc la meilleure solution pour Mono.
Pour commenter 'Kristof':
Je suis désolé de ne pas avoir écrit tous les détails.
Voici une solution très simple avec la contrainte de type générique non géré de 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;
}
}
}
Nécessite une bascule non sécurisée dans la configuration de votre projet.
Usage:
int intValue = EnumExtensions<YourEnumType>.To<int>( yourEnumValue );