riki
riki

Reputation: 2403

What is not a covariant in OOPS

I have been trying understand the idea of covariant along with the Liskov substitution principle with some of the examples I found in the internet. This is one the those example I am currently following and it kind of confused me to understand what is not a covariant.

Let us have an example:

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
}

public class EmployeeExtendedDetail : Employee
{
    public string Address { get; set; }
    public string Email { get; set; }
}

public class EmployeeRepository
{
    public virtual Employee GetEmployeeDetialById(int id)
    {
        return new Employee();
    }
}

public class EmployeeExtendedDetailRepository : EmployeeRepository
{
    public override EmployeeExtendedDetail GetEmployeeDetialById(int id)
    {
        return new EmployeeExtendedDetail();
    }
}

In the sub-type EmployeeExtendedDetailRepository the return type of the method GetEmployeeDetialById is changed to EmployeeExtendedDetail, following the example in the above article.

Now my understanding of covariant is that two entities are called covariant when for example we can assign an instance of List<derived> to an instance of List<base>. I want to keep the generic type and out/in parameters aside for now and just wanted to understand from the above example.

Now from the above example:

L1. EmployeeRepository empRepo1 = new EmployeeExtendedDetailRepository();
L2. Employee employee2 = empRepo1.GetEmployeeDetialById(1); // OK
L3. EmployeeExtendedDetail employee3 = empRepo1.GetEmployeeDetialById(1); //ERROR - cannot implicitly convert
L4. EmployeeExtendedDetail employee4 = (EmployeeExtendedDetail)empRepo1.GetEmployeeDetialById(1); // OK

L5. EmployeeExtendedDetailRepository empRepo2 = new EmployeeExtendedDetailRepository();
L6. Employee employee5 = empRepo2.GetEmployeeDetialById(1); // OK
L7. EmployeeExtendedDetail employee6 = empRepo2.GetEmployeeDetialById(1); // OK

From the above code in the L1 on call of empRepo1.GetEmployeeDetialById(1) will execute the overridden version the method GetEmployeeDetialById and should return a type of EmployeeExtendedDetail and L2 we are able to assign a sub-type to a super-type. Does it mean they are covariant?? Same for the L6.

Again in L3 requires a explicit conversion.

Well, in my honest words I am simply not able to understand the idea of what is not a covariant in general.

Upvotes: 0

Views: 67

Answers (2)

Mark Seemann
Mark Seemann

Reputation: 233307

I don't have much to add to Johnathan Barclay's answer, which is essentially correct. Depending on where in your learning journey you are, you may or may not find my article Functor variance compared to C#'s notion of variance useful.

I do, however, have one nit to pick. While I do realize that Barbara Liskov's original paper did 'explain' the Substitution Principle (LSP) with wording similar to 'subtype may not change behaviour' (my emphasis), after having taught this for a decade, I find that this choice of words confuse people rather than help them.

What the LSP really states is that

  • Subtypes must be contravariant in input
  • Subtypes must be covariant in output
  • Preconditions must not be strengthened in the subtype
  • Postconditions must not be weakened in the subtype

This principle is closely related to Postel's law that says that you should be liberal in what you accept, but conservative in what you send.

Here are two more articles on the topic:

One can present this in quite abstract and technical terms (which I do in those articles), but in reality, the LSP is quite intuitive if you look at it from the perspective of client code.

If client code only knows that it deals with a supertype, it expects to be able to invoke an operation with all sorts of input. Since it doesn't know which subtype it may be dealing with, it'll expect to use the full range of possible input values. Thus, a subtype must accept all input values, because if it doesn't, it's going to break the client code. It can, on the other hand, be more liberal with what it accepts, since that's not going to break client code. This is called contravariance.

Likewise, client code expects return values (or posterior state) to be anything that the supertype allows. A subtype may not return something that the supertype disallows, because this again would break the client. On the other hand, a supertype may choose to be more conservative with what it returns, because that's not going to break any clients.

This is called covariance, because the return type varies with the type. As the types (classes) get more specific (subtypes being more specific than supertypes) so do the return values.

The opposite of that is named contravariance for the same reason. Inputs may become more general as the type becomes more specific.

Upvotes: 1

Johnathan Barclay
Johnathan Barclay

Reputation: 20373

Now my understanding of covariant is that two entities are called covariant when for example we can assign an instance of List to an instance of List.

That is a description of generic type variance, but your example code deals with another topic entirely: covariant returns, which were introduced in C#9.

From the above code in the L1 on call of empRepo1.GetEmployeeDetialById(1) will execute the overridden version the method GetEmployeeDetialById and should return a type of EmployeeExtendedDetail

That is partially correct; indeed the overridden version of GetEmployeeDetialById will be invoked, and the runtime type of the object returned will be EmployeeExtendedDetail, but as the compile time type of empRepo1 is EmployeeRepository, then the compile time type of the returned object will be Employee.

The compiler therefore doesn't allow that to be assigned to a EmployeeExtendedDetail variable (without an explicit cast).

LSP states that a more specific type can be used in place of a les specific type, but it can't change the behaviour. Covariance is safe here, because EmployeeExtendedDetail derives from Employee, but it doesn't change the static behaviour of a EmployeeRepository variable.


An example to illustrate the purpose of covariant returns:

// Use EmployeeExtendedDetailRepository as its specific type
EmployeeExtendedDetailRepository empRepo = new EmployeeExtendedDetailRepository();
EmployeeExtendedDetail employee = empRepo.GetEmployeeDetialById(1);

// Use EmployeeExtendedDetailRepository in place of EmployeeRepository
void SomeMethod(EmployeeRepository empRepo)
{
    Employee employee = empRepo.GetEmployeeDetialById(1);
}

SomeMethod(new EmployeeExtendedDetailRepository());

Upvotes: 2

Related Questions