Jak porównać wartości typów ogólnych?

81

Jak porównać wartości typów ogólnych?

Zredukowałem to do minimalnej próbki:

public class Foo<T> where T : IComparable
{
    private T _minimumValue = default(T);

    public bool IsInRange(T value) 
    {
        return (value >= _minimumValue); // <-- Error here
    }
}

Błąd:

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

Co u licha !? Tsą już ograniczone do IComparable, a nawet kiedy ograniczanie go do typów wartości ( where T: struct), nadal nie może zastosować każdy z operatorów <, >, <=, >=, ==lub !=. (Wiem, że Equals()istnieją obejścia dotyczące ==i !=, ale nie pomaga to operatorom relacyjnym).

A więc dwa pytania:

  1. Dlaczego obserwujemy to dziwne zachowanie? Co powstrzymuje nas przed porównaniem wartości typów ogólnych, o których wiadomo, że są IComparable? Czy nie jest to w jakiś sposób sprzeczne z celem ogólnych ograniczeń?
  2. Jak to rozwiązać lub przynajmniej obejść?

(Zdaję sobie sprawę, że jest już kilka pytań związanych z tym pozornie prostym problemem - ale żaden z wątków nie daje wyczerpującej ani praktycznej odpowiedzi, więc tutaj.)

gstercken
źródło

Odpowiedzi:

96

IComparablenie przeciąża >=operatora. Powinieneś użyć

