Confronto nullo o predefinito dell'argomento generico in C #


288

Ho un metodo generico definito in questo modo:

public void MyMethod<T>(T myArgument)

La prima cosa che voglio fare è verificare se il valore di myArgument è il valore predefinito per quel tipo, qualcosa del genere:

if (myArgument == default(T))

Ma questo non viene compilato perché non ho garantito che T implementerà l'operatore ==. Quindi ho cambiato il codice in questo:

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

Ora questo viene compilato, ma fallirà se myArgument è null, che fa parte di ciò per cui sto testando. Posso aggiungere un controllo null esplicito come questo:

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

Ora questo mi sembra ridondante. ReSharper sta anche suggerendo di cambiare la parte myArgument == null in myArgument == default (T) che è da dove ho iniziato. C'è un modo migliore per risolvere questo problema?

Devo supportare sia i tipi di riferimenti che i tipi di valore.


C # ora supporta Null Conditional Operators , che è zucchero sintatico per l'ultimo esempio che dai. Il tuo codice diventerebbe if (myArgument?.Equals( default(T) ) != null ).
wizard07KSU

1
@ wizard07KSU Questo non funziona per i tipi di valore, vale a dire che viene valutato truein ogni caso perché Equalsverrà sempre chiamato per i tipi di valore poiché myArgumentnon può essere nullin questo caso e il risultato di Equals(un valore booleano) non lo sarà mai null.
diaspro

Quasi duplicato altrettanto prezioso (quindi non votare per chiudere): l' operatore == non può essere applicato a tipi generici in C #?
GSerg

Risposte:


583

Per evitare il pugilato, il modo migliore per confrontare i generici per l'uguaglianza è con EqualityComparer<T>.Default. Questo rispetta IEquatable<T>(senza boxe) object.Equalse gestisce tutte le Nullable<T>sfumature "sollevate". Quindi:

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

Questo corrisponderà:

  • null per le classi
  • null (vuoto) per Nullable<T>
  • zero / false / etc per altre strutture

28
Wow, deliziosamente oscuro! Questo è sicuramente il modo di procedere, complimenti.
Nick Farina,

1
Sicuramente la migliore risposta. Nessuna riga irregolare nel mio codice dopo la riscrittura per utilizzare questa soluzione.
Nathan Ridley,

13
Bella risposta! Ancora meglio sta aggiungendo un metodo di estensione per questa riga di codice in modo che tu possa andare obj.IsDefaultForType ()
rikoe

2
@nawfal nel caso di Person, p1.Equals(p2)dipenderebbe se implementasse sull'API IEquatable<Person>pubblica o tramite un'implementazione esplicita - cioè il compilatore può vedere un Equals(Person other)metodo pubblico . Però; in generici , lo stesso IL è usato per tutti T; uno T1che capita di implementare IEquatable<T1>deve essere trattato in modo identico a uno T2che non lo fa - quindi no, non individuerà un Equals(T1 other)metodo, anche se esiste in fase di esecuzione. In entrambi i casi, c'è anche nullda pensare (a entrambi gli oggetti). Quindi, con generics, userei il codice che ho pubblicato.
Marc Gravell

5
Non riesco a decidere se questa risposta mi ha allontanato o avvicinato alla follia. +1
Steven Liekens,

118

Cosa ne pensi di questo:

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

L'uso del static object.Equals()metodo evita la necessità di eseguire il nullcontrollo da soli. Qualificare esplicitamente la chiamata con object.probabilmente non è necessario a seconda del contesto, ma normalmente prefisso le staticchiamate con il nome del tipo solo per rendere il codice più solubile.


2
Puoi persino rilasciare "l'oggetto". parte poiché è ridondante. if (uguale a (myArgument, default (T)))
Stefan Moser,

13
È vero, normalmente lo è, ma potrebbe non dipendere dal contesto. Potrebbe esserci un metodo Equals () che accetta due argomenti. Tendo a mettere esplicitamente in prefisso tutte le chiamate statiche con il nome della classe, anche solo per rendere il codice più facile da leggere.
Kent Boogaart,

8
È necessario notare che causerà la boxe e in alcuni casi potrebbe essere importante
nightcoder

2
Per me questo non funziona quando si usano numeri interi già inscatolati. Perché sarà quindi un oggetto e il valore predefinito per l'oggetto è null invece di 0.
riezebosch

28

Sono stato in grado di individuare un articolo di Microsoft Connect che discute questo problema in dettaglio:

Sfortunatamente, questo comportamento è in base alla progettazione e non esiste una soluzione semplice per consentire l'utilizzo con parametri di tipo che possono contenere tipi di valore.

