Porównanie zerowe lub domyślne ogólnego argumentu w C #

288

Mam ogólną metodę zdefiniowaną w ten sposób:

public void MyMethod<T>(T myArgument)

Pierwszą rzeczą, którą chcę zrobić, jest sprawdzenie, czy wartość myArgument jest wartością domyślną dla tego typu, mniej więcej tak:

if (myArgument == default(T))

Ale to się nie kompiluje, ponieważ nie mam gwarancji, że T zaimplementuje operator ==. Więc zmieniłem kod na ten:

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

Teraz to się kompiluje, ale zakończy się niepowodzeniem, jeśli myArgument ma wartość null, co jest częścią tego, co testuję. Mogę dodać jawny czek zerowy w następujący sposób:

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

Teraz wydaje mi się to zbędne. ReSharper sugeruje nawet, że zmienię część myArgument == null na myArgument == default (T), od której zacząłem. Czy istnieje lepszy sposób na rozwiązanie tego problemu?

Muszę obsługiwać zarówno typy referencji, jak i typy wartości.

Stefan Moser
źródło
C # obsługuje teraz Null Conditional Operators , który jest cukrem syntaktycznym dla ostatniego podanego przykładu. Twój kod stałby się if (myArgument?.Equals( default(T) ) != null ).
wizard07KSU,
1
@ wizard07KSU To nie działa dla typów wartości, tzn. ocenia truew każdym przypadku, ponieważ Equalszawsze będzie wywoływany dla typów wartości, ponieważ myArgumentnie może być nullw tym przypadku, a wynik Equals(boolean) nigdy nie będzie null.
jaspis
Równie wartościowy prawie duplikat (więc nie głosowanie na zamknięcie): Czy operator == nie może być stosowany do typów ogólnych w C #?
GSerg

Odpowiedzi:

585

Aby uniknąć boksu, najlepszym sposobem na porównanie generyków dla równości jest EqualityComparer<T>.Default. Dotyczy to IEquatable<T>(bez boksu), a także object.Equalsobsługuje wszystkie Nullable<T>„podniesione” niuanse. W związku z tym:

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

To będzie pasować:

  • null dla klas
  • null (pusty) dla Nullable<T>
  • zero / false / etc dla innych struktur
Marc Gravell
źródło
29
Wow, jak cudownie niejasno! To zdecydowanie najlepsza droga, kudos.
Nick Farina
1
Zdecydowanie najlepsza odpowiedź. Brak wierszy w moim kodzie po przepisaniu, aby użyć tego rozwiązania.
Nathan Ridley,
14
Świetna odpowiedź! Jeszcze lepsze jest dodanie metody rozszerzenia dla tego wiersza kodu, aby można było przejść obj.IsDefaultForType ()
rikoe
2
@nawfal w przypadku Person, p1.Equals(p2)będzie zależeć od tego, czy implementuje IEquatable<Person>na API publicznej lub poprzez wyraźne realizacji - czyli może kompilator zobaczyć publiczną Equals(Person other)metodę. Jednak; w rodzajowych , to samo stosuje się do IL wszystkie T; T1że dzieje się wdrożenie IEquatable<T1>musi być traktowane identycznie T2że nie - więc nie, to nie będzie rozpoznać Equals(T1 other)metody, nawet jeśli istnieje w czasie wykonywania. W obu przypadkach należy również nullpomyśleć o (dowolnym obiekcie). Tak więc w przypadku generycznych chciałbym użyć kodu, który opublikowałem.
Marc Gravell
5
Nie mogę zdecydować, czy ta odpowiedź odepchnęła mnie od szaleństwa, czy też nie. +1
Steven Liekens 18.04.17
118

Co powiesz na to:

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

Zastosowanie tej static object.Equals()metody pozwala uniknąć konieczności nullsamodzielnego sprawdzania. Jawne kwalifikowanie wywołania object.prawdopodobnie nie jest konieczne w zależności od kontekstu, ale zwykle poprzedzam staticwywołania nazwą typu, aby kod był bardziej rozpuszczalny.

Kent Boogaart
źródło
2
Możesz nawet upuścić „obiekt”. część, ponieważ jest zbędna. if (Equals (myArgument, default (T)))
Stefan Moser
13
To prawda, że ​​zwykle tak jest, ale może nie być zależne od kontekstu. Może istnieć metoda Equals (), która pobiera dwa argumenty. Staram się jawnie poprzedzać wszystkie wywołania statyczne nazwą klasy, choćby po to, aby kod był łatwiejszy do odczytania.
Kent Boogaart
8
Należy pamiętać, że spowoduje to boks, a w niektórych przypadkach może być ważne
nightcoder
2
Dla mnie to nie działa, gdy używa się liczb całkowitych, które są już zapakowane. Ponieważ będzie to wtedy obiekt, a domyślna wartość obiektu to null zamiast 0.
riezebosch
28

Udało mi się znaleźć artykuł Microsoft Connect, który szczegółowo omawia ten problem:

Niestety takie zachowanie jest zgodne z projektem i nie ma łatwego rozwiązania umożliwiającego użycie parametrów typu, które mogą zawierać typy wartości.