value.CompareTo(_minimumValue) >= 0
faester
źródło
7
Świetnie, to działa (i oczywiście to wyjaśnia) - wielkie dzięki! Ale to trochę niezadowalające i pozostawia pytanie: dlaczego IComparable nie przeciąża operatorów porównania? Czy jest to świadoma i przemyślana decyzja projektowa, z dobrego powodu - czy coś, co zostało przeoczone podczas projektowania ramy? W końcu „x.CompareTo (y)> = 0” jest mniej czytelne niż „x> = y”, nie?
gstercken
Zdecydowanie rozumiem twój punkt widzenia. Myślę, że problem polega na tym, że operatorzy są statyczni, co oznacza, że ​​nie mogą pasować do interfejsu. Nie będę oceniał, czy jest to dobry wybór, czy nie, ale wydaje mi się, że metody z nazwami własnymi są łatwiejsze do odczytania niż operatory, gdy typy nie są prymitywne; to jednak kwestia gustu.
faester
5
@gstercken: jeden problem IComparableprzeciążenia operatorów porównania jest to, że istnieją sytuacje, w których X.Equals(Y)należy return false, ale X.CompareTo(Y)powinien wrócić zero (co sugeruje ani elementem jest większa niż druga) [np ExpenseItemmoże mieć naturalną kolejność względem TotalCost, i nie może być brak naturalnego uporządkowania pozycji kosztowych, których koszt jest taki sam, ale nie oznacza to, że każda pozycja wydatkowa, która kosztuje 3141,59 USD, powinna być traktowana jako ekwiwalent każdej innej pozycji, która kosztuje tyle samo.
supercat
1
@gstercken: Zasadniczo istnieje kilka rzeczy, które ==mogą logicznie oznaczać. W niektórych kontekstach możliwe jest, X==Yaby być prawdą, podczas gdy X.Equals(Y)jest fałszem, a podczas gdy w innych kontekstach X==Ymoże być fałszem, gdy X.Equals(Y)jest prawdziwe. Nawet jeśli operatorzy mogą być przeciążone dla interfejsów, przeciążenia <, <=, >a >=pod względem IComparable<T>może dać wrażenie, że ==i !=będzie również przeciążone w takich warunkach. Gdyby C #, podobnie jak vb, zabronił używania ==typów klas, dla których nie był przeciążony, mogłoby to nie być takie złe, ale ...
supercat Kwietnia
2
... niestety C # zdecydował się użyć tokenu ==do reprezentowania zarówno przeciążalnego operatora równości, jak i nieprzeciążalnego testu równości odniesienia.
supercat
35

Problem z przeciążeniem operatora

Niestety interfejsy nie mogą zawierać przeciążonych operatorów. Spróbuj wpisać to w swoim kompilatorze:

public interface IInequalityComaparable<T>
{
    bool operator >(T lhs, T rhs);
    bool operator >=(T lhs, T rhs);
    bool operator <(T lhs, T rhs);
    bool operator <=(T lhs, T rhs);
}

Nie wiem, dlaczego na to nie pozwolili, ale domyślam się, że skomplikowało to definicję języka i byłoby trudne dla użytkowników do poprawnej implementacji.

Albo to, albo projektanci nie lubili możliwości nadużyć. Na przykład wyobraź sobie, że robisz >=porównanie na class MagicMrMeow. Lub nawet na class Matrix<T>. Co oznacza wynik dla tych dwóch wartości ?; Zwłaszcza, gdy może być niejasność?

Oficjalne obejście

Ponieważ powyższy interfejs nie jest legalny, mamy IComparable<T>interfejs umożliwiający obejście problemu. Nie implementuje operatorów i udostępnia tylko jedną metodę,int CompareTo(T other);

Zobacz http://msdn.microsoft.com/en-us/library/4d7sx9hd.aspx

intWynik jest rzeczywiście tri-bitowy lub tri-nary (podobny do Boolean, ale z trzech stanów). Ta tabela wyjaśnia znaczenie wyników:

Value              Meaning

Less than zero     This object is less than
                   the object specified by the CompareTo method.

Zero               This object is equal to the method parameter.

Greater than zero  This object is greater than the method parameter.

Korzystanie z obejścia

Aby zrobić odpowiednik value >= _minimumValue, musisz zamiast tego napisać:

value.CompareTo(_minimumValue) >= 0
Merlyn Morgan-Graham
źródło
2
Ach, racja - ma sens. Zapomniałem, że interfejsy w C # nie mogą przeciążać operatorów.
gstercken
32

Jeśli valuemoże być puste, bieżąca odpowiedź może się nie powieść. Zamiast tego użyj czegoś takiego:

Comparer<T>.Default.Compare(value, _minimumValue) >= 0
Peter Hedberg
źródło
1
Dzięki za wskazówkę. Potrzebowałem tego dla niektórych metod rozszerzeń, nad którymi pracowałem. Zobacz poniżej.
InteXX,
1
Ok, rozumiem. Nie ograniczałem Tsię IComparable. Mimo wszystko twoja napiwek doprowadziła mnie do szaleństwa.
InteXX,
6
public bool IsInRange(T value) 
{
    return (value.CompareTo(_minimumValue) >= 0);
}

Podczas pracy z IComparable generics wszystkie operatory mniejsze niż / większe niż muszą zostać przekonwertowane na wywołania do CompareTo. Niezależnie od tego, jakiego operatora użyjesz, zachowaj porównywane wartości w tej samej kolejności i porównaj je z zerem. ( x <op> yStanie x.CompareTo(y) <op> 0, w którym <op>znajduje się >, >=etc.)

Poleciłbym również, aby ogólne ograniczenie, którego używasz, było where T : IComparable<T>. IComparable sam w sobie oznacza, że ​​obiekt można porównać z czymkolwiek, porównywanie obiektu z innymi tego samego typu jest prawdopodobnie bardziej odpowiednie.

David Yaw
źródło
3

Zamiast klasy value >= _minimValueużytkowej Comparer:

public bool IsInRange(T value ) {
    var result = Comparer<T>.Default.Compare(value, _minimumValue);
    if ( result >= 0 ) { return true; }
    else { return false; }
}
TcKs
źródło
Czy wprowadzić użycie a, Comparergdy istnieje już ogólne ograniczenie, które Tnależy wdrożyć IComparable?
Fredrik Mörk
Ogólne ograniczenia @Fredrik mają tendencję do narastania. Zgadzam się z pominięciem ich tutaj.
Marc Gravell
2

Jak powiedzieli inni, należy jawnie użyć metody CompareTo. Powodem, dla którego nie można używać interfejsów z operatorami, jest to, że klasa może zaimplementować dowolną liczbę interfejsów bez wyraźnego rankingu między nimi. Załóżmy, że ktoś próbował obliczyć wyrażenie „a = foo + 5;” gdy foo zaimplementowało sześć interfejsów, z których wszystkie definiują operator „+” z drugim argumentem będącym liczbą całkowitą; jakiego interfejsu powinien używać operator?

Fakt, że klasy mogą wyprowadzać wiele interfejsów, sprawia, że ​​interfejsy są bardzo potężne. Niestety, często zmusza to do wyraźniejszego wyrażenia tego, co naprawdę chce się zrobić.

supercat
źródło
1
Nie sądzę, że MI nie jest już problemem w przypadku przeciążonych operatorów, jak ma to miejsce w przypadku zwykłych metod. Są po prostu zabawną składnią metod. Więc możesz rozwiązać ten problem, używając tych samych reguł, które byłyby używane do rozwiązywania go, jak zwykłe metody na interfejsach. Jednym z celów projektowania C # było, aby był on nieco znajomy dla programistów C ++, a przeciążone operatory były nadużywane w tym języku do diabła. Domyślam się, że projektanci preferowali nazwane metody, które zmuszają cię do podania jakiejś dokumentacji na potrzeby tych metod.
Merlyn Morgan-Graham
1

IComparablewymusza tylko funkcję o nazwie CompareTo(). Nie możesz więc zastosować żadnego z wymienionych operatorów

parapura rajkumar
źródło
0

Udało mi się użyć odpowiedzi Petera Hedburga do stworzenia przeciążonych metod rozszerzających dla typów generycznych. Zauważ, że CompareTometoda nie działa tutaj, ponieważ typ Tjest nieznany i nie przedstawia tego interfejsu. To powiedziawszy, jestem zainteresowany znalezieniem alternatyw.

Chciałbym napisać w C #, ale konwerter Telerika nie działa w tym kodzie. Nie znam języka C # na tyle, aby niezawodnie przekonwertować go ręcznie. Jeśli ktoś chciałby zrobić zaszczyt, byłbym zadowolony, gdyby został odpowiednio zredagowany.

<Extension>
<DebuggerStepThrough>
Public Sub RemoveDuplicates(Of T)(Instance As List(Of T))
  Instance.RemoveDuplicates(Function(X, Y) Comparer(Of T).Default.Compare(X, Y))
End Sub



<Extension>
<DebuggerStepThrough>
Public Sub RemoveDuplicates(Of T)(Instance As List(Of T), Comparison As Comparison(Of T))
  Instance.RemoveDuplicates(New List(Of Comparison(Of T)) From {Comparison})
End Sub



<Extension>
<DebuggerStepThrough>
Public Sub RemoveDuplicates(Of T)(Instance As List(Of T), Comparisons As List(Of Comparison(Of T)))
  Dim oResults As New List(Of Boolean)

  For i As Integer = 0 To Instance.Count - 1
    For j As Integer = Instance.Count - 1 To i + 1 Step -1
      oResults.Clear()

      For Each oComparison As Comparison(Of T) In Comparisons
        oResults.Add(oComparison(Instance(i), Instance(j)) = 0)
      Next oComparison

      If oResults.Any(Function(R) R) Then
        Instance.RemoveAt(j)
      End If
    Next j
  Next i
End Sub

--EDYTOWAĆ--

Udało mi się to wyczyścić, ograniczając się Tdo IComparable(Of T)wszystkich metod, jak wskazano w OP. Zauważ, że to ograniczenie wymaga również typu Tdo zaimplementowania IComparable(Of <type>).

<Extension>
<DebuggerStepThrough>
Public Sub RemoveDuplicates(Of T As IComparable(Of T))(Instance As List(Of T))
  Instance.RemoveDuplicates(Function(X, Y) X.CompareTo(Y))
End Sub
InteXX
źródło