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

288

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 nulle explicite comme ceci:

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

Maintenant, cela me semble redondant. ReSharper suggère même que je change la partie nulle de myArgument == en myArgument == default (T), là où j'ai commencé. Existe-t-il une meilleure façon de résoudre ce problème?

Je dois prendre en charge les types de références et les types de valeurs.

Stefan Moser
la source
C # prend désormais en charge les opérateurs conditionnels Null , qui est du sucre syntaxique pour le dernier exemple que vous donnez. Votre code deviendrait if (myArgument?.Equals( default(T) ) != null ).
wizard07KSU
1
@ wizard07KSU Cela ne fonctionne pas pour les types de valeur, c'est-à-dire qu'il est évalué truedans tous les cas parce que Equalssera toujours appelé pour les types de valeur car myArgumentne peut pas être nulldans ce cas et le résultat de Equals(un booléen) ne le sera jamais null.
jaspe
Également valable presque en double (donc ne pas voter pour fermer): L'opérateur == ne peut-il pas être appliqué aux types génériques en C #?
GSerg

Réponses:

583

Pour éviter la boxe, 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 gère toutes les Nullable<T>nuances "levées". Par conséquent:

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

Cela correspondra à:

  • null pour les classes
  • null (vide) pour Nullable<T>
  • zéro / faux / etc pour les autres structures
Marc Gravell
la source
28
Wow, c'est délicieusement obscur! C'est certainement la voie à suivre, bravo.
Nick Farina
1
Certainement la meilleure réponse. Aucune ligne ondulée dans mon code après la réécriture pour utiliser cette solution.
Nathan Ridley
13
Très bonne réponse! Encore mieux est d'ajouter une méthode d'extension pour cette ligne de code afin que vous puissiez aller obj.IsDefaultForType ()
rikoe
2
@nawfal dans le cas de Person, p1.Equals(p2)dépendrait de l'implémentation IEquatable<Person>sur l'API publique, ou via une implémentation explicite - c'est-à-dire que le compilateur peut voir une Equals(Person other)méthode publique . Toutefois; chez les génériques , le même IL est utilisé pour tous T; un T1qui arrive à implémenter IEquatable<T1>doit être traité de manière identique à un T2qui ne le fait pas - donc non, il ne repérera pas une Equals(T1 other)méthode, même si elle existe au moment de l'exécution. Dans les deux cas, il faut aussi nullpenser à (l'un ou l'autre objet). Donc, avec des génériques, j'utiliserais le code que j'ai publié.
Marc Gravell
5
Je ne peux pas décider si cette réponse m'a éloigné ou plus proche de la folie. +1
Steven Liekens
118

Que dis-tu de ça:

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

L'utilisation de la static object.Equals()méthode vous évite d'avoir à effectuer vous-même la nullvérification. Il object.n'est probablement pas nécessaire de qualifier explicitement l'appel avec votre contexte, mais je préfixe normalement les staticappels avec le nom du type juste pour rendre le code plus soluble.

Kent Boogaart
la source
2
Vous pouvez même déposer «l'objet». partie car il est redondant. if (Equals (myArgument, default (T)))
Stefan Moser
13
Certes, c'est normalement, mais peut ne pas dépendre du contexte. Il peut y avoir une méthode Instals Equals () qui accepte 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.
Kent Boogaart,
8
Besoin de noter que cela entraînera la boxe et dans certains cas, cela peut être important
nightcoder
2
Pour moi, cela ne fonctionne pas lorsque vous utilisez des nombres entiers déjà encadrés. Parce que ce sera alors un objet et la valeur par défaut pour object est null au lieu de 0.
riezebosch
28

J'ai pu localiser un article Microsoft Connect qui traite de ce problème en détail:

Malheureusement, ce comportement est inhérent à la conception et il n'y a pas de solution facile pour permettre l'utilisation de paramètres de type pouvant contenir des types de valeur.

Si les types sont connus pour être des types de référence, la surcharge par défaut de défini sur l'objet 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 compile:

public class Test<T> where T : Exception

Si les types sont connus pour être des types de valeur, effectue des tests spécifiques d'égalité de valeur en fonction des types exacts utilisés. Il n'y a pas de bonne comparaison "par défaut" ici car les comparaisons de référence ne sont pas significatives sur les types de valeur et le compilateur ne peut pas savoir quelle comparaison de valeur spécifique émettre. Le compilateur peut é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 valeur sur T, il n'y a rien de raisonnable à générer ici par le compilateur:

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 une valeur ou un type de référence, il n'y a de même rien à générer qui serait valable pour tous les types possibles. Une comparaison de référence ne serait pas valide pour les types de valeur et une sorte de comparaison de valeur serait inattendue pour les types de 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 faire des comparaisons avec l'opérateur "==", vous devrez utiliser l'une de ces méthodes:

