Domanda

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ò che sto testando.Posso aggiungere un controllo null esplicito come questo:

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

Ora questo mi sembra ridondante.ReSharper mi suggerisce addirittura di modificare la parte myArgument == null in myArgument == default(T) che è da dove ho iniziato.Esiste un modo migliore per risolvere questo problema?

Ho bisogno di supporto Entrambi tipi di riferimento e tipi di valore.

È stato utile?

Soluzione

Per evitare la boxe, il modo migliore per confrontare i generici in termini di uguaglianza è con EqualityComparer<T>.Default.Questo rispetta IEquatable<T> (senza boxe) così come object.Equals, e gestisce tutti i file 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

Altri suggerimenti

Cosa ne pensi di questo:

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

Usando il static object.Equals() Il metodo evita la necessità di eseguire l'operazione null controllati.Qualificare esplicitamente la chiamata con object. probabilmente non è necessario a seconda del contesto, ma normalmente utilizzo il prefisso static chiama con il nome del tipo solo per rendere il codice più solubile.

Sono riuscito a individuare un Articolo di Microsoft Connect che tratta questo problema in dettaglio:

Sfortunatamente, questo comportamento è legato alla progettazione e non esiste una soluzione semplice per abilitarne l'uso con parametri di tipo che potrebbero contenere tipi di valore.

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

public class Test<T> where T : Exception

Se i tipi sono noti per essere tipi valore, esegue test specifici di uguaglianza dei valori 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 specifico emettere.Il compilatore potrebbe emettere una chiamata a ValueType.Equals(Object) ma questo metodo utilizza la riflessione ed è piuttosto inefficiente rispetto ai confronti di valori specifici.Pertanto, anche se dovessi specificare un vincolo di tipo valore su T, non c'è nulla di ragionevole che il compilatore possa generare qui:

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 dei riferimenti non sarebbe valido per i tipi di valore e una sorta di confronto dei valori sarebbe imprevisto per i tipi di riferimento che non eseguono l'overload.

Ecco cosa puoi fare...

Ho convalidato che entrambi questi metodi funzionano per un confronto generico di tipi di riferimento e di 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 conosciuta, puoi comunicarlo al compilatore utilizzando restrizioni di tipo generiche.

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

Il compilatore riconosce quindi come eseguire operazioni su MyBase e non genererà l'errore "L'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 implementi IComparable.

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

E poi usa il CompareTo metodo definito dall'art Interfaccia comparabile.

Prova questo:

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

che dovrebbe essere compilato e fare quello che vuoi.

(Modificato)

Marc Gravell ha la risposta migliore, ma volevo pubblicare un semplice snippet di codice che ho elaborato 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();
}

Un'altra cosa:qualcuno con VS2008 può provare questo come metodo di estensione?Sono bloccato al 2005 qui e sono curioso di vedere se ciò sarebbe consentito.


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

Per gestire tutti i tipi di T, incluso dove T è un tipo primitivo, dovrai compilare 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;
    }

Ci sarà un problema qui -

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

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

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

Penso che probabilmente dovrai dividere questa logica in due parti e verificare prima la presenza di 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 null per definizione, quindi se value sembra essere una classe che deriva da ValueType, sappiamo già che non è null.D'altra parte, se non è un tipo di valore, possiamo semplicemente confrontare il cast di valore su un oggetto con null.Potremmo evitare il controllo su ValueType andando direttamente a un cast to object, ma ciò significherebbe che un tipo di valore verrebbe inscatolato, cosa che probabilmente vorremmo evitare poiché implica che un nuovo oggetto venga creato nell'heap.

Nel metodo IsNullOrEmpty controlliamo il caso speciale di una stringa.Per tutti gli altri tipi, stiamo confrontando il valore (che già sappiamo è non null) rispetto al suo valore predefinito che per tutti i tipi di riferimento è null e per i tipi di valore è solitamente una qualche forma di zero (se sono integrali).

Utilizzando questi metodi, il codice seguente si comporta come previsto:

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

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

Non so se funziona con i tuoi requisiti o meno, 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 valori null) in questo modo :

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

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

@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 il test null esplicito seguito dall'invocazione del metodo o object.Equals Equals come suggerito sopra.

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

Penso che tu fossi vicino.

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

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

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

default(T).Equals(myArgument);
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top