LektionsBloggen: Möjliggöra sortering av en List<T>

Lektionens datum: 2/7 2008
Lektionens rubrik: Generics, Anonyma funktioner (1/3)
Lektionsbloggens fokus: Hur objekt av en klass kan bli sorteringsbara

Dagens lektion täckte in en hel del och känns minst sagt knepig att sammanfatta. Jag väljer att fokusera på hur objekt av en klass kan bli sorteringsbara.

Kortfattat om Generics
Generics gör det möjligt att implementera en klass som kan hantera objekt av vilken typ som helst. Till exempel kan klassen List<T>, som ingår i .Net Framework, användas för att skapa en lista av vilken slags objekt som helst. Om du har gjort en klass som heter Employee kan du skapa en lista av Employee-objekt enligt följande;

            //Skapa några Employee-objekt
            Employee frida = new Employee("Frida", 10000);
            Employee kalle = new Employee("Kalle", 9000);
            Employee fredrika = new Employee("Fredrika", 12000);

            //Skapa en lista av Employee-objekt
            List<Employee> listOfEmployees = new List<Employee>();
            
            //Lägg till Employee-objekt i listan
            listOfEmployees.Add(frida);
            listOfEmployees.Add(kalle);
            listOfEmployees.Add(fredrika);

Syntaxen som är associerad med Generics är den html-taggliknande konstruktionen där man mellan taggarna ska skriva den typ/klass man vill att den generiska klassen ska hantera, därav List<Employee> Mer om poängen med Generics kan du läsa här och här.

Göra objekt av en klass sorteringsbara så att List<T>.Sort() kan anropas
List<T> innehåller ett antal funktioner som kan användas för att hantera de objekt som ingår i listan, till exempel sortering. Men för att objekt av en viss typ ska kunna sorteras måste klassen implementera ett interface som heter IComparable, detta interface finns dessutom i en generisk version, IComparable<T>, och eftersom generics är bra (läs någon av länkarna ovan, eller någon bok ;-) så använder vi den. För att inte röra ihop det här med WinForms-grejer blir detta exempel en Console Application. Jag kör vidare på mitt djurtema och skapar följande klass:

    class Dog
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Breed { get; set; }

        public Dog(string name, int age, string breed)
        {
            Name = name;
            Age = age;
            Breed = breed;
        }

        public override string ToString()
        {
            return string.Format("{0} the {1} is {2} years old.", Name, Breed, Age);
        }
    }

I Main-metoden gör jag sedan följande:

        static void Main(string[] args)
        {
            List<Dog> dogList = new List<Dog>();

            dogList.Add(new Dog("Mozart", 2, "Cocker Spaniel"));
            dogList.Add(new Dog("Barker", 1, "Labrador"));
            dogList.Add(new Dog("Fido", 3, "Poodle"));

            //Här blir det runtime-fel, vad ska en dog sorteras på?
            dogList.Sort();

            foreach (Dog dog in dogList)
            {
                Console.WriteLine(dog.ToString());
            }

            Console.ReadLine();
        }

Jag skapar en lista som hanterar Dog-objekt och lägger till ett antal sådana till listan. Därefter är tanken att listan ska sortas med List-klassens Sort-metod och därefter ska listan skrivas ut till skärmen. Men precis som det står i kommentaren ovan så kommer det här inte att fungera. För hur ska Sort-metoden, som kan hantera precis vilken typ som helst, kunna veta hur objekt av Dog ska sorteras? Ska de sortas på namn, ålder eller ras? Dog måste som sagt implementera interfacet IComparable<T>. Här ser du tilläggen i Dog efter att den ändrats:

    class Dog : IComparable<Dog>
    {
        /.../

        #region IComparable<Dog> Members

        public int CompareTo(Dog other)
        {
            return this.Name.CompareTo(other.Name);
        }

        #endregion
    }

Här ser vi hur klassen Dog implementerat IComparable<Dog>. Interfacet kräver en metod som heter CompareTo, returnerar int och tar emot ett objekt av typen Dog. Denna metod kommer nu att användas av List-klassens Sort-metod när den sorterar objekten. Notera att jag i denna implementation valt att sortera objekten på Name. Det kan jag göra rätt lätt eftersom Name är en string och även string-klassen implementerar IComparable så genom att anropa stringklassens CompareTo-metod kan jag få ut det int-värde som metoden borde returnera. Int-värdet använder förresten Sort-metoden för att avgöra hur objekten ska sorteras, beroende på om värdet är positivt, negativt eller noll. Sådär, då har vi gjort det möjligt att sortera på namn och när programmet nu körs får man följande utskrift:

