Search code examples
c#interfacepolymorphismbase-classgeneric-collections

Using IComparable in list<T> with different inheritance related types of T


Just some sample classes here. I try to use polymorphism in gerneric Lists, so I want each list to use its own CompareTo-Method. I don't want to find a different class arrangement. I want to understand why that is not working.

class Program
{
    static void Main(string[] args)
    {

        List<Animal> MyAnimals = new List<Animal>();
        MyAnimals.Add(new Dog() { Name = "karl", Number = 1 });
        MyAnimals.Add(new Dog() { Name = "carla", Number = 2 });
        MyAnimals.Add(new Dog() { Name = "loki", Number = 3 });
        MyAnimals.Add(new Cat() { Name = "karsten", Size = 3 });
        MyAnimals.Add(new Cat() { Name = "charlie", Size = 5 });
        MyAnimals.Add(new Cat() { Name = "mona", Size = 1 });
        MyAnimals.Add(new Cat() { Name = "sisi", Size = 2 });
        MyAnimals.Sort();
        ShowList(MyAnimals);
        List<Cat> cats = new List<Cat>();
        List<Dog> dogs = new List<Dog>();

        foreach (var item in MyAnimals)
        {
            if (item is Cat)
                cats.Add(item as Cat);
            if (item is Dog)
                dogs.Add(item as Dog);
        }

        dogs.Reverse();
        dogs.Sort();
        cats.Sort();
        ShowList(dogs);
        ShowList(cats);

        Console.ReadKey();

    }

    private static void ShowList<T>(List<T> MyAnimals)
    {
        Console.WriteLine("List of "+MyAnimals[0].GetType().Name);
        foreach (var item in MyAnimals)
        {
            Console.WriteLine(item);
        }
    }
}

abstract class Animal  : IComparable{
    public string Name { get; set; }

    public int CompareTo(object obj)
    {
        if (obj == null) return 1;

        Animal animal = obj as Animal;
        if (animal != null)
            return this.Name.CompareTo(animal.Name);
        else
            throw new ArgumentException("Object is not a Animal");
    }

    public override string ToString()
    {
        return Name;
    }
}
class Dog : Animal, IComparable
{
    public int Number { get; set; }
    public override string ToString()
    {
        return base.ToString() + "\tNumber:"+ Number + "\n";
    }
    public int CompareTo(object obj)
    {
        if (obj == null) return 1;

        if (obj is Dog animal)
            if (this.Number.CompareTo(animal.Number) == 0)
            {
                return this.Name.CompareTo(animal.Name);
            }
            else
            {
                return this.Number.CompareTo(animal.Number);
            }
        else
            throw new ArgumentException("Object is not a Dog");

    }
}

class Cat : Animal, IComparable
{
    public int Size { get; set; }
    public override string ToString()
    {
        return base.ToString() + "\tNumber:" + Size + "\n";
    }

    public new int CompareTo(object obj)
    {
        if (obj == null) return 1;

        if (obj is Cat animal)
            if (this.Size.CompareTo(animal.Size) == 0)
            {
                return this.Name.CompareTo(animal.Name);
            }
            else
            {
                return this.Size.CompareTo(animal.Size);
            }
        else
            throw new ArgumentException("Object is not a Cat");

    }
}

Why is MyAnimals.Sort() not using the CompareTo() in Animal class? Is there a way to use polymorhpism as it is meant to be? So that List of Animals is Compared by AnimalMethod and Dogs List by Dogs CompareTo Method and so on?


Solution

  • The problem here is that the Dog and Cat classes have a CompareTo method that hides the base-class method, and because Sort will use the ComapareTo method of the item's actual type instead of the type defined for the items on the List<T>, you will get an exception when trying to compare a Dog with a Cat.

    One way to resolve this is to implement the IComparable<T> interface instead, so that we specify the exact type we want to compare an object with. This way, the correct implementation will be used by the Sort method.

    Also note that instead of re-implementing the CompareTo method of Animal (which compares the Name properties) for each derived class, we can simply call base.CompareTo(other) from the derived class's CompareTo method when necessary:

    abstract class Animal : IComparable<Animal>
    {
        public string Name { get; set; }
    
        public int CompareTo(Animal other)
        {
            if (other == null) return 1;
            return string.Compare(Name, other.Name);
        }
    
        public override string ToString()
        {
            return Name;
        }
    }
    
    class Dog : Animal, IComparable<Dog>
    {
        public int Number { get; set; }
    
        public override string ToString()
        {
            return base.ToString() + "\tNumber:" + Number + "\n";
        }
    
        public int CompareTo(Dog other)
        {
            if (other == null) return 1;
            var numberCompare = Number.CompareTo(other.Number);
            return numberCompare == 0 ? base.CompareTo(other) : numberCompare;
        }
    }
    
    class Cat : Animal, IComparable<Cat>
    {
        public int Size { get; set; }
    
        public override string ToString()
        {
            return base.ToString() + "\tNumber:" + Size + "\n";
        }
    
        public int CompareTo(Cat other)
        {
            if (other == null) return 1;
            var sizeCompare = Size.CompareTo(other.Size);
            return sizeCompare == 0 ? base.CompareTo(other) : sizeCompare;
        }
    }
    

    Also, you might want to change your ShowList method to show the type T rather then the derived type of the first element in the list (otherwise the List<Animal> displays that it's a List<Dog> because the first item is a Dog):

    private static void ShowList<T>(List<T> MyAnimals)
    {
        Console.WriteLine("List of " + typeof(T).Name);
    
        foreach (var item in MyAnimals)
        {
            Console.WriteLine(item);
        }
    }