2011-09-04 13 views
29

Czy powinienem wdrożyć zarówno IComparable, jak i rodzajowy IComparable<T>? Czy są jakieś ograniczenia, jeśli wdrażam tylko jedną z nich?IComparable i IComparable <T>

+0

Naprawdę musisz tylko napisać kod dla 'IComparable '. Otrzymasz 'IComparable' za darmo przez delegowanie rzeczywistych porównań do ogólnych implementacji. –

Odpowiedz

18

Tak, powinieneś zaimplementować oba.

Jeśli zaimplementujesz jeden kod, który zależy od drugiego, zawiedzie.

Istnieje wiele kodów, które używają IComparable lub IComparable<T>, ale nie obie, więc wdrożenie obu zapewni, że kod będzie działał z takim kodem.

+0

Jeśli kontrolujesz cały kod, czy możesz określić, że potrzebujesz tylko wersji ogólnej? A może część kodu ramowego nadal wymaga wersji nie ogólnej? –

+0

@David, niektóre klasy szkieletowe (na przykład kolekcje) mogą polegać na jednym lub drugim. –

+5

@David - 'IComparable' jest z pewnością używany przez niektóre ramy kodu. Wierzę, że 'Array.Sort' i' ArrayList.Sort' go używają. – Oded

4

Podczas IEquatable <T> generalnie nie powinny być realizowane przez klasy lakierowanych, ponieważ takie wyprowadzenie grałby dziwnie z dziedziczenia, chyba że realizacja prostu wywołuje Object.Equals (w tym przypadku byłoby bezcelowe), sytuacja odwrotna powstaje z ogólny IComparable <T>. Semantyka Object.Equals i IEquatable <T> oznacza, że ​​za każdym razem, gdy zdefiniowana jest definicja IEquatable <T>, jej zachowanie powinno odzwierciedlać zachowanie obiektu Object.Equals (pomijając możliwość bycia szybszym i unikanie boksowania). Dwa obiekty typu DerivedFoo, które są porównywane jako równe, gdy są traktowane jako typ DerivedFoo, powinny również porównywać równe, gdy są traktowane jako obiekty typu Foo i odwrotnie. Z drugiej strony jest całkiem możliwe, że dwa obiekty typu DerivedFoo, które mają nierówną pozycję, gdy są uznawane za typ DerivedFoo, powinny być równo traktowane, gdy są traktowane jako typ Foo. Jedynym sposobem na zapewnienie tego jest użycie IComparable <T>.

Załóżmy na przykład, że mamy klasę SchedulerEvent, która zawiera pola ScheduledTime (typu DateTime) i ScheduledAction (typu MethodInvoker). Klasa zawiera podtypy SchedulerEventWithMessage (która dodaje pole Message typu string) i SchedulerEventWithGong (która dodaje pole GongVolume typu Double). Klasa SchedulerEvent ma naturalną kolejność według ScheduledTime, ale jest całkowicie możliwa, jeśli zdarzenia, które są nieuporządkowane względem siebie, są nierówne. Klasy SchedulerEventWithMessage i SchedulerEventWithGong mają również naturalne uporządkowania, ale nie w porównaniu z elementami klasy SchedulerEvent.

Załóżmy, że jeden ma dwa zdarzenia SchedulerEventWithMessage X i Y zaplanowane na ten sam czas, ale X.Message to "aardvark", a Y.Message to "zymurgy". ((IComparable <SchedulerEvent>) X) .CompareTo (Y) powinny zgłosić zero (od wydarzeń mają równe razy), ale ((IComparable <SchedulerEventWithMessage>) X) .CompareTo (Y) powinien zwracać liczbę ujemną (ponieważ "aardvark" sortuje przed "zymurgy"). Jeśli klasa nie zachowywałaby się w ten sposób, byłoby trudne lub niemożliwe konsekwentne zamówienie listy zawierającej mieszaninę obiektów SchedulerEventWithMessage i SchedulerEventWithGong.

Nawiasem mówiąc, można argumentować, że byłoby użyteczne, aby semantyka IEquatable <T> porównywała obiekty tylko z podstawą elementów typu T, tak że np. IEquatable <SchedulerEvent> sprawdza ustawienia ScheduledTime i ScheduledAction pod kątem równości, ale nawet w przypadku zastosowania w SchedulerEventWithMessage lub SchedulerEventWithGong nie sprawdzi właściwości Message ani GongVolume. Rzeczywiście, byłoby to przydatne semantyki dla metody IEquatable <T> i wolałbym takie semantyki, ale dla jednego problemu: Porównywarka <T> .Default.GetHashCode (T) zawsze wywołuje tę samą funkcję Object.GetHashCode() niezależnie od typ T. To znacznie ogranicza zdolność IEquatable <T> do różnicowania jej zachowania z różnymi typami T.

+0

Bardzo interesująca odpowiedź. –

19

Oded ma rację, powinieneś zaimplementować oba, ponieważ istnieją kolekcje i inne klasy, które opierają się tylko na jednej z implementacji.

Istnieje jednak pewna sztuczka: IComparable <T> nie powinien generować wyjątków, a IComparable powinno. Podczas implementacji IComparable <T> odpowiadasz za to, że wszystkie wystąpienia T mogą być porównywane ze sobą. Obejmuje to także wartość null (traktuj wartość null jako mniejszą niż wszystkie niepuste instancje T i wszystko będzie dobrze).

Jednak ogólne IComparable akceptuje System.Object i nie można zagwarantować, że wszystkie możliwe obiekty byłyby porównywalne z instancjami T. Dlatego, jeśli otrzymasz wystąpienie inne niż T do IComparable, po prostu wyrzuć System.ArgumentException . W przeciwnym razie przekieruj połączenie do implementacji IComparable <T>.

Oto przykład:

public class Piano : IComparable<Piano>, IComparable 
{ 
    public int CompareTo(Piano other) { ... } 
    ... 
    public int CompareTo(object obj) 
    { 

     if (obj != null && !(obj is Piano)) 
      throw new ArgumentException("Object must be of type Piano."); 

     return CompareTo(obj as Piano); 

    } 
} 

Ten przykład jest częścią znacznie dłuższego artykułu, który zawiera obszerną analizę skutków ubocznych, które należy zająć przy wdrażaniu IComparable <T>: How to Implement IComparable<T> Interface in Base and Derived Classes