L'opérateur == ne peut-il pas être appliqué aux types génériques en C #?

326

Selon la documentation de l' ==opérateur dans MSDN ,

Pour les types de valeurs prédéfinis, l'opérateur d'égalité (==) renvoie true si les valeurs de ses opérandes sont égales, false sinon. Pour les types de référence autres que chaîne, == renvoie true si ses deux opérandes font référence au même objet. Pour le type de chaîne, == compare les valeurs des chaînes. Les types de valeurs définis par l'utilisateur peuvent surcharger l'opérateur == (voir opérateur). Il en va de même pour les types de référence définis par l'utilisateur, bien que par défaut == se comporte comme décrit ci-dessus pour les types de référence prédéfinis et définis par l'utilisateur.

Alors pourquoi cet extrait de code ne parvient-il pas à être compilé?

bool Compare<T>(T x, T y) { return x == y; }

J'obtiens l'erreur Operator '==' ne peut pas être appliqué aux opérandes de type 'T' et 'T' . Je me demande pourquoi, dans la mesure où je comprends l' ==opérateur est prédéfini pour tous les types?

Edit: Merci à tous. Je n'ai pas remarqué au début que la déclaration concernait uniquement les types de référence. J'ai également pensé que la comparaison bit par bit est fournie pour tous les types de valeur, ce que je sais maintenant n'est pas correct.

Mais, dans le cas où j'utilise un type de référence, l' ==opérateur utiliserait-il la comparaison de référence prédéfinie, ou utiliserait-il la version surchargée de l'opérateur si un type en définissait un?

Edit 2: Par essais et erreurs, nous avons appris que l' ==opérateur utilisera la comparaison de référence prédéfinie lors de l'utilisation d'un type générique sans restriction. En fait, le compilateur utilisera la meilleure méthode qu'il puisse trouver pour l'argument de type restreint, mais ne cherchera pas plus loin. Par exemple, le code ci-dessous sera toujours imprimé true, même lorsqu'il Test.test<B>(new B(), new B())est appelé:

class A { public static bool operator==(A x, A y) { return true; } }
class B : A { public static bool operator==(B x, B y) { return false; } }
class Test { void test<T>(T a, T b) where T : A { Console.WriteLine(a == b); } }
Hosam Aly
la source
Voir à nouveau ma réponse pour la réponse à votre question de suivi.
Giovanni Galbo
Il pourrait être utile de comprendre que même sans génériques, il existe certains types pour lesquels le ==n'est pas autorisé entre deux opérandes du même type. Cela est vrai pour les structtypes (sauf les types "prédéfinis") qui ne surchargent pas le operator ==. Comme exemple simple, essayez ceci:var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */
Jeppe Stig Nielsen
Poursuivant mon propre vieux commentaire. Par exemple (voir autre thread ), avec var kvp1 = new KeyValuePair<int, int>(); var kvp2 = kvp1;, alors vous ne pouvez pas vérifier kvp1 == kvp2car KeyValuePair<,>est une structure, ce n'est pas un type prédéfini C # et il ne surcharge pas le operator ==. Pourtant, un exemple est donné par var li = new List<int>(); var e1 = li.GetEnumerator(); var e2 = e1;lequel vous ne pouvez pas faire e1 == e2(ici nous avons la structure imbriquée List<>.Enumerator(appelée "List`1+Enumerator[T]"par le runtime) qui ne surcharge pas ==).
Jeppe Stig Nielsen
RE: "Alors pourquoi cet extrait de code ne parvient-il pas à se compiler?" - Euh ... parce que vous ne pouvez pas retourner un boold'un void...
BrainSlugs83
1
@ BrainSlugs83 Merci d'avoir attrapé un bug de 10 ans!
Hosam Aly

Réponses:

143

"... par défaut == se comporte comme décrit ci-dessus pour les types de référence prédéfinis et définis par l'utilisateur."

