Czy operator == nie może być stosowany do typów ogólnych w C #?

326

Zgodnie z dokumentacją ==operatora w MSDN ,

W przypadku predefiniowanych typów wartości operator równości (==) zwraca wartość true, jeśli wartości jego argumentów są równe, w przeciwnym razie false. W przypadku typów odwołań innych niż łańcuch == zwraca wartość true, jeśli dwa operandy odnoszą się do tego samego obiektu. Dla typu ciągu == porównuje wartości ciągów. Zdefiniowane przez użytkownika typy wartości mogą przeciążać operatora == (patrz operator). Podobnie mogą być typy referencyjne zdefiniowane przez użytkownika, chociaż domyślnie == zachowuje się tak, jak opisano powyżej, zarówno dla typów predefiniowanych, jak i zdefiniowanych przez użytkownika.

Dlaczego więc ten fragment kodu się nie kompiluje?

bool Compare<T>(T x, T y) { return x == y; }

Pojawia się błąd Operator „==” nie może być stosowany do operandów typu „T” i „T” . Zastanawiam się, dlaczego, skoro o ile rozumiem, ==operator jest predefiniowany dla wszystkich typów?

Edycja: Dzięki wszystkim. Z początku nie zauważyłem, że stwierdzenie dotyczy tylko typów referencji. Pomyślałem też, że porównanie bit po bicie jest dla wszystkich typów wartości, co wiem teraz jest nie poprawny.

Ale w przypadku, gdy korzystam z typu odniesienia, czy ==operator użyłby wstępnie zdefiniowanego porównania odniesienia, czy użyłby przeciążonej wersji operatora, jeśli typ byłby zdefiniowany?

Edycja 2: Dzięki próbom i błędom dowiedzieliśmy się, że ==operator użyje predefiniowanego porównania referencyjnego, gdy użyje nieograniczonego typu ogólnego. Właściwie kompilator użyje najlepszej metody, jaką może znaleźć dla argumentu typu ograniczonego, ale nie będzie dalej szukał. Na przykład poniższy kod zawsze będzie drukowany true, nawet gdy Test.test<B>(new B(), new B())zostanie wywołany:

class A { public static bool operator==(A x, A y) { return true; } }
class B : A { public static bool operator==(B x, B y) { return false; } }
class Test { void test<T>(T a, T b) where T : A { Console.WriteLine(a == b); } }
Hosam Aly
źródło
Zobacz moją odpowiedź ponownie, aby uzyskać odpowiedź na twoje kolejne pytanie.
Giovanni Galbo
Przydatne może być zrozumienie, że nawet bez generyków istnieją pewne typy, dla których ==nie można używać dwóch operandów tego samego typu. Dotyczy to structtypów (z wyjątkiem typów „predefiniowanych”), które nie przeciążają operator ==. Jako prosty przykład spróbuj tego:var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */
Jeppe Stig Nielsen,
Kontynuując mój stary komentarz. Na przykład (zobacz inny wątek ), za pomocą var kvp1 = new KeyValuePair<int, int>(); var kvp2 = kvp1;, nie można sprawdzić, kvp1 == kvp2ponieważ KeyValuePair<,>jest strukturą, nie jest typem wstępnie zdefiniowanym w języku C # i nie powoduje przeciążenia operator ==. Podany jest jednak przykład, za var li = new List<int>(); var e1 = li.GetEnumerator(); var e2 = e1;pomocą którego nie można tego zrobić e1 == e2(tutaj mamy zagnieżdżoną strukturę List<>.Enumerator(wywoływaną "List`1+Enumerator[T]"przez środowisko wykonawcze), która się nie przeciąża ==).
Jeppe Stig Nielsen
RE: „Więc dlaczego ten fragment kodu się nie kompiluje?” - Eee ... bo nie możesz zwrócić a boolz void...
BrainSlugs83
1
@ BrainSlugs83 Dzięki za złapanie 10-letniego błędu!
Hosam Aly

Odpowiedzi:

143

„... domyślnie == zachowuje się tak, jak opisano powyżej, zarówno dla predefiniowanych, jak i zdefiniowanych przez użytkownika typów referencji.”

