4

I have read a few questions on the SO and elsewhere and still do not understand well where this "widening" of a parameter type can be helpful, i.e. compliying to Liskov substitution principle. The following code I took from an answer on the SO, explaining contravariance:

//Contravariance of parameter types: OK
class Food
class FastFood extends Food

class Person { eat(FastFood food) }
class FatPerson extends Person { eat(Food food) }

So I understand that the overridden method accepts more generic paramater than the method in its ancestor. But in practice, how does this help? I mean, if the original method works with certain properties of the derived type, none of this will be available in the derivative using the supertype of the parameter. Therefore, I might have issues with fulfilling the contract postconditions, if those relate to the subtype somehow. Like:

class Animal {}
class Cat { void Meow() void CatSpecificThing()}
...

class A
{
   List<Cat> ListOfCats;
   void X(Cat c)
   {
      c.Meow()
      c.CatSpecificThings()
      ListOfCats.Add(c)

   }
}
class B : A
{
   void X(Animal a)
   {
       //how is this now useful? I cannot do anything that needed Cat
   }
}

Let's say the postcondition of X method is to update the ListOfCats. But in the overriden method in the derived class, I would not be able to do it if there was just the supertype..?

I would be extremely happy for a simple example that demonstrates how this is useful.

  • Your B is an odd duck. It is an A, and thus has a list of cats. Presumably that list of cats is used somehow, which is to say that it is returned by some method. If the same type is both consumed as a parameter and returned as a return type it pretty much has to be invariant rather than contravariant (as we'd like for the parameter) and covariant (as we'd like for a return type). As it stands, your B would have to have its own list of animals, using A's X with only cats... – Erik Eidt Jan 30 '18 at 15:45
3

I think one point where you are possibly getting confused is that contravariance includes the case where the types are equal. It isn't a Liskov violation to use the same type in the arguments of the derived method, so in your example it's perfectly okay to use a Cat instead of an Animal in B. The vast majority of real-world use cases will use the exact same class rather than a wider one.

As an example of where contravariance is useful, consider the following Scala example:

class Pet
class Cat extends Pet
class Kitten extends Cat

class Cats(cats: Cat*) {
  def sorted[B >: Cat](ordering: math.Ordering[B]): Seq[Cat] =
    cats.sorted(ordering)
}

class Kittens(kittens: Kitten*) extends Cats {
  override def sorted[B >: Kitten](ordering: math.Ordering[B]): Seq[Kitten] =
    kittens.sorted(ordering)
}

In Scala, >: is how you specify contravariance. Here, you can sort Kittens using an Ordering[Kitten], but you can also use an Ordering[Cat] or an Ordering[Pet], which makes sense. Cats can only be sorted using an Ordering[Cat], or an Ordering[Pet], so their ordering argument is narrower, it can accept fewer types. It wouldn't make sense to sort Cats by an Ordering[Kitten].

What if we narrowed the argument instead by only accepting Ordering[Kitten] in the sorted for Kittens? That would make the following code fail to type check:

val cats: Cats = new Kittens(new Kitten, new Kitten)
val ordering: math.Ordering[Cat] = math.Ordering.by(_.toString)
cats.sorted(ordering)

This wouldn't type check because the last line wants to call the sorted in class Kittens, but that method doesn't allow an Ordering[Cat]. That failure to type check is why narrowing the method arguments in a subclass violates the Liskov Substitution Principle.

0

In this case it seems useless, but microsoft docs itself gives quite a useful case:

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

In this case you are able to assign a more generic parametered action to a more derived one. Why is this useful? your base class action might already exist, this way you don't have to wrap it into another (lambda) function to create the desired: Action<Derived>

Instead of saying I have this interface why do i need this base class method. Turn your question around: I have this Base class with this method (which is already used), I want to implement the interface. without contravariance you would specifically have to add an extra method.

Other reasons could be that: Derived/Cat class is in an unreachable dependency but de base class is reachable. By being able to use the baseclass your code is less coupled and just as specific as it needs to be (more re-useable).

  • I can see even WIkipedia describes that with the CatShelter example, which in derived class accepts the Animal type (en.wikipedia.org/wiki/…). But I just cannot see jhow this is useful and why – John V Jan 30 '18 at 14:49
  • if CatShelter implementation would also be able to store Objects. (this class is also used directly, not through interface) then you don't need another method to implement the interface. – Joel Harkes Jan 30 '18 at 14:54
  • There is another example in that article: The Circle class inherits Shape and overrides Area. The example creates a SortedSet<T> of Circle objects, using a constructor that takes an IComparer<Circle>. However, instead of passing an IComparer<Circle>, the example passes a ShapeAreaComparer object, which implements IComparer<Shape>. The example can pass a comparer of a less derived type (Shape) when the code calls for a comparer of a more derived type (Circle), because the type parameter of the IComparer<T> generic interface is contravariant. – Erik Eidt Jan 30 '18 at 15:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.