302 votes

Comparaison d'un argument générique par défaut ou nul en C#

J'ai une méthode générique définie comme ceci :

public void MyMethod<T>(T myArgument)

La première chose que je veux faire est de vérifier si la valeur de myArgument est la valeur par défaut pour ce type, quelque chose comme ceci :

if (myArgument == default(T))

Mais cela ne compile pas car je n'ai pas garanti que T implémentera l'opérateur ==. J'ai donc changé le code en ceci :

if (myArgument.Equals(default(T)))

Maintenant, cela compile, mais échouera si myArgument est nul, ce qui fait partie de ce que je teste. Je peux ajouter une vérification explicite de null comme ceci :

if (myArgument == null || myArgument.Equals(default(T)))

Maintenant, ça me semble redondant. ReSharper me suggère même de changer la partie myArgument == null en myArgument == default(T), ce qui est le point de départ. Existe-t-il une meilleure façon de résoudre ce problème ?

Je dois soutenir les deux les types de références et les types de valeurs.

0 votes

C# prend désormais en charge Opérateurs conditionnels nuls qui est un sucre syntaxique pour le dernier exemple que vous donnez. Votre code deviendrait if (myArgument?.Equals( default(T) ) != null ) .

1 votes

@wizard07KSU Cela ne fonctionne pas pour les types de valeurs, c'est-à-dire que l'évaluation est égale à true en tout cas parce que Equals sera toujours appelé pour les types de valeurs puisque myArgument ne peut être null dans ce cas et le résultat de Equals (un booléen) ne sera jamais null .

0 votes

Un quasi-duplicata de valeur égale (donc pas de vote de proximité) : L'opérateur == ne peut-il pas être appliqué aux types génériques en C# ?

641voto

Marc Gravell Points 482669

Pour éviter la mise en boîte, la meilleure façon de comparer les génériques pour l'égalité est avec EqualityComparer<T>.Default . Cela respecte IEquatable<T> (sans boxe) ainsi que object.Equals et s'occupe de tous les Nullable<T> des nuances "soulevées". D'où :

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}

Cela correspondra :

  • nul pour les classes
  • null (vide) pour Nullable<T>
  • zero/false/etc pour les autres structs

38 votes

Wow, comme c'est délicieusement obscur ! C'est définitivement la voie à suivre, bravo.

1 votes

C'est définitivement la meilleure réponse. Pas de lignes sinueuses dans mon code après réécriture pour utiliser cette solution.

17 votes

Excellente réponse ! Une solution encore meilleure serait d'ajouter une méthode d'extension pour cette ligne de code afin de pouvoir utiliser obj.IsDefaultForType()

119voto

Kent Boogaart Points 97432

Que dites-vous de ça ?

if (object.Equals(myArgument, default(T)))
{
    //...
}

Utilisation de la static object.Equals() vous évite d'avoir à effectuer les opérations suivantes null vérifiez vous-même. En qualifiant explicitement l'appel avec object. n'est probablement pas nécessaire en fonction de votre contexte, mais je préfère normalement préfixer static avec le nom du type juste pour rendre le code plus soluble.

2 votes

Vous pouvez même laisser tomber la partie "objet." puisqu'elle est redondante. if (Equals(myArgument, default(T)))

13 votes

C'est vrai, c'est normalement le cas, mais cela peut ne pas l'être en fonction du contexte. Il peut y avoir une instance de la méthode Equals() qui prend deux arguments. J'ai tendance à préfixer explicitement tous les appels statiques avec le nom de la classe, ne serait-ce que pour rendre le code plus facile à lire.

8 votes

Il faut noter que cela provoquera des boxeurs et que dans certains cas, cela peut être important.

29voto

spoon16 Points 17694

J'ai pu trouver un Article sur Microsoft Connect qui traite de cette question en détail :

Malheureusement, ce comportement est dû à la conception et il n'existe pas de solution simple pour permettre l'utilisation de paramètres de type avec qui peuvent contenir des types de valeurs.