Typ T niekoniecznie jest typem referencyjnym, więc kompilator nie może przyjąć tego założenia.

Jednak to się skompiluje, ponieważ jest bardziej jednoznaczne:

    bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }

Postępuj zgodnie z dodatkowym pytaniem: „Ale w przypadku, gdy używam typu odniesienia, operator == użyłby wstępnie zdefiniowanego porównania odniesienia, czy użyłby przeciążonej wersji operatora, jeśli typ zdefiniowałby?”

Myślałem, że == w Generics użyłby przeciążonej wersji, ale poniższy test pokazuje inaczej. Ciekawe ... Chciałbym wiedzieć, dlaczego! Jeśli ktoś wie, udostępnij.

namespace TestProject
{
 class Program
 {
    static void Main(string[] args)
    {
        Test a = new Test();
        Test b = new Test();

        Console.WriteLine("Inline:");
        bool x = a == b;
        Console.WriteLine("Generic:");
        Compare<Test>(a, b);

    }


    static bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }
 }

 class Test
 {
    public static bool operator ==(Test a, Test b)
    {
        Console.WriteLine("Overloaded == called");
        return a.Equals(b);
    }

    public static bool operator !=(Test a, Test b)
    {
        Console.WriteLine("Overloaded != called");
        return a.Equals(b);
    }
  }
}

Wynik

Inline: Przeciążony == wywołany

Rodzajowy:

Naciśnij dowolny klawisz, aby kontynuować . . .

Kontynuacja 2

Chcę podkreślić, że zmieniając moją metodę porównywania na

    static bool Compare<T>(T x, T y) where T : Test
    {
        return x == y;
    }

powoduje wywołanie przeciążonego operatora ==. Wydaje mi się, że bez podania typu (jako miejsca ) kompilator nie może wywnioskować, że powinien użyć przeciążonego operatora ... choć sądzę, że będzie miał wystarczającą ilość informacji, aby podjąć decyzję nawet bez podania typu.

Giovanni Galbo
źródło
Dzięki. Nie zauważyłem, że stwierdzenie dotyczyło tylko typów referencji.
Hosam Aly,
4
Re: Follow Up 2: Właściwie kompilator połączy go z najlepszą metodą, jaką znajdzie, czyli w tym przypadku Test.op_Equal. Ale jeśli miałeś klasę, która wywodzi się z testu i przesłania operator, operator testu będzie nadal wywoływany.
Hosam Aly,
4
Dobrą praktyką, na którą chciałbym zwrócić uwagę, jest to, że zawsze powinieneś robić rzeczywiste porównanie w przesłoniętej Equalsmetodzie (nie w ==operatorze).
jpbochi
11
Rozwiązanie problemu przeciążenia następuje w czasie kompilacji. Więc kiedy mamy do czynienia z ==typami rodzajowymi Ti T, stwierdzamy najlepsze przeciążenie, biorąc pod uwagę ograniczenia, jakie niesie T(istnieje specjalna zasada, że ​​nigdy nie określi dla tego typu wartości (co dałoby bezsensowny wynik), stąd musi być niektóre ograniczenia gwarantujące, że jest to typ referencyjny). W swojej śledzić 2 , jeśli przyjeżdża się z DerivedTestprzedmiotami, a DerivedTestwywodzi Testale wprowadza nowy przeciążenia ==, będziesz miał „problem” ponownie. To, które przeciążenie się nazywa, jest „wypalane” do IL w czasie kompilacji.
Jeppe Stig Nielsen
1
dziwnie, wydaje się, że działa to w przypadku ogólnych typów referencyjnych (gdzie można oczekiwać, że to porównanie będzie dotyczyło równości referencyjnej), ale w przypadku łańcuchów wydaje się również używać równości referencyjnej - możesz więc skończyć na porównywaniu 2 identycznych ciągów i posiadaniu == (gdy w metoda ogólna z ograniczeniem klasy) mówi, że są różne.
JonnyRaa
291

Jak powiedzieli inni, będzie działać tylko wtedy, gdy T jest ograniczony do typu odniesienia. Bez żadnych ograniczeń można porównać z wartością null, ale tylko z wartością null - i to porównanie zawsze będzie fałszywe dla typów wartości, które nie mają wartości zerowych.