Jeśli wiadomo, że typy są typami referencyjnymi, domyślne przeciążenie zdefiniowanych zmiennych obiektowych testuje zmienność pod kątem równości referencji, chociaż typ może określać własne niestandardowe przeciążenie. Kompilator określa, które przeciążenie zastosować, na podstawie statycznego typu zmiennej (określenie nie jest polimorficzne). Dlatego jeśli zmienisz przykład, aby ograniczyć ogólny parametr typu T do niezapieczętowanego typu odwołania (takiego jak wyjątek), kompilator może określić określone przeciążenie do użycia, a następujący kod skompiluje się:

public class Test<T> where T : Exception

Jeśli wiadomo, że typy są typami wartości, przeprowadza określone testy równości wartości w oparciu o dokładnie zastosowane typy. Nie ma tutaj dobrego „domyślnego” porównania, ponieważ porównania referencyjne nie mają znaczenia dla typów wartości, a kompilator nie może wiedzieć, które konkretne porównanie wartości ma zostać wyemitowane. Kompilator może emitować wywołanie ValueType.Equals (Object), ale ta metoda wykorzystuje odbicie i jest dość nieefektywna w porównaniu do konkretnych porównań wartości. Dlatego nawet jeśli określisz ograniczenie typu wartości na T, kompilator nie ma tutaj żadnego rozsądnego powodu:

public class Test<T> where T : struct

W przedstawionym przypadku, w którym kompilator nawet nie wie, czy T jest wartością lub typem odniesienia, podobnie nie ma nic do wygenerowania, które byłoby prawidłowe dla wszystkich możliwych typów. Porównanie referencji nie będzie poprawne dla typów wartości, a pewnego rodzaju porównanie wartości byłoby nieoczekiwane dla typów referencji, które nie są przeciążone.

Oto, co możesz zrobić ...

Zweryfikowałem, że obie te metody działają w celu ogólnego porównania typów referencyjnych i typów wartości:

object.Equals(param, default(T))

lub

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

Aby wykonać porównania z operatorem „==”, musisz użyć jednej z następujących metod:

Jeśli wszystkie przypadki T wywodzą się ze znanej klasy bazowej, możesz poinformować kompilator o ograniczeniach typów ogólnych.

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

Następnie kompilator rozpoznaje sposób wykonywania operacji MyBasei nie wyrzuci błędu „Operator” == ”, którego nie można zastosować do argumentów typu„ T ”i„ T ”, które widzisz teraz.

Inną opcją byłoby ograniczenie T do dowolnego typu, który implementuje IComparable.

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

A następnie użyj CompareTometody zdefiniowanej przez interfejs IComparable .

Eric Schoonover
źródło
4
„takie zachowanie jest zgodne z projektem i nie ma łatwego rozwiązania, które umożliwi użycie parametrów typu, które mogą zawierać typy wartości”. W rzeczywistości Microsoft się myli. Istnieje proste rozwiązanie: MS powinien rozszerzyć kod operacji ceq na typy wartości jako operator bitowy. Następnie mogą podać element wewnętrzny, który po prostu używa tego kodu operacyjnego, np. Object.BitwiseOrReferenceEquals <T> (wartość, wartość domyślna (T)), który po prostu używa ceq. Zarówno dla wartości, jak i typów referencyjnych sprawdziłoby to bitową równość wartości (ale dla typów referencyjnych referencyjna bitowa równość jest taka sama jak object.ReferenceEquals)
Qwertie
1
Myślę, że link do Microsoft Connect, który chciałeś, to connect.microsoft.com/VisualStudio/feedback/details/304501/...
Qwertie
18

Spróbuj tego:

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

które powinny się skompilować i robić, co chcesz.

Lasse V. Karlsen
źródło
Czy <code> default (T) </code> nie jest zbędny? <code> EqualityComparer <T> .Default.Equals (myArgument) </code> powinien załatwić sprawę.
Joshcodes
2
1) próbowałeś i 2) z czym porównujesz obiekt porównujący? EqualsMetoda IEqualityComparerpobiera dwa argumenty, dwa przedmioty do porównania, więc nie, to nie jest zbędny.
Lasse V. Karlsen
Jest to nawet lepsze niż zaakceptowana odpowiedź IMHO, ponieważ obsługuje boksowanie / rozpakowywanie i inne typy. Zobacz odpowiedź na pytania „zamknięte jako dupe”: stackoverflow.com/a/864860/210780
ashes999
7

(Edytowane)

Marc Gravell ma najlepszą odpowiedź, ale chciałem opublikować prosty fragment kodu, który opracowałem, aby go zademonstrować. Po prostu uruchom to w prostej aplikacji na konsolę 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();
}

Jeszcze jedno: czy ktoś z VS2008 może wypróbować to jako metodę rozszerzenia? Utknąłem tutaj w 2005 roku i jestem ciekawy, czy byłoby to dozwolone.


Edycja: Oto, jak to zrobić jako metodę rozszerzenia:

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
źródło
3
Działa „jako metoda rozszerzenia”. Co jest interesujące, ponieważ działa, nawet jeśli powiesz o.IsDefault <object> (), gdy o ma wartość null. Scary =)
Nick Farina
6

