Sortowania ciąg dalszy :)

W jednym z poprzednich wpisów (link) pokazałem Wam mechanizmy platformy .NET służące do sortowania kolekcji typów generycznych.
Dziś chciałbym nieco rozwinąć ten temat. Dodamy możliwość określenia właściwości oraz porządku sortowania, a także zaprzęgniemy do pracy mechanizm refleksji oferowany przez .NET.

Dla przykładu użyjemy klasy Movie z poprzedniego artykułu, która wygląda następująco:

public class Movie
{
    #region Public members
    public int ID { get; set; }
    public string Title { get; set; }
    public string Director { get; set; }
    public string Genre { get; set; }
    public int Year { get; set; }
    #endregion

    #region Constructor
    public Movie(int id, string title, string director, string genre, int year)
    {
        ID = id;
        Title = title;
        Director = director;
        Genre = genre;
        Year = year;
    }
    #endregion

    #region Methods
    public override string ToString()
    {
        return string.Format("ID: {0}, Name: {1}, Director: {2}, Genre: {3}, Year: {4}", Id, Title, Director, Genre, Year);
    }
    #endregion
}

W poprzednim wpisie pokazałem zastosowanie interfejsu IComparer<T> i utworzyliśmy kilka drobnych klas sortujących według określonej właściwości, np.: MovieComparerByGenre, MovieComparerByTitle itd. Poniżej rozwiązanie bardziej ogólne łączące kilka drobnych klas w jedną:

public class MovieComparer : IComparer<Movie>
{
    #region Private members
    private string _propertyName;
    #endregion

    #region Public members
    public string PropertyName
    {
        get { return _propertyName; }
        set
        {
            if (!typeof(Movie).HasProperty(value))
            {
                throw new ArgumentException(string.Format("Type: '{0}' does not have property '{1}'.", typeof(Movie), value));
            }
            _propertyName = value;
        }
    }
    public SortingOrder SortingOrder { get; set; }
    #endregion

    #region Constructor
    public MovieComparer(string propertyName, SortingOrder sortOrder)
    {
        PropertyName = propertyName;
        SortingOrder = sortOrder;
    }
    #endregion

    #region IComparer member
    public int Compare(Movie movie1, Movie movie2)
    {
        int result;
        if (movie1 == null && movie2 == null)
        {
            result = 0;
        }
        else if (movie1 == null)
        {
            result = this.SortingOrder == SortingOrder.Ascending ? -1 : 1;
        }
        else if (movie2 == null)
        {
            result = this.SortingOrder == SortingOrder.Ascending ? 1 : -1;
        }
        else
        {
            switch (_propertyName)
            {
                case "Title":
                    result = this.SortingOrder == SortingOrder.Ascending ? movie1.Title.CompareTo(movie2.Title)
                                                                         : movie2.Title.CompareTo(movie1.Title);
                    break;
                case "Director":
                    result = this.SortingOrder == SortingOrder.Ascending ? movie1.Director.CompareTo(movie2.Director)
                                                                         : movie2.Director.CompareTo(movie1.Director);
                    break;
                case "Genre":
                    result = this.SortingOrder == SortingOrder.Ascending ? movie1.Genre.CompareTo(movie2.Genre)
                                                                         : movie2.Genre.CompareTo(movie1.Genre);
                    break;
                case "Year":
                    result = this.SortingOrder == SortingOrder.Ascending ? movie1.Year.CompareTo(movie2.Year)
                                                                         : movie2.Year.CompareTo(movie1.Year);
                    break;
                default:    // ID
                    result = this.SortingOrder == SortingOrder.Ascending ? movie1.ID.CompareTo(movie2.ID)
                                                                         : movie2.ID.CompareTo(movie1.ID);
                    break;
            }
        }
        return result;
    }
    #endregion
}

Wywołanie:

var movieComparer = new MovieComparer("Title", SortingOrder.Ascending);
movies.Sort(movieComparer);

movieComparer.PropertyName = "Year";
movieComparer.SortingOrder = SortingOrder.Descending;
movies.Sort(movieComparer);

Właściwość, po której chcemy posortować kolekcję oraz porządek sortowania możemy przekazać za pomocą konstruktora klasy MovieComparer lub ustawiając odpowiednie właściwości obiektu tej klasy. Dodałem również warunki sprawdzające obiekty pod kątem istnienia ich instancji oraz dodatkowo, sprawdzamy za pomocą mechanizmu refleksji, czy podana przez nas właściwość należy do klasy Movie i w przypadku błędnej nazwy rzucany jest odpowiedni wyjątek.

Chcąc zastosować ten mechanizm do sortowania innych klas musimy dla każdej z nich utworzyć klasę porównującą. W związku z powyższym warto wydzielić klasę bazową, która będzie zawierała wszystkie wspólne elementy mechanizmu porównującego w celu uniknięcia duplikacji kodu (zasada DRY). Poniżej kod abstrakcyjnej klasy bazowej:

public abstract class ComparerBase<T> : IComparer<T>
{
    #region Private members
    private string _propertyName;
    #endregion

    #region Public members
    public string PropertyName
    {
        get { return _propertyName; }
        set
        {
            if (!typeof(T).HasProperty(value))
            {
                throw new ArgumentException(string.Format("Type: '{0}' does not have property '{1}'.", typeof(T).FullName, value));
            }
            _propertyName = value;
        }
    }

    public SortingOrder SortingOrder { get; set; }
    #endregion

    #region Constructor
    public ComparerBase(string propertyName, SortingOrder sortingOrder = SortingOrder.Ascending)
    {
        PropertyName = propertyName;
        SortingOrder = sortingOrder;
    }
    #endregion

    #region Public methods
    public int Compare(T object1, T object2)
    {
        int result;
        if (object1 == null && object2 == null)
        {
            result = 0;
        }
        else if (object1 == null)
        {
            result = this.SortingOrder == SortingOrder.Ascending ? -1 : 1;
        }
        else if (object2 == null)
        {
            result = this.SortingOrder == SortingOrder.Ascending ? 1 : -1;
        }
        else
        {
            result = CompareMain(object1, object2);
        }
        return result;
    }
    #endregion

    #region Private methods
    protected abstract int CompareMain(T object1, T object2);
    #endregion
}

Zastosowałem tutaj mały trick. Otóż w klasach dziedziczących po ComparerBase, nie będziemy przeciążali metody Compare, która zostanie stała dla wszystkich podklas i zawiera początkowe warunki sprawdzające pod kątem wartości null. Właściwe porównanie konkretnych obiektów zawrzemy w metodzie CompareMain i tą metodę należy przeciążyć w klasach podrzędnych. Poniżej poprawiony kod klasy MovieComparer:

public class MovieComparer : ComparerBase<Movie>
{
    #region Constructor
    public MovieComparer(string propertyName, SortingOrder sortingOrder)
        : base(propertyName, sortingOrder) { }
    #endregion

    #region Private methods
    protected override int CompareMain(Movie movie1, Movie movie2)
    {
        int result;
        switch (PropertyName)
        {
            case "Title":
                result = this.SortingOrder == SortingOrder.Ascending ? movie1.Title.CompareTo(movie2.Title)
                                                                     : movie2.Title.CompareTo(movie1.Title);
                break;
            case "Director":
                result = this.SortingOrder == SortingOrder.Ascending ? movie1.Director.CompareTo(movie2.Director)
                                                                     : movie2.Director.CompareTo(movie1.Director);
                break;
            case "Genre":
                result = this.SortingOrder == SortingOrder.Ascending ? movie1.Genre.CompareTo(movie2.Genre)
                                                                     : movie2.Genre.CompareTo(movie1.Genre);
                break;
            case "Year":
                result = this.SortingOrder == SortingOrder.Ascending ? movie1.Year.CompareTo(movie2.Year)
                                                                     : movie2.Year.CompareTo(movie1.Year);
                break;
            default:    // ID
                result = this.SortingOrder == SortingOrder.Ascending ? movie1.ID.CompareTo(movie2.ID)
                                                                     : movie2.ID.CompareTo(movie1.ID);
               break;
        }
        return result;
    }
    #endregion
}

Jak widać kod został znacznie uproszczony a jedyne co musimy zrobić, to wywołać konstruktor klasy bazowej z odpowiednimi parametrami. Sposób wywołania sortowania pozostał bez zmian.

Mając solidne podstawy możemy się pokusić o napisanie uniwersalnej generycznej klasy porównującej wykorzystującej mechanizm refleksji.

public class GenericComparer<T> : ComparerBase<T>
{
    #region Constructor
    public GenericComparer(string propertyName, SortingOrder sortingOrder = SortingOrder.Ascending)
        : base(propertyName, sortingOrder) { }
    #endregion

    #region Private methods
    protected override int CompareMain(T object1, T object2)
    {
        PropertyInfo propertyInfo = typeof(T).GetProperty(PropertyName);

        IComparable value1 = (IComparable)propertyInfo.GetValue(object1, null);
        IComparable value2 = (IComparable)propertyInfo.GetValue(object2, null);

        return SortingOrder == SortingOrder.Ascending ? value1.CompareTo(value2) : value2.CompareTo(value1);
    }
    #endregion
}

Wywołanie sortowania, nie różni się niczym w stosunku do standardowej klasy porównującej:

var genericComparer = new GenericComparer<Movie>("Title", SortingOrder.Ascending);
movies.Sort(genericComparer);

genericComparer.PropertyName = "Year";
genericComparer.SortingOrder = SortingOrder.Descending;
movies.Sort(genericComparer);