Se i tipi sono noti come tipi di riferimento, il sovraccarico predefinito definito sull'oggetto verifica le variabili per l'uguaglianza di riferimento, sebbene un tipo possa specificare il proprio sovraccarico personalizzato. Il compilatore determina quale sovraccarico utilizzare in base al tipo statico della variabile (la determinazione non è polimorfica). Pertanto, se si modifica l'esempio per vincolare il parametro di tipo generico T a un tipo di riferimento non sigillato (come Eccezione), il compilatore può determinare il sovraccarico specifico da utilizzare e verrà compilato il codice seguente:

public class Test<T> where T : Exception

Se i tipi sono noti per essere tipi di valore, esegue test di uguaglianza di valore specifici in base ai tipi esatti utilizzati. Non esiste un buon confronto "predefinito" qui poiché i confronti di riferimento non sono significativi sui tipi di valore e il compilatore non può sapere quale confronto di valori specifici emettere. Il compilatore potrebbe emettere una chiamata a ValueType.Equals (Object) ma questo metodo utilizza la riflessione ed è abbastanza inefficiente rispetto ai confronti di valori specifici. Pertanto, anche se dovessi specificare un vincolo del tipo di valore su T, non c'è nulla di ragionevole da generare qui per il compilatore:

public class Test<T> where T : struct

Nel caso presentato, in cui il compilatore non sa nemmeno se T è un valore o un tipo di riferimento, allo stesso modo non c'è nulla da generare che sia valido per tutti i tipi possibili. Un confronto di riferimento non sarebbe valido per i tipi di valore e una sorta di confronto di valori non sarebbe previsto per i tipi di riferimento che non sovraccaricano.

Ecco cosa puoi fare...

Ho verificato che entrambi questi metodi funzionano per un confronto generico di tipi di riferimento e valore:

object.Equals(param, default(T))

o

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

Per fare confronti con l'operatore "==" dovrai utilizzare uno di questi metodi:

Se tutti i casi di T derivano da una classe base nota, è possibile informare il compilatore utilizzando restrizioni di tipo generico.

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

Il compilatore riconosce quindi come eseguire le operazioni MyBasee non genererà l'errore "Operatore '==' non può essere applicato agli operandi di tipo 'T' e 'T'" che stai vedendo ora.

Un'altra opzione sarebbe quella di limitare T a qualsiasi tipo che implementa IComparable.

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

E quindi utilizzare il CompareTometodo definito dall'interfaccia IComparable .


4
"questo comportamento è in base alla progettazione e non esiste una soluzione semplice per consentire l'utilizzo di parametri di tipo che possono contenere tipi di valore." In realtà Microsoft ha torto. C'è una soluzione semplice: MS dovrebbe estendere il codice operativo ceq per funzionare su tipi di valore come operatore bit a bit. Quindi potrebbero fornire un intrinseco che utilizza semplicemente questo codice operativo, ad esempio object.BitwiseOrReferenceEquals <T> (valore, default (T)) che utilizza semplicemente ceq. Sia per il valore che per i tipi di riferimento, verificherebbe l'uguaglianza bit a bit del valore (ma per i tipi di riferimento, l'uguaglianza bit a bit di riferimento è uguale a object.ReferenceEquals)
Qwertie

1
Penso che il collegamento Microsoft Connect che volevi fosse connect.microsoft.com/VisualStudio/feedback/details/304501/…
Qwertie

18

Prova questo:

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

che dovrebbe compilare e fare ciò che vuoi.


<code> default (T) </code> non è ridondante? <code> EqualityComparer <T> .Default.Equals (myArgument) </code> dovrebbe fare il trucco.
Joshcodes,

2
1) l'hai provato, e 2) a cosa stai confrontando, l'oggetto comparatore? Il Equalsmetodo di IEqualityCompareraccetta due argomenti, i due oggetti da confrontare, quindi no, non è ridondante.
Lasse V. Karlsen,

Questo è ancora meglio della risposta accettata IMHO perché gestisce boxe / unboxing e altri tipi. Vedere questo "chiuso" babbeo risposta domande: stackoverflow.com/a/864860/210780
ashes999

7

(Modificato)

Marc Gravell ha la risposta migliore, ma volevo pubblicare un semplice frammento di codice su cui ho lavorato per dimostrarlo. Basta eseguirlo in una semplice app 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();
}

Ancora una cosa: qualcuno con VS2008 può provare questo come metodo di estensione? Sono bloccato con il 2005 qui e sono curioso di vedere se ciò sarebbe permesso.