Aby obsłużyć wszystkie typy T, w tym te, gdzie T jest typem pierwotnym, musisz skompilować obie metody porównania:

    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
źródło
1
Zauważ, że funkcja została zmieniona, aby zaakceptować Func <T> i zwrócić T, co moim zdaniem zostało przypadkowo pominięte w kodzie pytającego.
Nick Farina
Wygląda na to, że ReSharper zadziera ze mną. Nie zdawałem sobie sprawy, że jego ostrzeżenie o możliwym porównaniu typu wartości z wartością null nie było ostrzeżeniem kompilatora.
Nathan Ridley
2
FYI: Jeśli T okaże się typem wartości, wówczas porównanie z wartością null będzie traktowane przez jitter jako zawsze fałszywe.
Eric Lippert
Ma to sens - środowisko wykonawcze będzie porównywać wskaźnik z typem wartości. Kontrola Equals () działa jednak w tym przypadku (co ciekawe, ponieważ wydaje się bardzo dynamicznym językiem powiedzieć 5.Equals (4), który się kompiluje).
Nick Farina
2
Zobacz EqualityComparer <T> odpowiedź na alternatywę, która nie wymaga boks et
Marc Gravell
2

Będzie tu problem -

Jeśli chcesz, aby działało to dla dowolnego typu, wartość domyślna (T) będzie zawsze pusta dla typów referencyjnych, a 0 (lub struct pełna 0) dla typów wartości.

Prawdopodobnie nie jest to zachowanie, którego szukasz. Jeśli chcesz, aby działało to w sposób ogólny, prawdopodobnie musisz użyć refleksji, aby sprawdzić typ T i obsłużyć typy wartości inne niż typy referencyjne.

Alternatywnie, możesz nałożyć na to ograniczenie interfejsu, a interfejs może zapewnić sposób sprawdzenia domyślnego ustawienia klasy / struct.

Reed Copsey
źródło
1

Myślę, że prawdopodobnie musisz podzielić tę logikę na dwie części i najpierw sprawdzić, czy nie ma wartości 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;
}

W metodzie IsNull opieramy się na fakcie, że obiekty ValueType z definicji nie mogą mieć wartości NULL, więc jeśli wartość przypadkowo jest klasą wywodzącą się z ValueType, wiemy już, że nie jest ona pusta. Z drugiej strony, jeśli nie jest to typ wartości, możemy po prostu porównać wartość rzutowaną na obiekt z wartością null. Moglibyśmy uniknąć sprawdzania wartości ValueType, przechodząc prosto do rzutowania na obiekt, ale oznaczałoby to, że typ wartości zostałby zapakowany w pole, czego prawdopodobnie chcemy uniknąć, ponieważ oznacza to, że na stosie tworzony jest nowy obiekt.

W metodzie IsNullOrEmpty sprawdzamy specjalny przypadek ciągu. Dla wszystkich innych typów porównujemy wartość (która już wie, że nie jest pusta) z wartością domyślną, która dla wszystkich typów referencyjnych jest pusta, a dla typów wartości jest zwykle jakąś formą zera (jeśli są integralne).

Korzystając z tych metod, następujący kod zachowuje się tak, jak można się spodziewać:

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
źródło
1

Metoda rozszerzenia oparta na zaakceptowanej odpowiedzi.

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

Stosowanie:

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

Na przemian z null, aby uprościć:

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

Stosowanie:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }
dynamiclynk
źródło
0

Używam:

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
źródło
-1

Nie wiem, czy to działa z twoimi wymaganiami, czy nie, ale możesz ograniczyć T do typu, który implementuje interfejs, taki jak IComparable, a następnie użyć metody ComparesTo () z tego interfejsu (który IIRC obsługuje / obsługuje wartości zerowe) w ten sposób :

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

Prawdopodobnie są też inne interfejsy, których można by użyć IEquitable itp.

Caryden
źródło
OP martwi się o wyjątek NullReferenceException i gwarantujesz mu to samo.
nawfal
-2

@ilitirit:

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

Operator „==” nie może być stosowany do argumentów typu „T” i „T”

Nie mogę wymyślić sposobu, aby to zrobić bez jawnego testu zerowego, a następnie wywołania metody lub obiektu Equals.Equals, jak sugerowano powyżej.

Możesz opracować rozwiązanie za pomocą System.Comparison, ale tak naprawdę skończy się to znacznie większą liczbą linii kodu i znacznie zwiększy złożoność.

cfeduke
źródło
-3

Myślę, że byłeś blisko.

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

Teraz to się kompiluje, ale jeśli się nie powiedzie myArgument ma wartość null, co jest częścią tego, co testuję. Mogę dodać jawny czek zerowy w następujący sposób:

Wystarczy odwrócić obiekt, na którym wzywa się równych, aby uzyskać eleganckie podejście zerowe.

default(T).Equals(myArgument);
Scott McKay
źródło
Myślałem dokładnie to samo.
Chris Gessler
6
default (T) typu odwołania ma wartość null i skutkuje gwarantowanym wyjątkiem NullReferenceException.
Stefan Steinegger,