Le type T n'est pas nécessairement un type de référence, le compilateur ne peut donc pas faire cette hypothèse.

Cependant, cela se compilera car il est plus explicite:

    bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }

Suivi d'une question supplémentaire: "Mais, si j'utilise un type de référence, l'opérateur == utiliserait-il la comparaison de référence prédéfinie, ou utiliserait-il la version surchargée de l'opérateur si un type en définissait un?"

J'aurais pensé que == sur les génériques utiliserait la version surchargée, mais le test suivant démontre le contraire. Intéressant ... j'aimerais savoir pourquoi! Si quelqu'un sait, veuillez partager.

namespace TestProject
{
 class Program
 {
    static void Main(string[] args)
    {
        Test a = new Test();
        Test b = new Test();

        Console.WriteLine("Inline:");
        bool x = a == b;
        Console.WriteLine("Generic:");
        Compare<Test>(a, b);

    }


    static bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }
 }

 class Test
 {
    public static bool operator ==(Test a, Test b)
    {
        Console.WriteLine("Overloaded == called");
        return a.Equals(b);
    }

    public static bool operator !=(Test a, Test b)
    {
        Console.WriteLine("Overloaded != called");
        return a.Equals(b);
    }
  }
}

Production

Inline: surchargé == appelé

Générique:

Appuyez sur n'importe quelle touche pour continuer . . .

Suivi 2

Je tiens à souligner que changer ma méthode de comparaison en

    static bool Compare<T>(T x, T y) where T : Test
    {
        return x == y;
    }

provoque l'appel de l'opérateur == surchargé. Je suppose que sans spécifier le type (comme ), le compilateur ne peut pas déduire qu'il devrait utiliser l'opérateur surchargé ... bien que je pense qu'il aurait suffisamment d'informations pour prendre cette décision même sans spécifier le type.