Si tous les cas de T dérivent d'une classe de base connue, vous pouvez informer le 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 lancera pas l'erreur "Operator '==' ne peut pas être appliquée aux opérandes de type" T "et" T "" que vous voyez maintenant.

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

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

Et puis utilisez la CompareTométhode définie par l' interface IComparable .

Eric Schoonover
la source
4
"ce comportement est inhérent à la conception et il n'y a pas de solution facile pour permettre l'utilisation de paramètres de type pouvant contenir des types de valeur." En fait, Microsoft a tort. Il existe une solution simple: MS devrait étendre l'opcode ceq pour fonctionner sur les types de valeur en tant qu'opérateur au niveau du bit. Ensuite, ils pourraient fournir un intrinsèque qui utilise simplement cet opcode, par exemple object.BitwiseOrReferenceEquals <T> (valeur, par défaut (T)) qui utilise simplement ceq. Pour les types de valeur et de référence, cela vérifierait l'égalité au niveau du bit de la valeur (mais pour les types de référence, l'égalité au niveau du bit de référence est identique à object.ReferenceEquals)
Qwertie
1
Je pense que le lien Microsoft Connect que vous vouliez était connect.microsoft.com/VisualStudio/feedback/details/304501/…
Qwertie
18

Essaye ça:

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

qui devrait compiler et faire ce que vous voulez.

Lasse V. Karlsen
la source
Le <code> défaut (T) </code> n'est-il pas redondant? <code> EqualityComparer <T> .Default.Equals (myArgument) </code> devrait faire l'affaire.
Joshcodes
2
1) l'avez-vous essayé, et 2) à quoi comparez-vous ensuite, l'objet comparateur? La Equalsméthode de IEqualityComparerprend deux arguments, les deux objets à comparer, donc non, ce n'est pas redondant.
Lasse V. Karlsen
C'est encore mieux que la réponse acceptée à mon humble avis car elle gère la boxe / unboxing et d'autres types. Voir cette réponse aux questions «fermé comme dupe»: stackoverflow.com/a/864860/210780
ashes999
7

(Édité)

Marc Gravell a la meilleure réponse, mais je voulais publier un simple extrait de code que j'ai élaboré pour le démontrer. Exécutez simplement ceci dans une simple application console C #:

public static class TypeHelper<T>
{
    public static bool IsDefault(T val)
    {
         return EqualityComparer<T>.Default.Equals(obj,default(T));
    }
}

static void Main(string[] args)
{
    // value type
    Console.WriteLine(TypeHelper<int>.IsDefault(1)); //False
    Console.WriteLine(TypeHelper<int>.IsDefault(0)); // True

    // reference type
    Console.WriteLine(TypeHelper<string>.IsDefault("test")); //False
    Console.WriteLine(TypeHelper<string>.IsDefault(null)); //True //True

    Console.ReadKey();
}

Une dernière chose: quelqu'un avec VS2008 peut-il essayer cela comme méthode d'extension? Je suis coincé avec 2005 ici et je suis curieux de voir si cela serait autorisé.


Edit: Voici comment le faire fonctionner comme méthode d'extension:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // value type
        Console.WriteLine(1.IsDefault());
        Console.WriteLine(0.IsDefault());

        // reference type
        Console.WriteLine("test".IsDefault());
        // null must be cast to a type
        Console.WriteLine(((String)null).IsDefault());
    }
}

// The type cannot be generic
public static class TypeHelper
{
    // I made the method generic instead
    public static bool IsDefault<T>(this T val)
    {
        return EqualityComparer<T>.Default.Equals(val, default(T));
    }
}
Joel Coehoorn
la source
3
Il fonctionne comme méthode d'extension. Ce qui est intéressant car cela fonctionne même si vous dites o.IsDefault <object> () lorsque o est nul. Effrayant =)
Nick Farina
6

Pour gérer tous les types de T, y compris lorsque T est un type primitif, vous devrez compiler dans 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;
    }
Nick Farina
la source
1
Notez que la fonction a été modifiée pour accepter Func <T> et retourner T, qui, je pense, a été accidentellement omis du code de l'interrogateur.
Nick Farina
On dirait que ReSharper joue avec moi. N'a pas réalisé que son avertissement concernant une comparaison possible entre un type de valeur et null n'était pas un avertissement du compilateur.
Nathan Ridley
2
FYI: Si T se révèle être un type de valeur, la comparaison avec null sera traitée comme toujours fausse par la gigue.
Eric Lippert
Cela a du sens - le runtime comparera un pointeur à un type de valeur. La vérification Equals () fonctionne cependant dans ce cas (intéressant, car il semble très dynamique de dire 5.Equals (4) qui compile).
Nick Farina
2
Voir la réponse EqualityComparer <T> pour une alternative qui n'implique pas la boxe et
Marc Gravell
2

Il va y avoir un problème ici -

Si vous allez autoriser cela à fonctionner pour n'importe quel type, la valeur par défaut (T) sera toujours nulle pour les types de référence et 0 (ou structure pleine de 0) pour les types de valeur.