Niewątpliwie zaletą stosowania klasy GenericComparer jest to, że pozwala nam zaoszczędzić sporu czasu i miejsca na pisaniu kodu porównującego specjalnie dla każdej klasy, której elementy chcemy w przyszłości sortować. Jedynym problemem, który możemy napotkać podczas pracy z tą klasą, to kwestie wydajnościowe, ze względu na to, że stosowanie mechanizmu refleksji nie należy do najszybszych. W przypadku małych lub średnich kolekcji, spadek wydajności jest praktycznie niezauważalny gołym okiem (przynajmniej jak dla mnie) – testowałem sortowanie kolekcji około 2500-3000 obiektów z kilkunastoma właściwościami. Problem może się pojawić w przypadku większych kolekcji ale tutaj nie mam żadnych danych. Zachęcam Was do testów i podzielenia się ewentualnymi wynikami oraz spostrzeżeniami.

Kolejną rzeczą, o której chcę wspomnieć jest sortowanie w przypadku kiedy klasa posiada również właściwości złożone (dla przyp. typy proste to int, float i poniekąd string). Co więc powinniśmy zrobić w przypadku kiedy nasza klasa Movie zawierałaby właściwość Director, która byłaby typu Person:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public override string ToString()
    {
        return string.Format("{0}, {1}", LastName, FirstName);
    }
}

Teraz rozszerzona klasa Movie:

public class Movie
{
    #region Public members
    public int ID { get; set; }
    public string Title { get; set; }
    public Person Director { get; set; }
    public string Genre { get; set; }
    public int Year { get; set; }
    #endregion

    #region Constructors
    public Movie(int id, string title, string genre, int year, string directorFirstName, string directorLastName)
        : this(id, title, genre, year, new Person(directorFirstName, directorLastName)) { }

    public Movie(int id, string title, string genre, int year, Person director)
    {
        ID = id;
        Title = title;
        Director = director;
        Genre = genre;
        Year = year;
    }
    #endregion

    #region Public methods
    public override string ToString()
    {
        return string.Format("ID: {0}, Name: {1}, Director: {2}, Genre: {3}, Year: {4}", ID, Title, Director, Genre, Year);
    }
    #endregion
}

Powstają tutaj dwa problemy:
1. Po zmianach został nam kawałek kodu w klasie MovieComparer, który będzie uniemożliwiał kompilację programu:

case "Director":
    result = this.SortingOrder == SortingOrder.Ascending ? movie1.Director.CompareTo(movie2.Director)
                                                         : movie2.Director.CompareTo(movie1.Director);
    break;

Problem polega na tym, że kompilator nie ma pojęcia w jaki sposób porównać wartości właściwości Director dwóch obiektów, ponieważ nasza klasa Person nie implementuje interfejsu IComparable, a tym samym nie udostępnia metody CompareTo. W tym celu należy nieznacznie zmodyfikować kod klasy Person:

public class Person : IComparable
{
    [...]
    #region IComparable Members
    public int CompareTo(object other)
    {
        var otherPerson = (Person)other;
        int result;
        if (otherPerson == null)
        {
            result = 1;
        }
        else
        {
            result = this.LastName.CompareTo(otherPerson.LastName);
        }
        return result;
    }
    #endregion
    [...]
}

2. Jeśli w trakcie działania programu przekażemy do klasy MovieComparer właściwość w postaci np. „Director.FirstName”, kompilator rzuci wyjątek o błędnym argumencie, ponieważ nie znajdzie takiej właściwości. W tym przypadku można użyć np. rekurencji w połączeniu z mechanizmem refleksji, w celu odnalezienia określonej właściwości ale to już wykracza poza ramy tego wpisu. Na szczęście prostszym rozwiązaniem jest tzw. spłaszczenie właściwości, co można zrobić dodając do klasy Movie właściwość tylko do odczytu o nazwie DirectorFirstName zwracającą imię reżysera:

public string DirectorFirstName
{
    get { return Director != null ? Director.FirstName : string.Empty; }
}

a następnie dodanie warunku sortowania po tej właściwości do klasy MovieComparer:

case "DirectorFirstName":
    result = this.SortingOrder == SortingOrder.Ascending ? movie1.DirectorFirstName.CompareTo(movie2.DirectorFirstName)
                                                         : movie2.DirectorFirstName.CompareTo(movie1.DirectorFirstName);
    break;

Analogicznie postępujemy w przypadku właściwości „DirectorLastName”.

Mam nadzieję, że oba wpisy pomogły Wam zrozumieć podstawy sortowania kolekcji. Przykład do dzisiejszego wpisu znajdziecie poniżej.

Sortowanie2
Sortowanie2
Sortowanie2.zip
15.8 KiB
Pobrano 40 razy
Szczegóły...

Tagi:, , ,

Nie ma jeszcze komentarzy.

Dodaj komentarz

Uzupełnij * Time limit is exhausted. Please reload the CAPTCHA.