Giovanni Galbo
la source
Merci. Je n'ai pas remarqué que la déclaration concernait uniquement les types de référence.
Hosam Aly
4
Re: Follow Up 2: En fait, le compilateur liera la meilleure méthode qu'il trouve, qui est dans ce cas Test.op_Equal. Mais si vous aviez une classe qui dérive de Test et remplace l'opérateur, alors l'opérateur de Test sera toujours appelé.
Hosam Aly
4
La bonne pratique que je voudrais souligner est que vous devez toujours faire la comparaison réelle à l'intérieur d'une Equalsméthode surchargée (pas dans l' ==opérateur).
jpbochi
11
La résolution de surcharge se produit au moment de la compilation. Donc, quand nous avons ==entre les types génériques Tet T, la meilleure surcharge est trouvée, étant donné les contraintes qui sont portées par T(il y a une règle spéciale qui ne mettra jamais un type de valeur pour cela (ce qui donnerait un résultat sans signification), donc il doit y avoir une contrainte garantissant qu'il s'agit d'un type de référence). Dans votre suivi 2 , si vous venez avec des DerivedTestobjets, et DerivedTestdérive de Testmais introduit une nouvelle surcharge de ==, vous aurez à nouveau le "problème". Quelle surcharge est appelée, est "gravée" dans l'IL au moment de la compilation.
Jeppe Stig Nielsen
1
bizarrement, cela semble fonctionner pour les types de référence généraux (où vous vous attendez à ce que cette comparaison soit sur l'égalité de référence), mais pour les chaînes, il semble également utiliser l'égalité de référence - vous pouvez donc finir par comparer 2 chaînes identiques et avoir == (dans un méthode générique avec la contrainte de classe) disent qu'ils sont différents.
JonnyRaa
292

Comme d'autres l'ont dit, cela ne fonctionnera que lorsque T est contraint d'être un type de référence. Sans aucune contrainte, vous pouvez comparer avec null, mais seulement null - et cette comparaison sera toujours false pour les types de valeur non nullable.

Au lieu d'appeler Equals, il est préférable d'utiliser un IComparer<T>- et si vous n'avez plus d'informations, EqualityComparer<T>.Defaultc'est un bon choix:

public bool Compare<T>(T x, T y)
{
    return EqualityComparer<T>.Default.Equals(x, y);
}

Mis à part toute autre chose, cela évite la boxe / casting.

Jon Skeet
la source
Merci. J'essayais d'écrire une classe wrapper simple, donc je voulais juste déléguer l'opération au membre enveloppé réel. Mais connaître EqualityComparer <T> .Default a certainement ajouté de la valeur pour moi. :)
Hosam Aly
Mineure à part, Jon; vous voudrez peut-être noter le commentaire concernant pobox vs yoda sur mon post.
Marc Gravell
4
Bon conseil pour utiliser EqualityComparer <T>
chakrit
1
+1 pour avoir souligné qu'il peut se comparer à null et pour un type de valeur non nullable, ce sera toujours faux
Jalal Said
@BlueRaja: Oui, car il existe des règles spéciales pour les comparaisons avec le littéral nul. Par conséquent, "sans aucune contrainte, vous pouvez comparer avec null, mais seulement null". C'est déjà dans la réponse. Alors, pourquoi cela ne peut-il pas être exact?
Jon Skeet
41

En général, EqualityComparer<T>.Default.Equalsdevrait faire le travail avec tout ce qui met en œuvre IEquatable<T>, ou qui a un sensEquals implémentation .

Si, cependant, ==et Equalssont mis en œuvre différemment pour une raison quelconque, alors mon travail sur les opérateurs génériques devrait être utile; il prend en charge les versions opérateur de (entre autres):

  • Égal (valeur T1, valeur T2)
  • NotEqual (valeur T1, valeur T2)
  • GreaterThan (valeur T1, valeur T2)
  • LessThan (valeur T1, valeur T2)
  • GreaterThanOrEqual (valeur T1, valeur T2)
  • LessThanOrEqual (valeur T1, valeur T2)
Marc Gravell
la source
Bibliothèque très intéressante! :) (Note latérale: Puis-je suggérer d'utiliser le lien vers www.yoda.arachsys.com, car la pobox a été bloquée par le pare-feu sur mon lieu de travail? Il est possible que d'autres soient confrontés au même problème.)
Hosam Aly
L'idée est que pobox.com/~skeet va toujours pointer vers mon site - même si elle se déplace ailleurs. J'ai tendance à publier des liens via pobox.com pour la postérité - mais vous pouvez actuellement remplacer yoda.arachsys.com à la place.
Jon Skeet
Le problème avec pobox.com est qu'il s'agit d'un service de messagerie électronique sur le Web (ou du moins le pare-feu de l'entreprise le dit), il est donc bloqué. C'est pourquoi je n'ai pas pu suivre son lien.
Hosam Aly du
"Si, cependant, == et Equals sont implémentés différemment pour une raison quelconque" - Holy fume! Mais quoi! Peut-être que j'ai juste besoin de voir un cas d'utilisation du contraire, mais une bibliothèque avec une sémantique égale et divergente rencontrera probablement des problèmes plus importants que des problèmes avec les génériques.
Edward Brey
@EdwardBrey vous ne vous trompez pas; ce serait bien si le compilateur pouvait appliquer cela, mais ...
Marc Gravell
31

Autant de réponses, et pas une seule n'explique le POURQUOI? (que Giovanni a explicitement demandé) ...

Les génériques .NET n'agissent pas comme des modèles C ++. Dans les modèles C ++, la résolution de surcharge se produit une fois que les paramètres réels du modèle sont connus.

Dans les génériques .NET (y compris C #), la résolution de surcharge se produit sans connaître les paramètres génériques réels. Les seules informations que le compilateur peut utiliser pour choisir la fonction à appeler proviennent des contraintes de type sur les paramètres génériques.

Ben Voigt
la source
2
mais pourquoi le compilateur ne peut-il pas les traiter comme un objet générique? après tout ==fonctionne pour tous les types, que ce soit des types de référence ou des types de valeur. Telle devrait être la question à laquelle je ne pense pas que vous ayez répondu.
nawfal
4
@nawfal: En fait non, ==ne fonctionne pas pour tous les types de valeur. Plus important encore, il n'a pas la même signification pour tous les types, donc le compilateur ne sait pas quoi en faire.
Ben Voigt
1
Ben, oh oui j'ai raté les structures personnalisées que nous pouvons créer sans aucune ==. Pouvez-vous également inclure cette partie dans votre réponse, car je suppose que c'est le point principal ici
nawfal
12

La compilation ne peut pas savoir que T ne peut pas être un struct (type de valeur). Il faut donc lui dire que ça ne peut être que de type référence je pense:

bool Compare<T>(T x, T y) where T : class { return x == y; }

C'est parce que si T pourrait être un type de valeur, il pourrait y avoir des cas où il x == yserait mal formé - dans les cas où un type n'a pas d'opérateur == défini. La même chose se produira pour ce qui est plus évident:

void CallFoo<T>(T x) { x.foo(); }

Cela échoue également, car vous pourriez passer un type T qui n'aurait pas de fonction foo. C # vous oblige à vous assurer que tous les types possibles ont toujours une fonction foo. Cela se fait par la clause where.

Johannes Schaub - litb
la source
1
Merci pour la clarification. Je ne savais pas que les types de valeur ne prenaient pas en charge l'opérateur == hors de la boîte.
Hosam Aly
1
Hosam, j'ai testé avec gmcs (mono), et il compare toujours les références. (c'est-à-dire qu'il n'utilise pas d'opérateur défini en option == pour T)
Johannes Schaub - litb
Il y a une mise en garde avec cette solution: l'opérateur == ne peut pas être surchargé; voir cette question StackOverflow .
Dimitri C.
8

Il semble que sans la contrainte de classe:

bool Compare<T> (T x, T y) where T: class
{
    return x == y;
}

Il faut se rendre compte que tandis que classcontraint Equalsdans l' ==opérateur hérite de Object.Equals, tandis que celui d'une structure remplaceValueType.Equals .

Notez que:

bool Compare<T> (T x, T y) where T: struct
{
    return x == y;
}

donne également la même erreur de compilation.

Pour l'instant, je ne comprends pas pourquoi le compilateur rejette une comparaison d'opérateurs d'égalité de type valeur. Je sais cependant que cela fonctionne:

bool Compare<T> (T x, T y)
{
    return x.Equals(y);
}
Jon Limjap
la source
tu sais que je suis un c # noob total. mais je pense que cela échoue parce que le compilateur ne sait pas quoi faire. comme T n'est pas encore connu, ce qui est fait dépend du type T si les types de valeur sont autorisés. pour les références, les références sont simplement comparées indépendamment de T. si vous faites .Equals, alors .Equal est simplement appelé.
Johannes Schaub - litb
mais si vous faites == sur un type de valeur, le type de valeur ne doit pas nécessairement implémenter cet opérateur.
Johannes Schaub - litb
Cela aurait du sens, litb :) Il est possible que les structures définies par l'utilisateur ne surchargent pas ==, d'où l'échec du compilateur.
Jon Limjap
2
La première méthode ne compare pas utiliser , Object.Equalsmais les tests à la place l' égalité de référence. Par exemple, Compare("0", 0.ToString())retournerait false, car les arguments seraient des références à des chaînes distinctes, qui ont toutes deux un zéro comme seul caractère.
supercat
1
Gotcha mineur sur ce dernier - vous ne l'avez pas limité aux structures, donc un NullReferenceExceptionpourrait se produire.
Flynn1179
6

Eh bien dans mon cas, je voulais tester unitairement l'opérateur d'égalité. J'avais besoin d'appeler le code sous les opérateurs d'égalité sans définir explicitement le type générique. Conseils pour le EqualityComparersont pas utiles comme EqualityComparerappeléEquals méthode mais pas pour l'opérateur d'égalité.

Voici comment j'ai pu travailler avec des types génériques en créant un fichier LINQ. Il appelle le bon code pour ==et les !=opérateurs:

/// <summary>
/// Gets the result of "a == b"
/// </summary>
public bool GetEqualityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.Equal(paramA, paramB);
    // compile it
    var invokeEqualityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeEqualityOperator(a, b);
}

/// <summary>
/// Gets the result of "a =! b"
/// </summary>
public bool GetInequalityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.NotEqual(paramA, paramB);
    // compile it
    var invokeInequalityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeInequalityOperator(a, b);
}
U. Bulle
la source
4

Il existe une entrée MSDN Connect pour ce ici

La réponse d'Alex Turner commence par:

Malheureusement, ce comportement est inhérent à la conception et il n'y a pas de solution simple pour activer l'utilisation de == avec des paramètres de type qui peuvent contenir des types de valeur.

Recep
la source
4

Si vous voulez vous assurer que les opérateurs de votre type personnalisé sont appelés, vous pouvez le faire via la réflexion. Obtenez simplement le type en utilisant votre paramètre générique et récupérez le MethodInfo pour l'opérateur souhaité (par exemple op_Equality, op_Inequality, op_LessThan ...).

var methodInfo = typeof (T).GetMethod("op_Equality", 
                             BindingFlags.Static | BindingFlags.Public);    

Exécutez ensuite l'opérateur à l'aide de la méthode Invoke de MethodInfo et passez les objets en tant que paramètres.

var result = (bool) methodInfo.Invoke(null, new object[] { object1, object2});

Cela invoquera votre opérateur surchargé et non celui défini par les contraintes appliquées au paramètre générique. Cela pourrait ne pas être pratique, mais pourrait s'avérer utile pour tester vos opérateurs en unité lors de l'utilisation d'une classe de base générique qui contient quelques tests.