Ce n'est probablement pas le comportement que vous recherchez, cependant. Si vous souhaitez que cela fonctionne de manière générique, vous devrez probablement utiliser la réflexion pour vérifier le type de T et gérer les types de valeur différents des types de référence.

Alternativement, vous pouvez mettre une contrainte d'interface sur cela, et l'interface pourrait fournir un moyen de vérifier la valeur par défaut de la classe / struct.

Reed Copsey
la source
1

Je pense que vous devez probablement diviser cette logique en deux parties et vérifier d'abord null.

public static bool IsNullOrEmpty<T>(T value)
{
    if (IsNull(value))
    {
        return true;
    }
    if (value is string)
    {
        return string.IsNullOrEmpty(value as string);
    }
    return value.Equals(default(T));
}

public static bool IsNull<T>(T value)
{
    if (value is ValueType)
    {
        return false;
    }
    return null == (object)value;
}

Dans la méthode IsNull, nous nous appuyons sur le fait que les objets ValueType ne peuvent pas être null par définition, donc si value se trouve être une classe qui dérive de ValueType, nous savons déjà que ce n'est pas null. D'un autre côté, s'il ne s'agit pas d'un type de valeur, nous pouvons simplement comparer la valeur cast à un objet par rapport à null. Nous pourrions éviter la vérification par rapport à ValueType en allant directement à un transtypage en objet, mais cela signifierait qu'un type de valeur serait encadré, ce que nous voulons probablement éviter car cela implique qu'un nouvel objet est créé sur le tas.

Dans la méthode IsNullOrEmpty, nous vérifions le cas particulier d'une chaîne. Pour tous les autres types, nous comparons la valeur (qui sait déjà n'est pas nulle) avec sa valeur par défaut qui pour tous les types de référence est nulle et pour les types de valeur est généralement une certaine forme de zéro (si elles sont intégrales).

En utilisant ces méthodes, le code suivant se comporte comme vous pouvez vous y attendre:

class Program
{
    public class MyClass
    {
        public string MyString { get; set; }
    }

    static void Main()
    {
        int  i1 = 1;    Test("i1", i1); // False
        int  i2 = 0;    Test("i2", i2); // True
        int? i3 = 2;    Test("i3", i3); // False
        int? i4 = null; Test("i4", i4); // True

        Console.WriteLine();

        string s1 = "hello";      Test("s1", s1); // False
        string s2 = null;         Test("s2", s2); // True
        string s3 = string.Empty; Test("s3", s3); // True
        string s4 = "";           Test("s4", s4); // True

        Console.WriteLine();

        MyClass mc1 = new MyClass(); Test("mc1", mc1); // False
        MyClass mc2 = null;          Test("mc2", mc2); // True
    }

    public static void Test<T>(string fieldName, T field)
    {
        Console.WriteLine(fieldName + ": " + IsNullOrEmpty(field));
    }

    // public static bool IsNullOrEmpty<T>(T value) ...

    // public static bool IsNull<T>(T value) ...
}
Damian Powell
la source
1

Méthode d'extension basée sur la réponse acceptée.

   public static bool IsDefault<T>(this T inObj)
   {
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Usage:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue == null || tValue.IsDefault()) return false;
   }

Alterner avec null pour simplifier:

   public static bool IsNullOrDefault<T>(this T inObj)
   {
       if (inObj == null) return true;
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Usage:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }
dynamiclynk
la source
0

J'utilise:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}
kofifus
la source
-1

Je ne sais pas si cela fonctionne avec vos besoins ou non, mais vous pouvez contraindre T à être un type qui implémente une interface telle que IComparable, puis utiliser la méthode ComparesTo () à partir de cette interface (que l'IIRC prend en charge / gère les valeurs nulles) comme ceci :

public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))

Il existe probablement d'autres interfaces que vous pourriez également utiliser IEquitable, etc.

caryden
la source
OP s'inquiète de NullReferenceException et vous lui garantissez la même chose.
nawfal
-2

@ilitirit:

public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

L'opérateur '==' ne peut pas être appliqué aux opérandes de type 'T' et 'T'

Je ne peux pas penser à un moyen de le faire sans le test null explicite suivi par l'invocation de la méthode Equals ou de object.Equals comme suggéré ci-dessus.

Vous pouvez concevoir une solution en utilisant System.Comparison, mais cela va finir avec beaucoup plus de lignes de code et augmenter considérablement la complexité.

cfeduke
la source
-3

Je pense que tu étais proche.

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 nulle explicite comme ceci:

Vous avez juste besoin d'inverser l'objet sur lequel l'égal est appelé pour une approche élégante sans danger.

default(T).Equals(myArgument);
Scott McKay
la source
Je pensais exactement la même chose.
Chris Gessler
6
la valeur par défaut (T) d'un type de référence est null et entraîne une exception NullReferenceException garantie.
Stefan Steinegger