web-dev-qa-db-fra.com

Contraintes génériques, où T: struct et T: class

Je voudrais faire la distinction entre les cas suivants:

  1. Un type de valeur simple (par exemple int)
  2. Un type de valeur nullable (par exemple int?)
  3. Un type de référence (par exemple, string) - éventuellement, cela ne me dérangerait pas que cela soit mappé sur (1) ou (2) ci-dessus

Je suis venu avec le code suivant, qui fonctionne très bien pour les cas (1) et (2):

static void Foo<T>(T a) where T : struct { } // 1

static void Foo<T>(T? a) where T : struct { } // 2

Cependant, si j'essaie de détecter cas (3) comme ça, il ne compile pas:

static void Foo<T>(T a) where T : class { } // 3

Le message d'erreur est Le type 'X' définit déjà un membre appelé 'Foo' avec les mêmes types de paramètres . Eh bien, d’une manière ou d’une autre, je ne peux pas faire la différence entre where T : struct et where T : class.

Si je supprime la troisième fonction (3), le code suivant ne compile pas non plus:

int x = 1;
int? y = 2;
string z = "a";

Foo (x); // OK, calls (1)
Foo (y); // OK, calls (2)
Foo (z); // error: the type 'string' must be a non-nullable value type ...

Comment puis-je obtenir la compilation de Foo(z) en la mappant à l’une des fonctions ci-dessus (ou à une troisième avec une autre contrainte à laquelle je n’ai pas pensé)?

44
Pierre Arnaud

Les contraintes ne font pas partie de la signature, mais les paramètres le sont. Et les contraintes de paramètres sont appliquées lors de la résolution de la surcharge.

Alors mettons la contrainte dans un paramètre. C'est moche, mais ça marche.

class RequireStruct<T> where T : struct { }
class RequireClass<T> where T : class { }

static void Foo<T>(T a, RequireStruct<T> ignore = null) where T : struct { } // 1
static void Foo<T>(T? a) where T : struct { } // 2
static void Foo<T>(T a, RequireClass<T> ignore = null) where T : class { } // 3

(mieux vaut six ans de retard que jamais?)

46
Alcaro

Vous ne pouvez malheureusement pas différencier le type de méthode à appeler en fonction des contraintes.

Vous devez donc définir une méthode dans une classe différente ou avec un nom différent.

Suite à votre commentaire sur La réponse de Marnix , vous pouvez réaliser ce que vous voulez en utilisant un peu de réflexion.

Dans l'exemple ci-dessous, la méthode Foo<T> sans contrainte utilise la réflexion pour filtrer les appels à la méthode contrainte appropriée, soit FooWithStruct<T> ou FooWithClass<T>. Pour des raisons de performances, nous allons créer et mettre en cache un délégué fortement typé plutôt que d'utiliser la réflexion en clair chaque fois que la méthode Foo<T> est appelée.

int x = 42;
MyClass.Foo(x);    // displays "Non-Nullable Struct"

int? y = 123;
MyClass.Foo(y);    // displays "Nullable Struct"

string z = "Test";
MyClass.Foo(z);    // displays "Class"

// ...

public static class MyClass
{
    public static void Foo<T>(T? a) where T : struct
    {
        Console.WriteLine("Nullable Struct");
    }

    public static void Foo<T>(T a)
    {
        Type t = typeof(T);

        Delegate action;
        if (!FooDelegateCache.TryGetValue(t, out action))
        {
            MethodInfo mi = t.IsValueType ? FooWithStructInfo : FooWithClassInfo;
            action = Delegate.CreateDelegate(typeof(Action<T>), mi.MakeGenericMethod(t));
            FooDelegateCache.Add(t, action);
        }
        ((Action<T>)action)(a);
    }

    private static void FooWithStruct<T>(T a) where T : struct
    {
        Console.WriteLine("Non-Nullable Struct");
    }

