web-dev-qa-db-fra.com

C # non-boxing conversion de générique en int?

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.

53
Jeff Sharp

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!

17
Drew Noakes

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 :

  1. int à int

    casting d'objet -> 42 ms
    lanceur1 -> 102 ms
    lanceur2 -> 102 ms
    caster3 -> 90 ms
    caster4 -> 101 ms

  2. int à int?

    casting d'objet -> 651 ms
    lanceur1 -> échec
    lanceur2 -> échec
    caster3 -> 109 ms
    caster4 -> fail

  3. int? à int

    casting d'objet -> 1957 ms
    lanceur1 -> échec
    lanceur2 -> échec
    caster3 -> 124 ms
    caster4 -> fail

  4. enum à int

    casting d'objet -> 405 ms
    lanceur1 -> échec
    lanceur2 -> 102 ms
    lanceur3 -> 78 ms
    caster4 -> fail

  5. int à enum

    casting d'objet -> 370 ms
    lanceur1 -> échec
    lanceur2 -> 93 ms
    lanceur3 -> 87 ms
    caster4 -> fail

  6. int? à enum

    casting d'objet -> 2340 ms
    lanceur1 -> échec
    lanceur2 -> échec
    lanceur3 -> 258 ms
    caster4 -> fail

  7. 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:

  1. int à double

    casting d'objet -> échec
    lanceur1 -> échec
    lanceur2 -> échec
    caster3 -> 109 ms
    caster4 -> 118 ms

  2. enum à int?

    casting d'objet -> échec
    lanceur1 -> échec
    lanceur2 -> échec
    lanceur3 -> 93 ms
    caster4 -> fail

  3. int à enum?

    casting d'objet -> échec
    lanceur1 -> échec
    lanceur2 -> échec
    lanceur3 -> 93 ms
    caster4 -> fail

  4. enum? à int?

    casting d'objet -> échec
    lanceur1 -> échec
    lanceur2 -> échec
    lanceur3 -> 121 ms
    caster4 -> fail

  5. 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:

  1. 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

  2. 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:

  1. 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 .

  2. 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.

  3. 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).

  4. 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.

40
nawfal

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.

31
Michael B

... 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

4
NSGaga

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.

3
Cecil Has a Name

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.

2
MooLim Lee

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 );
0
Eideren