Si les types sont connus pour être des types de référence, la surcharge par défaut de defined on object teste les variables pour l'égalité de référence, bien qu'un type puisse spécifier sa propre surcharge personnalisée. Le compilateur détermine la surcharge à utiliser en fonction du type statique de la variable (la détermination n'est pas polymorphe). Par conséquent, si vous modifiez votre exemple pour contraindre le paramètre de type générique T à un type de référence non scellé (tel que Exception), le compilateur peut déterminer la surcharge spécifique à utiliser et le code suivant sera compilé :

public class Test<T> where T : Exception

Si les types sont connus pour être des types de valeur, effectue des tests d'égalité de valeur spécifiques basés sur les types exacts utilisés. Il n'y a pas de bonne comparaison "par défaut" ici puisque les comparaisons de référence n'ont pas de sens sur les types de valeur et que le compilateur ne peut pas savoir quelle comparaison de valeur spécifique émettre. Le compilateur pourrait émettre un appel à ValueType.Equals(Object) mais cette méthode utilise la réflexion et est assez inefficace par rapport aux comparaisons de valeurs spécifiques. Par conséquent, même si vous deviez spécifier une contrainte de type de valeur sur T, le compilateur n'a rien de raisonnable à générer ici :

public class Test<T> where T : struct

Dans le cas que vous avez présenté, où le compilateur ne sait même pas si T est un type valeur ou référence, il n'y a de même rien à générer qui serait valide pour tous les types possibles. Une comparaison de référence ne serait pas valide pour les types valeur et une sorte de comparaison de valeur serait inattendue pour les types référence qui ne surchargent pas.

Voici ce que vous pouvez faire...

J'ai validé que ces deux méthodes fonctionnent pour une comparaison générique des types de référence et de valeur :

object.Equals(param, default(T))

ou

EqualityComparer<T>.Default.Equals(param, default(T))

Pour effectuer des comparaisons avec l'opérateur "==", vous devez utiliser l'une de ces méthodes :

Si tous les cas de T dérivent d'une classe de base connue, vous pouvez le faire savoir au compilateur en utilisant des restrictions de type génériques.

public void MyMethod<T>(T myArgument) where T : MyBase

Le compilateur reconnaît alors comment effectuer des opérations sur MyBase et ne produira pas l'erreur "Operator '==' cannot be applied to operands of type 'T' and 'T'" que vous voyez actuellement.

Une autre option serait de restreindre T à tout type qui implémente IComparable .

public void MyMethod<T>(T myArgument) where T : IComparable

Et ensuite utiliser le CompareTo définie par la méthode Interface IComparable .

5 votes

"ce comportement est de par sa conception et il n'y a pas de solution facile pour permettre l'utilisation de paramètres avec type qui peuvent contenir des types de valeur." En fait, Microsoft a tort. Il existe une solution facile : MS devrait étendre l'opcode ceq pour fonctionner sur les types de valeur comme un opérateur bitwise. Ils pourraient alors fournir un intrinsèque qui utilise simplement cet opcode, par exemple object.BitwiseOrReferenceEquals<T>(value, default(T)) qui utilise simplement ceq. Pour les types valeur et référence, ceci vérifierait l'égalité bitwise de la valeur (mais pour les types de référence, l'égalité binaire des références est identique à object.ReferenceEquals)

1 votes

Je pense que le lien Microsoft Connect que vous vouliez était connect.microsoft.com/VisualStudio/feedback/details/304501/

20voto

Lasse V. Karlsen Points 148037

Essayez ça :

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))

qui devrait compiler, et faire ce que vous voulez.

0 votes

Le <code>default(T)</code> n'est-il pas redondant ? <code>EqualityComparer<T>.Default.Equals(myArgument)</code> devrait faire l'affaire.

2 votes

1) avez-vous essayé, et 2) à quoi comparez-vous alors, l'objet de comparaison ? Le site Equals méthode de IEqualityComparer prend deux arguments, les deux objets à comparer, donc non, il n'est pas redondant.

0 votes

C'est encore mieux que la réponse acceptée IMHO parce qu'elle gère le boxing/unboxing et d'autres types. Voir la réponse à cette question "fermée en tant que doublon" : stackoverflow.com/a/864860/210780

6voto

Nick Farina Points 1608

Pour gérer tous les types de T, y compris lorsque T est un type primitif, vous devrez compiler les deux méthodes de comparaison :

    T Get<T>(Func<T> createObject)
    {
        T obj = createObject();
        if (obj == null || obj.Equals(default(T)))
            return obj;

        // .. do a bunch of stuff
        return obj;
    }

1 votes

Notez que la fonction a été modifiée pour accepter Func<T> et retourner T, ce qui, je pense, a été accidentellement omis dans le code du questionneur.

0 votes

On dirait que ReSharper se fout de moi. Je n'avais pas réalisé que son avertissement sur une éventuelle comparaison entre un type de valeur et null n'était pas un avertissement du compilateur.

2 votes

Pour info : si T s'avère être un type de valeur, la comparaison avec null sera traitée comme toujours fausse par la gigue.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X