Zamiast wywoływać Equals, lepiej użyć IComparer<T> - a jeśli nie masz więcej informacji, EqualityComparer<T>.Defaultto dobry wybór:

public bool Compare<T>(T x, T y)
{
    return EqualityComparer<T>.Default.Equals(x, y);
}

Oprócz czegokolwiek innego, pozwala to uniknąć boksu / castingu.

Jon Skeet
źródło
Dzięki. Próbowałem napisać prostą klasę opakowania, więc po prostu chciałem przekazać tę operację rzeczywistemu opakowanemu członkowi. Ale znajomość EqualityComparer <T>. Domyślnie z pewnością dodała mi wartość. :)
Hosam Aly,
Pomijając drobne, Jon; warto zauważyć komentarz dotyczący mojego poboxu przeciwko pobox vs.
Marc Gravell
4
Dobra
1
+1 za wskazanie, że można porównać do wartości zerowej, a dla typu wartości zerowej zawsze będzie to fałsz
Jalal Said
@BlueRaja: Tak, ponieważ istnieją specjalne zasady porównań z literałem zerowym. Dlatego „bez żadnych ograniczeń można porównać z wartością null, ale tylko z wartością null”. Jest już w odpowiedzi. Dlaczego więc nie może to być poprawne?
Jon Skeet,
41

Ogólnie rzecz biorąc, EqualityComparer<T>.Default.Equalspowinien wykonywać pracę ze wszystkim, co implementujeIEquatable<T> lub ma sensowną Equalsimplementację.

Jeśli jednak ==i Equalsz jakiegoś powodu są wdrażane inaczej, moja praca nad operatorami rodzajowymi powinna być użyteczna; wspiera operatora wersje (między innymi):

  • Równa (wartość T1, wartość T2)
  • NotEqual (wartość T1, wartość T2)
  • GreaterThan (wartość T1, wartość T2)
  • LessThan (wartość T1, wartość T2)
  • GreaterThanOrEqual (wartość T1, wartość T2)
  • LessThanOrEqual (wartość T1, wartość T2)
Marc Gravell
źródło
Bardzo interesująca biblioteka! :) (Uwaga dodatkowa: Czy mogę zasugerować użycie linku do www.yoda.arachsys.com, ponieważ pobox został zablokowany przez zaporę w moim miejscu pracy? Możliwe, że inni mogą napotkać ten sam problem.)
Hosam Aly,
Chodzi o to, że pobox.com/~skeet zawsze będzie wskazywać na moją stronę internetową - nawet jeśli przeniesie się gdzie indziej. Zwykle publikuję linki za pośrednictwem pobox.com ze względu na potomstwo - ale obecnie możesz zastąpić yoda.arachsys.com.
Jon Skeet
Problem z pobox.com polega na tym, że jest to internetowa usługa e-mail (lub tak twierdzi zapora firmy), więc jest zablokowana. Dlatego nie mogłem śledzić jego linku.
Hosam Aly
„Jeśli jednak z jakiegoś powodu == i Equals są wdrażane inaczej” - Święty pali! Cóż jednak! Może po prostu muszę zobaczyć przypadek użycia, który jest przeciwny, ale biblioteka z rozbieżną semantyką równą prawdopodobnie napotka większe problemy niż kłopoty z generycznymi.
Edward Brey,
@EdwardBrey się nie mylisz; byłoby miło, gdyby kompilator mógł to narzucić, ale ...
Marc Gravell
31

Tak wiele odpowiedzi, a żadna z nich nie wyjaśnia DLACZEGO? (o co wyraźnie poprosił Giovanni) ...

Generyczne .NET nie działają jak szablony C ++. W szablonach C ++ rozwiązywanie przeciążenia występuje po poznaniu rzeczywistych parametrów szablonu.

