Czas na małe szaleństwo z interfejsami, łączeniem konstruktorów, przesłanianiem metod i metodami rozszerzającymi!
Wyobraźmy sobie, że mamy tablicę własnych obiektów np samochodów. Chcemy posortować te samochody raz po identyfikatorze, raz po prędkości maksymalnej, a jeszcze innym razem według koloru.
Jest to bardzo proste dzięki zastosowaniu interfejsu
IComparer (zdefiniowany w System.Collections). Przy okazji skorzystamy z pozostałych wcześniej wymienionych technik.
Należy pamiętać że stosowanie interfejsu IComparer to tylko jedna z wielu możliwości posortowania obiektów. Można użyć LINQ, delegatów i anonimowych metod a nawet napisać własną funkcję sortującą.
Dzisiaj skoncentrujemy się jednak na IComparerze. Więc zaczynamy.
Klasa Car
W prostym projekcie konsolowym stworzyłem sobie nowy plik z klasą Car która na początku wyglądała tak:
class Car
{
public int Id { get; set; }
public int Vmax { get; set; }
public string Color { get; set; }
public Car() : this (1, 100, "Black") { }
public Car(int Id, int Vmax, string Color)
{
this.Id = Id;
this.Vmax = Vmax;
this.Color = Color;
}
}
Mamy tutaj proste pola Id, Vmax, Color utworzone za pomocą skróconej wersji {get; set;}.
Dla przykładu zastosowane zostało też łączenie konstruktorów (constructor chaining). Domyślny bezparametrowy konstruktor przed wykonaniem swojej pracy uruchamia konstruktor "główny" który wykonuje całą pracę polegającą w tym przypadku na przypisaniu odpowiednich wartości do pól.
Utwórzmy zatem tablicę obiektów Car!
W głównej metodzie programu możemy napisać na przykład:
static void Main(string[] args)
{
Car[] cars = {
new Car(2, 200, "Green"),
new Car(),
new Car(3, 180, "Blue"),
new Car(4, 190, "Yellow")
};
Console.ReadLine();
}
}
Teraz możemy wyświetlić tą tablicę.
Jednak napotkamy tutaj pewien problem. Chcielibyśmy mieć możliwość wyświetlenia całej tablicy za pomocą jednego wiersza kodu zamiast pisania na przykład pętli foreach w każdym miejscu gdzie wyświetlenie takiej tablicy byłoby potrzebne. Ponieważ jest to zwykła tablica własnych obiektów nie ma domyślnie żadnej metody wypisującej wszystkie samochody. Idealnym rozwiązaniem byłaby edycja klasy Array i dodanie odpowiedniej metody. Lecz czy możemy gdzieś edytować klasę Array? Raczej nie bardzo...
Z pomocą przychodzą nam metody rozszerzające.
Metody rozszerzające
Nawet jeśli edycja kodu jakiejś klasy jest niemożliwa (np jeśli korzystamy ze skompilowanej biblioteki). Możemy rozszerzyć jej funkcjonalność właśnie za pomocą metod rozszerzających.
Musimy utworzyć nową klasę z rozszerzeniami. Najlepiej wstawić ją do osobnego pliku w projekcie.
Klasa ta musi być statyczna i może wyglądać tak:
static class Extensions
{
public static void DisplayArray(this Car[] cars)
{
foreach (Car c in cars)
{
Console.WriteLine(c.ToString());
}
}
}
Wszystko opiera się o tajemniczy parametr wewnątrz nawiasów: DisplayArray(
this Car[] cars).
Słowo kluczowe this jest użyte tutaj w takim kontekście aby poinformować kompilator że definiujemy właśnie metodę rozszerzającą, która ma być zastosowana do tablicy typu Car!
Jeśli teraz chcielibyśmy wyświetlić całą tablicę samochodów możemy to zrobić bardzo prosto.
static void Main(string[] args)
{
Car[] cars = {
new Car(2, 200, "Green"),
new Car(),
new Car(3, 180, "Blue"),
new Car(4, 190, "Yellow")
};
cars.DisplayArray();
Console.ReadLine();
}
Zwróćmy uwagę, że gdy po kropce zaczynamy wpisywać nazwę funkcji DisplayArray() to IntelliSense już widzi naszą metodę i dodatkowo oznacza ją niebieską strzałką, która oznacza że jest to właśnie metoda rozszerzająca:
Jeszcze tylko przeciążenie metody ToString() na obiekcie Car i możemy działać dalej:
class Car
{
public int Id { get; set; }
public int Vmax { get; set; }
public string Color { get; set; }
public Car() : this (1, 100, "Black") { }
public Car(int Id, int Vmax, string Color)
{
this.Id = Id;
this.Vmax = Vmax;
this.Color = Color;
}
public override string ToString()
{
return String.Format("ID: {0}\tVmax: {1}\tColor: {2}", Id, Vmax, Color);
}
}
Ponieważ każdy obiekt zawiera definicję metody ToString() musimy ją przeciążyć używając słowa kluczowego override.
Po skompilowaniu i uruchomieniu programu widzimy pierwsze rezultaty:
Sortowanie
Nareszcie przechodzimy do głównego problemu: sortowania obiektów.
Będziemy posługiwać się funkcją statyczną Array.Sort()
Jest ona wielokrotnie przeciążana więc możemy podać tutaj 2 parametry. Pierwszym parametrem będzie oczywiście nasza tablica do posortowania a drugim parametrem będzie obiekt implementujący interfejs IComparer.
Tworzymy zatem nowy plik np CarComparers.cs a w nim definicje trzech klas:
class CarColorComparer : IComparer
{
int IComparer.Compare(object o1, object o2)
{
Car t1 = o1 as Car;
Car t2 = o2 as Car;
if (t1 != null && t2 != null)
return String.Compare(t1.Color, t2.Color);
else
throw new ArgumentException("Parameter is not a Car!");
}
}
class CarIdComparer : IComparer
{
int IComparer.Compare(object o1, object o2)
{
Car t1 = o1 as Car;
Car t2 = o2 as Car;
if (t1 != null && t2 != null)
{
return t1.Id.CompareTo(t2.Id);
}
else
throw new ArgumentException("Parameter is not a Car!");
}
}
class CarVmaxComparer : IComparer
{
int IComparer.Compare(object o1, object o2)
{
Car t1 = o1 as Car;
Car t2 = o2 as Car;
if (t1 != null && t2 != null)
{
if (t1.Vmax > t2.Vmax) return 1;
else if (t1.Vmax < t2.Vmax) return -1;
else return 0;
}
else
throw new ArgumentException("Parameter is not a Car!");
}
}
Każda klasa implementuje interfejs IComparer więc musi posiadać metodę Compare przyjmującą dwa obiekty. W każdej metodzie rzutujemy je na typ Car. Jeśli któryś z obiektów nie jest typem Car to zostanie wyrzucony wyjątek.
Metoda Compare powinna zwracać 1 jeśli pierwszy obiekt jest "większy" od drugiego, -1 gdy jest "mniejszy" a 0 gdy oba obiekty traktujemy na równi.
W przypadku porównywania koloru czyli typu string możemy zaoszczędzić kodowania wywołując metodę String.Compare(t1.Color, t2.Color);
W przypadku inta w jednej metodzie wywołaliśmy funkcję CompareTo() a w drugiej zaimplementowaliśmy własne porównywanie.
Moglibyśmy użyć już konstrukcji Array.Sort(cars, new CarIdComparer()); jednak zrobimy to bardziej elegancko uniezależniając się od znajomości klas obiektów porównujących.
Dodajmy do klasy Car odpowiednie pola statyczne zwracające odpowiednie obiekty porównujące tym samym uzyskując ostateczną wersję klasy Car:
class Car
{
public int Id { get; set; }
public int Vmax { get; set; }
public string Color { get; set; }
public Car() : this (1, 100, "Black") { }
public Car(int Id, int Vmax, string Color)
{
this.Id = Id;
this.Vmax = Vmax;
this.Color = Color;
}
public static IComparer SortById
{ get { return (IComparer)new CarIdComparer(); } }
public static IComparer SortByVmax
{ get { return (IComparer)new CarVmaxComparer(); } }
public static IComparer SortByColor
{ get { return (IComparer)new CarColorComparer(); } }
public override string ToString()
{
return String.Format("ID: {0}\tVmax: {1}\tColor: {2}", Id, Vmax, Color);
}
}
Pola zawierają jedynie gettery zwracające obiekty implementujące odpowiednie porównania, czyli to o co nam chodziło.
Teraz aby przetestować działanie programu w głownej metodzie napiszmy:
static void Main(string[] args)
{
Car[] cars = {
new Car(2, 200, "Green"),
new Car(),
new Car(3, 180, "Blue"),
new Car(4, 190, "Yellow")
};
Console.WriteLine("Default order:");
cars.DisplayArray();
Console.WriteLine();
Console.WriteLine("Sorted by ID:");
Array.Sort(cars, Car.SortById);
cars.DisplayArray();
Console.WriteLine();
Console.WriteLine("Sorted by Vmax:");
Array.Sort(cars, Car.SortByVmax);
cars.DisplayArray();
Console.WriteLine();
Console.WriteLine("Sorted by Color:");
Array.Sort(cars, Car.SortByColor);
cars.DisplayArray();
Console.WriteLine();
Console.ReadLine();
}
Dokładając trochę pracy do zdefiniowania obiektów porównujących otrzymaliśmy eleganckie sortowanie po wielu właściwościach: