web-dev-qa-db-fra.com

Comparaison nulle ou par défaut d'un argument générique 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 ==. Alors j'ai changé le code en ceci:

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

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

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

Maintenant, cela me semble superflu. ReSharper suggère même que je modifie la partie myArgument == null en myArgument == default (T), c'est là que j'ai commencé. Existe-t-il un meilleur moyen de résoudre ce problème?

J'ai besoin de soutien tous les deux types de références et types de valeur.

239
Stefan Moser

Pour éviter la boxe, le meilleur moyen de comparer les génériques en termes d'égalité est d'utiliser EqualityComparer<T>.Default. Ceci respecte IEquatable<T> (sans boxe) ainsi que object.Equals et gère toutes les nuances Nullable<T> "soulevées". Par conséquent:

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

Cela correspondra à:

  • null pour les cours
  • null (vide) pour Nullable<T>
  • zero/false/etc pour d'autres structures
486
Marc Gravell

Que dis-tu de ça:

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

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

113
Kent Boogaart

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

Malheureusement, ce problème est inhérent au projet et il n’existe pas de solution simple 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 des variables de test d'objet définies 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 valeur, effectue des tests d’égalité de valeur spécifiques en fonction des types exacts utilisés. Il n’ya pas de bonne comparaison "par défaut" ici car les comparaisons de références 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, 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 de valeur ou de référence, il n’existe de même aucune information à 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 inattendu pour les types de référence qui ne sont pas surchargés.

Voici ce que vous pouvez faire ...

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

object.Equals(param, default(T))

ou

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

Pour faire des comparaisons avec l'opérateur "==", vous devez utiliser l'une des méthodes suivantes:

Si tous les cas de T proviennent d'une classe de base connue, vous pouvez en informer le compilateur à l'aide de 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 lève pas le message "Opérateur" == "ne peut pas être appliqué aux opérandes de type" T "et" T "" que vous voyez actuellement.

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

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

Et utilisez ensuite la méthode CompareTo définie par l'interface IComparable .

24
Eric Schoonover

Essaye ça:

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

cela devrait compiler et faire ce que vous voulez.

18

(Édité)

Marc Gravell a la meilleure réponse, mais je voulais poster un extrait de code simple que j'ai travaillé pour le démontrer. Il suffit de lancer 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 ceci 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 une 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));
    }
}
7
Joel Coehoorn

Pour gérer tous les types de T, y compris lorsque T est un type primitif, vous devez 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;
    }
6
Nick Farina

Il va y avoir un problème ici -

Si vous permettez que cela fonctionne pour tout type, default (T) sera toujours nul pour les types de référence et 0 (ou une structure remplie 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 des types de valeur différents des types de référence.

Alternativement, vous pouvez y appliquer une contrainte d'interface, qui pourrait également permettre de vérifier le comportement par défaut de la classe/struct.

2
Reed Copsey

Je pense que vous devez probablement scinder cette logique en deux parties et vérifier d’abord la valeur 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 basons sur le fait que les objets ValueType ne peuvent pas être null par définition. Par conséquent, si value est une classe dérivée 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 convertie à un objet avec la valeur null. Nous pourrions éviter la vérification avec ValueType en allant directement au transtypage en objet, mais cela voudrait dire qu'un type de valeur serait mis en boîte, ce que nous souhaitons 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à que c'est pas null) à sa valeur par défaut qui, pour tous les types de référence, est null et que les types de valeur sont généralement une forme de zéro (s'ils ).

En utilisant ces méthodes, le code suivant se comporte comme prévu:

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) ...
}
1
Damian Powell

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;
  }
}
0
kofifus