W generycznych .NET (w tym C #) rozwiązywanie przeciążeń występuje bez znajomości rzeczywistych parametrów ogólnych. Jedyne informacje, których kompilator może użyć do wybrania funkcji do wywołania, pochodzą z ograniczeń typu w parametrach ogólnych.

Ben Voigt
źródło
2
ale dlaczego kompilator nie może traktować ich jako ogólnego obiektu? mimo wszystko ==działa dla wszystkich typów, niezależnie od tego, czy są to typy referencyjne, czy typy wartości. To powinno być pytanie, na które nie sądzę, że odpowiedziałeś.
nawfal
4
@nawfal: Właściwie nie, ==nie działa dla wszystkich typów wartości. Co ważniejsze, nie ma tego samego znaczenia dla wszystkich typów, więc kompilator nie wie, co z tym zrobić.
Ben Voigt
1
Ben, och tak, brakowało mi niestandardowych struktur, które możemy stworzyć bez żadnego ==. Czy możesz również dołączyć tę część do swojej odpowiedzi, jak sądzę, że to jest główny punkt tutaj
nawfal
12

Kompilacja nie może wiedzieć, że T nie może być strukturą (typ wartości). Więc musisz powiedzieć, że może to być tylko typ referencyjny, myślę:

bool Compare<T>(T x, T y) where T : class { return x == y; }

Jest tak, ponieważ jeśli T może być typem wartości, mogą istnieć przypadki, w których x == ybyłby źle sformułowany - w przypadkach, gdy typ nie ma zdefiniowanego operatora ==. To samo stanie się z tym, co jest bardziej oczywiste:

void CallFoo<T>(T x) { x.foo(); }

To też się nie udaje, ponieważ można przekazać typ T, który nie miałby funkcji foo. C # zmusza cię do upewnienia się, że wszystkie możliwe typy zawsze mają funkcję foo. Robi to klauzula where.

Johannes Schaub - litb
źródło
1
Dziękuję za wyjaśnienie. Nie wiedziałem, że typy wartości nie obsługują operatora == po wyjęciu z pudełka.
Hosam Aly,
1
Hosam, testowałem z gmcs (mono) i zawsze porównuje referencje. (tzn. nie używa opcjonalnie zdefiniowanego operatora == dla T)
Johannes Schaub - litb
Jest jedno zastrzeżenie dotyczące tego rozwiązania: operator == nie może być przeciążony; zobacz to pytanie StackOverflow .
Dimitri C.
8

Wygląda na to, że bez ograniczenia klasy:

bool Compare<T> (T x, T y) where T: class
{
    return x == y;
}

Należy zdawać sobie sprawę, że będąc classograniczonym Equalsw== operatora dziedziczy Object.Equals, podczas gdy struktura zastępujeValueType.Equals .

Uwaga:

bool Compare<T> (T x, T y) where T: struct
{
    return x == y;
}

podaje również ten sam błąd kompilatora.

Jak dotąd nie rozumiem, dlaczego kompilator odrzuca porównanie operatora równości typu wartości. Wiem jednak na pewno, że to działa:

bool Compare<T> (T x, T y)
{
    return x.Equals(y);
}
Jon Limjap
źródło
wiesz, że jestem w sumie c # noob. ale myślę, że to się nie udaje, ponieważ kompilator nie wie, co robić. ponieważ T nie jest jeszcze znany, to, co zostanie zrobione, zależy od typu T, czy dozwolone są typy wartości. dla referencji, referencje są porównywane bez względu na T. Jeśli wykonasz .Equals, wtedy .Equal jest właśnie wywoływany.
Johannes Schaub - litb
ale jeśli zrobisz == dla typu wartości, typ wartości nie musi koniecznie implementować tego operatora.
Johannes Schaub - litb
To miałoby sens, litb :) Możliwe, że struktury zdefiniowane przez użytkownika nie przeciążają ==, więc kompilator się nie powiedzie.
Jon Limjap,
2
Pierwsza metoda porównania nie używa, Object.Equalsale testuje równość odniesienia. Na przykład Compare("0", 0.ToString())zwróciłoby wartość false, ponieważ argumenty byłyby odniesieniami do odrębnych ciągów, z których oba mają zero jako jedyny znak.
supercat
1
Drobne problemy z tym ostatnim - nie ograniczyłeś go do struktur, więc NullReferenceExceptionmoże się zdarzyć.
Flynn1179,
6