Christophe
la source
3

J'ai écrit la fonction suivante en regardant la dernière msdn. Il peut facilement comparer deux objets xet y:

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}
Charlie
la source
4
Vous pouvez vous débarrasser de vos booléens et écrirereturn ((IComparable)(x)).CompareTo(y) <= 0;
aloisdg passe à codidact.com le
1

bool Compare(T x, T y) where T : class { return x == y; }

Ce qui précède fonctionnera car == est pris en charge dans le cas de types de référence définis par l'utilisateur.
Dans le cas des types de valeur, == peut être remplacé. Dans ce cas, "! =" Doit également être défini.

Je pense que cela pourrait être la raison, cela interdit la comparaison générique en utilisant "==".

shahkalpesh
la source
2
Merci. Je pense que les types de référence peuvent également remplacer l'opérateur. Mais la raison de l'échec est maintenant claire.
Hosam Aly
1
Le ==jeton est utilisé pour deux opérateurs différents. Si, pour les types d'opérandes donnés, il existe une surcharge compatible de l'opérateur d'égalité, cette surcharge sera utilisée. Sinon, si les deux opérandes sont des types de référence compatibles entre eux, une comparaison de référence sera utilisée. Notez que dans la Compareméthode ci-dessus, le compilateur ne peut pas dire que la première signification s'applique, mais peut dire que la deuxième signification s'applique, donc le ==jeton utilisera cette dernière même s'il Tsurcharge l'opérateur de vérification d'égalité (par exemple s'il est de type String) .
supercat
0

Le .Equals()travail pour moi TKeyest un type générique.

public virtual TOutputDto GetOne(TKey id)
{
    var entity =
        _unitOfWork.BaseRepository
            .FindByCondition(x => 
                !x.IsDelete && 
                x.Id.Equals(id))
            .SingleOrDefault();


    // ...
}
Masoud Darvishian
la source
Ce n'est x.Id.Equalspas ça id.Equals. Vraisemblablement, le compilateur sait quelque chose sur le type de x.
Hosam Aly