Modifica: ecco come farlo funzionare come metodo di estensione:

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));
    }
}

3
"Funziona" come metodo di estensione. Il che è interessante poiché funziona anche se dici o.IsDefault <oggetto> () quando o è nullo. Scary =)
Nick Farina,

6

Per gestire tutti i tipi di T, incluso dove T è un tipo primitivo, dovrai compilare in entrambi i metodi di confronto:

    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
Si noti che la funzione è stata modificata per accettare Func <T> e restituire T, che penso sia stato accidentalmente omesso dal codice dell'interrogante.
Nick Farina,

Sembra che ReSharper stia scherzando con me. Non ha realizzato il suo avviso su un possibile confronto tra un tipo di valore e null non era un avviso del compilatore.
Nathan Ridley,

2
FYI: Se T risulta essere un tipo di valore, il confronto con null verrà trattato come sempre falso dal jitter.
Eric Lippert,

Ha senso: il runtime confronterà un puntatore con un tipo di valore. Il controllo Equals () funziona in quel caso comunque (interessante, dal momento che sembra un linguaggio molto dinamico dire 5.Equals (4) che compila).
Nick Farina,

2
Vedi la risposta EqualityComparer <T> per un'alternativa che non prevede la boxe et
Marc Gravell

2

Ci sarà un problema qui -

Se permetti che funzioni per qualsiasi tipo, default (T) sarà sempre nullo per i tipi di riferimento e 0 (o struttura piena di 0) per i tipi di valore.

Questo non è probabilmente il comportamento che stai cercando, però. Se vuoi che funzioni in modo generico, probabilmente devi usare la riflessione per controllare il tipo di T e gestire tipi di valore diversi dai tipi di riferimento.

In alternativa, è possibile inserire un vincolo di interfaccia su questo e l'interfaccia potrebbe fornire un modo per verificare il valore predefinito della classe / struttura.


1

Penso che probabilmente avrai bisogno di dividere questa logica in due parti e controllare prima 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;
}

Nel metodo IsNull, facciamo affidamento sul fatto che gli oggetti ValueType non possono essere nulli per definizione, quindi se il valore risulta essere una classe che deriva da ValueType, sappiamo già che non è null. D'altra parte, se non è un tipo di valore, allora possiamo semplicemente confrontare il cast di valori con un oggetto con null. Potremmo evitare il controllo su ValueType andando direttamente a un cast per l'oggetto, ma ciò significherebbe che un tipo di valore verrebbe inscatolato, cosa che probabilmente vorremmo evitare poiché implica che un nuovo oggetto viene creato sull'heap.

Nel metodo IsNullOrEmpty, stiamo verificando il caso speciale di una stringa. Per tutti gli altri tipi, stiamo confrontando il valore (che già sa non è nullo) con il suo valore predefinito che per tutti i tipi di riferimento è nullo e per i tipi di valore è di solito una forma di zero (se sono integrali).

Utilizzando questi metodi, il codice seguente si comporta come ci si potrebbe aspettare:

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

Metodo di estensione basato sulla risposta accettata.

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

Uso:

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

Alternare con null per semplificare:

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

Uso:

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

0

Io uso:

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

-1

Non so se funziona con i tuoi requisiti oppure no, ma potresti vincolare T ad essere un Tipo che implementa un'interfaccia come IComparable e quindi utilizzare il metodo ComparesTo () da quell'interfaccia (che IIRC supporta / gestisce i null) come questo :

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

Probabilmente ci sono altre interfacce che potresti usare come IEquitable, ecc.


OP è preoccupato per NullReferenceException e tu lo stai garantendo.
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'operatore '==' non può essere applicato agli operandi di tipo 'T' e 'T'

Non riesco a pensare a un modo per farlo senza l'esplicito test nullo seguito dall'invocazione del metodo o dell'oggetto Equals.Equals come suggerito sopra.

Puoi escogitare una soluzione usando System.Comparison, ma in realtà finirà con molte più righe di codice e aumenterà sostanzialmente la complessità.


-3

Penso che tu fossi vicino.

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

Ora questo viene compilato, ma fallirà se myArgumentè null, che fa parte di ciò per cui sto testando. Posso aggiungere un controllo null esplicito come questo:

Hai solo bisogno di invertire l'oggetto su cui viene chiamato l'equals per un elegante approccio null-safe.

default(T).Equals(myArgument);

Stavo pensando esattamente la stessa cosa.
Chris Gessler,

6
il valore predefinito (T) di un tipo di riferimento è null e genera una NullReferenceException garantita.
Stefan Steinegger,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.