W moim przypadku chciałem przetestować jednostkę operatora równości. Potrzebowałem wywołać kod w ramach operatorów równości bez jawnego ustawiania typu ogólnego. Porady dla EqualityComparernie były przydatne jako metoda EqualityComparernazywana Equals, ale nie były operatorem równości.

Oto jak mam to pracować z typami rodzajowymi, budując LINQ. Wywołuje odpowiedni kod ==i !=operatory:

/// <summary>
/// Gets the result of "a == b"
/// </summary>
public bool GetEqualityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.Equal(paramA, paramB);
    // compile it
    var invokeEqualityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeEqualityOperator(a, b);
}

/// <summary>
/// Gets the result of "a =! b"
/// </summary>
public bool GetInequalityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.NotEqual(paramA, paramB);
    // compile it
    var invokeInequalityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeInequalityOperator(a, b);
}
U. Bulle
źródło
4

Jest to wpis MSDN Connect dla tego tutaj

Odpowiedź Alexa Turnera zaczyna się od:

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

Recep
źródło
4

Jeśli chcesz się upewnić, że operatorzy niestandardowego typu są nazywani, możesz to zrobić przez odbicie. Wystarczy pobrać typ za pomocą parametru ogólnego i pobrać MethodInfo dla żądanego operatora (np. Op_Equality, op_Inequality, op_LessThan ...).

var methodInfo = typeof (T).GetMethod("op_Equality", 
                             BindingFlags.Static | BindingFlags.Public);    

Następnie uruchom operator za pomocą metody Invoke MethodInfo i przekaż obiekty jako parametry.

var result = (bool) methodInfo.Invoke(null, new object[] { object1, object2});

Spowoduje to wywołanie przeciążonego operatora, a nie tego określonego przez ograniczenia nałożone na parametr ogólny. Może to nie być praktyczne, ale może się przydać do testowania jednostkowego operatorów przy użyciu ogólnej klasy bazowej, która zawiera kilka testów.

Christophe
źródło
3

Napisałem następującą funkcję, patrząc na najnowszy msdn. Może łatwo porównywać dwa obiekty xi y:

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}
Charlie
źródło
4
Możesz pozbyć się swoich boolean i pisaćreturn ((IComparable)(x)).CompareTo(y) <= 0;
aloisdg przechodzi na codidact.com
1

bool Compare(T x, T y) where T : class { return x == y; }

Powyższe zadziała, ponieważ == jest załatwiane w przypadku typów referencyjnych zdefiniowanych przez użytkownika.
W przypadku typów wartości == można zastąpić. W takim przypadku należy również zdefiniować „! =”.

Myślę, że może to być przyczyną, nie pozwala na ogólne porównanie przy użyciu „==”.

shahkalpesh
źródło
2
Dzięki. Uważam, że typy referencyjne również mogą zastąpić operatora. Ale powód niepowodzenia jest teraz jasny.
Hosam Aly,
1
==Znak jest używany dla dwóch różnych operatorów. Jeśli dla danych typów argumentów istnieje kompatybilne przeciążenie operatora równości, to przeciążenie zostanie zastosowane. W przeciwnym razie, jeśli oba operandy są typami referencji, które są ze sobą kompatybilne, zostanie użyte porównanie referencji. Zauważ, że w Comparepowyższej metodzie kompilator nie może powiedzieć, że pierwsze znaczenie ma zastosowanie, ale może powiedzieć, że drugie znaczenie ma zastosowanie, więc ==token użyje tego drugiego, nawet jeśli Tprzeciąża operator sprawdzania równości (np. Jeśli jest typu String) .
supercat
0

.Equals()Pracuje dla mnie natomiast TKeyto typ rodzajowy.

public virtual TOutputDto GetOne(TKey id)
{
    var entity =
        _unitOfWork.BaseRepository
            .FindByCondition(x => 
                !x.IsDelete && 
                x.Id.Equals(id))
            .SingleOrDefault();


    // ...
}
Masoud Darvishian
źródło
To x.Id.Equalsnie tak id.Equals. Prawdopodobnie kompilator wie coś o typie x.
Hosam Aly,