it-swarm-eu.dev

Confronto null o predefinito di argomento generico in C #

Ho un metodo generico definito in questo modo:

public void MyMethod<T>(T myArgument)

La prima cosa che voglio fare è controllare 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 con questo:

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

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

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

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

Ho bisogno di supporto tutti e due tipi di riferimento e tipi di valore.

239
Stefan Moser

Per evitare il pugilato, il modo migliore per confrontare i generici per l'uguaglianza è con EqualityComparer<T>.Default. Ciò rispetta IEquatable<T> (senza boxing) e object.Equals, e gestisce tutte le sfumature Nullable<T> "revocate". 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
486
Marc Gravell

Cosa ne pensi di questo:

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

L'uso del metodo static object.Equals() evita la necessità che tu faccia il null check yourself. La qualificazione esplicita della chiamata con object. probabilmente non è necessaria a seconda del contesto, ma normalmente prefisso static chiamate con il nome del tipo solo per rendere il codice più solubile.

113
Kent Boogaart

Sono stato in grado di individuare un articolo di Microsoft Connect che tratta questo problema in modo dettagliato:

Sfortunatamente, questo comportamento è di progettazione e non esiste una soluzione semplice per abilitare l'uso di parametri di tipo che possono contenere tipi di valore.

Se i tipi sono noti per essere 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 sealed (ad esempio Exception), il compilatore può determinare il sovraccarico specifico da utilizzare e il seguente codice verrà compilato:

public class Test<T> where T : Exception

Se i tipi sono noti per essere tipi di valore, esegue test di uguaglianza di valore specifici basati sui tipi esatti utilizzati. Non esiste un buon confronto "predefinito" in quanto i confronti di riferimento non sono significativi per i 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 reflection 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 da generare qui per il compilatore:

public class Test<T> where T : struct

Nel caso che hai presentato, dove il compilatore non sa nemmeno se T è un valore o un tipo di riferimento, non c'è nulla da generare che sarebbe 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 sarebbe inaspettato per tipi di riferimento che non sovraccaricano.

Ecco cosa puoi fare...

Ho convalidato 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, puoi far sapere al compilatore che utilizza restrizioni di tipo generico.

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

Il compilatore riconosce quindi come eseguire le operazioni su MyBase e non lancia "Operator" == "non può essere applicato agli operandi di tipo" T "e" T "" che si stanno verificando ora.

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

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

Quindi utilizzare il metodo CompareTo definito dall'interfaccia IComparable .

24
Eric Schoonover

Prova questo:

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

che dovrebbe compilare e fare ciò che vuoi.

(Modificato)

Marc Gravell ha la migliore risposta, ma volevo pubblicare un semplice frammento di codice che ho elaborato per dimostrarlo. Basta eseguirlo in una semplice app per 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 con il 2005 qui e sono curioso di vedere se 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));
    }
}
7
Joel Coehoorn

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;
    }
6
Nick Farina

Ci sarà un problema qui -

Se vuoi consentire a questo di funzionare per qualsiasi tipo, il valore predefinito (T) sarà sempre nullo per i tipi di riferimento e 0 (o struct completo di 0) per i tipi di valore.

Questo probabilmente non è il comportamento che stai cercando, però. Se si desidera che funzioni in modo generico, è probabilmente necessario utilizzare il reflection per verificare il tipo di T e gestire i 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 l'impostazione predefinita della classe/struct.

2
Reed Copsey

Penso che tu abbia probabilmente 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, stiamo facendo affidamento sul fatto che gli oggetti ValueType non possono essere nulli per definizione, quindi se il valore capita di essere una classe che deriva da ValueType, sappiamo già che non è nullo. D'altra parte, se non è un tipo di valore, possiamo semplicemente confrontare il cast di valore con un oggetto con il valore null. Potremmo evitare il controllo contro ValueType andando direttamente a un cast per oggetto, ma ciò significherebbe che un tipo di valore verrebbe inserito in una scatola che è qualcosa 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, confrontiamo il valore (che già conosco è not null) rispetto al suo valore predefinito che per tutti i tipi di riferimento è nullo e per i tipi di valore è solitamente una forma zero (se sono interi ).

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
Damian Powell

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