Barker the Labrador is 1 years old.
Fido the Poodle is 3 years old.
Mozart the Cocker Spaniel is 2 years old.

Men hur ska vi göra om vi även vill att det ska vara möjligt att sortera på ålder? För att hantera detta får vi undersöka de olika överlagringar som finns av List-klassens Sort-metod. En av överlagringarna kräver att man skickar in ett objekt av en typ som implementerar interfacet IComparer<Dog>;

 

Så vi skapar en ny klass, DogComparer, som implementerar detta interface. Eftersom denna klass är hårt knuten till Dog känns det logiskt att nästla denna klass inuti Dog-klassen;

    class Dog : IComparable<Dog>
    {

        public class DogComparer : IComparer<Dog>
        {
            #region IComparer<Dog> Members

            public int Compare(Dog x, Dog y)
            {
                return x.Age.CompareTo(y.Age);
            }

            #endregion
        }

        /.../
    }

Icomparer<Dog> kräver alltså en metod som heter Compare, returnerar en int och tar emot två Dog-objekt. I övrigt skiljer det sig inte nämnvärt från IComparables CompareTo. Nu kan vi göra anropet till sort på något av följande två vis:

            //Alternativ 1
            Dog.DogComparer compareByAge = new Dog.DogComparer();
            dogList.Sort(compareByAge);                    
            
            //Alternativ 2
            dogList.Sort(new Dog.DogComparer());

Enda skillnaden här är tydligheten. I alternativ 1 skapar vi först en referens och skickar sedan in den, i alternativ 2 uttrycks samma sak på en rad. Och eftersom vi aldrig kommer behöva använda referensen compareByAge till någonting annat kan man lika gärna skiva enligt alternativ 2. När vi skickar in DogComparer-objektet i Sort-metoden uppfyller vi alltså överlagringen i bilden ovan, vi skickar in ett objekt som implementerar IComparer<Dog>.

Sist men inte minst ska vi göra så att man också kan sortera på ras. En variant skulle kunna vara att våran DogComparer-klass skulle kunna hetat DogAgeComparer och att vi nu skulle göra ännu en klass som som också implementerar IComparer men som istället heter DogBreedComparer. Men då blir det lätt väldigt många klasser om du tänker dig en klass med betydligt fler fält än dessa tre... En annan variant är att låta en variabel styra vilken sortering som ska göras, och här kommer jag skilja mig lite från det sätt som vår lärare visade idag till förmån för ett sätt jag upptäckte i ett av de exempel som läraren gick igenom (ListDemo, för er som var med ;-). Eftersom det kommer att finnas ett fast antal alternativ för sorteringen verkar det lämpligt att deklarera en enum, CompareType, och sedan låta DogComparer ha en static medlem av CompareType. Dessutom implementerar jag en constructor som tar emot en CompareType, så att man när man skapar ett sådant objekt direkt kan välja vilken sortering man vill ha. Därefter ändrar jag också i CompareTo-metoden så att den tar hänsyn till vilken sortering som ska användas. Hela klassen DogComparer ser nu ut så här:

public class DogComparer : IComparer<Dog>
        {
            //Enum
            public enum CompareType
            {
                ByAge,
                ByBreed
            }

            //Enum-medlem
            public static CompareType CompareBy { get; set; }

            //Constructor
            public DogComparer(CompareType compareBy)
            {
                CompareBy = compareBy;
            }

            public DogComparer() { }    //Om denna konstruktorn anropas kommer CompareBy ha 
                                        //värde ByAge eftersom den är först i enumeratorn

            #region IComparer<Dog> Members

            public int Compare(Dog x, Dog y)
            {
                int returnValue = 0;

                //Ta hänsyn till sorteringsval
                switch (CompareBy)
                {
                    case CompareType.ByAge:
                        returnValue = x.Age.CompareTo(y.Age);
                        break;
                    case CompareType.ByBreed:
                        returnValue = x.Breed.CompareTo(y.Breed);
                        break;
                }

                return returnValue;
            }

            #endregion
        }

Anropet till Sort-metoden kan nu göras så här för att sortera på ålder respektive ras;

            dogList.Sort(new Dog.DogComparer(Dog.DogComparer.CompareType.ByAge));
            dogList.Sort(new Dog.DogComparer(Dog.DogComparer.CompareType.ByBreed));

Här stannar jag för idag. Observera att lektionen i övrigt även tog upp List<T>-metoderna FindAll, ForEach och ConvertAll. Exempel finns i lärarens demo ListDemo. Imorgon fortsätter utbildningen på samma tema.

Ladda hem "the LektionsBloggen Solution"

No comments :

Post a Comment