    private static void FooWithClass<T>(T a) where T : class
    {
        Console.WriteLine("Class");
    }

    private static readonly MethodInfo FooWithStructInfo = typeof(MyClass).GetMethod("FooWithStruct", BindingFlags.NonPublic | BindingFlags.Static);
    private static readonly MethodInfo FooWithClassInfo = typeof(MyClass).GetMethod("FooWithClass", BindingFlags.NonPublic | BindingFlags.Static);
    private static readonly Dictionary<Type, Delegate> FooDelegateCache = new Dictionary<Type, Delegate>();
}

(Notez que cet exemple n’est pas threadsafe. Si vous avez besoin de la sécurité des threads, vous devrez soit verrouiller tous les accès au dictionnaire de cache, soit vous pouvez cibler. NET4 - utilisez ConcurrentDictionary<K,V> à la place.)

10
LukeH

Déposez la contrainte de struct sur la première méthode. Si vous devez différencier les types de valeur des classes, vous pouvez utiliser le type de l'argument pour le faire.

      static void Foo( T? a ) where T : struct
      {
         // nullable stuff here
      }

      static void Foo( T a )
      {
         if( a is ValueType )
         {
            // ValueType stuff here
         }
         else
         {
            // class stuff
         }
      }
5
Marnix van Valen

Pour amplifier mon commentaire en LukeH, un modèle utile si vous devez utiliser Reflection pour appeler différentes actions en fonction d'un paramètre de type (par opposition au type d'une instance d'objet) consiste à créer une classe statique générique privée, semblable au suivant (ceci le code exact n’a pas été testé, mais j’ai déjà fait ce genre de chose auparavant):

 classe statique FooInvoker <T> 
 {
 action publique <Foo> theAction = configureAction; 
 void ActionForOneKindOfThing <TT> (paramètre TT) où TT: thatKindOfThing, T 
 {
 ...
 } 
 void ActionForAnotherKindOfThing <TT> (paramètre TT) où TT: thatOtherKindOfThing, T 
 {
 ...
 } 
 void configureAction (T param) 
 {
 ... Déterminez quel genre de chose T est, et définissez ʻAction` à l'un des 
 ... ci-dessus méthodes. Alors finissez avec ...
 theAction (param); 
 } 
} 

Notez que Reflection lève une exception si l’on tente de créer un délégué pour ActionForOneKindOfThing<TT>(TT param) lorsque TT ne respecte pas les contraintes de cette méthode. Étant donné que le système a validé le type de TT lors de la création du délégué, il est possible d'appeler theAction en toute sécurité sans autre vérification de type. Notez également que si le code extérieur fait:

 FooInvoker <T> .theAction (param); 

seul le premier appel nécessitera une réflexion. Les appels suivants invoqueront simplement le délégué directement.

2
supercat

Si vous n'avez pas besoin de paramètres génériques et que vous voulez juste faire la différence entre ces 3 cas lors de la compilation, vous pouvez utiliser le code suivant.

static void Foo(object a) { } // reference type
static void Foo<T>(T? a) where T : struct { } // nullable
static void Foo(ValueType a) { } // valuetype
1
Daniel Kozłowski

Heureusement, ce genre de bricolage est moins requis depuis la version 7.3 de C #

Voir Quoi de neuf en C # 7.3 - Ce n'est pas très explicite, mais il semble maintenant utiliser les arguments 'où' dans une certaine mesure lors de la résolution de la surcharge. 

La résolution de surcharge a maintenant moins de cas ambigus

Voir aussi Sélection de la version C # dans votre projet Visual Studio

Il y aura toujours des affrontements avec les suivants

Foo(x);
...
static void Foo<T>(T a) where T : class { } // 3
static void Foo<T>(T a) where T : struct { } // 3

Mais va correctement résoudre

Foo(x);
...
static void Foo<T>(T a, bool b = false) where T : class { } // 3
static void Foo<T>(T a) where T : struct { } // 3
0
Sprotty