-->

C# return type covariance and Liskov substitution

2020-02-12 06:33发布

问题:

I'm trying to understand Covariance and LSP. From this question I can see C# does not support return type covariance. However Liskov substitution principle imposes covariance on return type.

Does it mean it is impossible to apply this principle in C# ? Or did I missunderstand something ?

回答1:

C# can still apply the Liskov substitution principle.

Consider:

public class Base1
{
}

public class Derived1 : Base1
{
}

public class Base2
{
    public virtual Base1 Method()
    {
        return new Base1();
    }
}

public class Derived2 : Base2
{
    public override Base1 Method()
    {
        return new Derived1();
    }
}

If C# supported covariant return types, then the override for Method() in Base2 could be declared thusly:

public class Derived2 : Base2
{
    public override Derived1 Method()
    {
        return new Derived1();
    }
}

C# does not allow this, and you must declare the return type the same as it is in the base class, namely Base1.

However, doing so does not make it violate the Liskov substitution principle.

Consider this:

Base2 test = new Base2();
Base1 item = test.Method();

Compared to:

Base2 test = new Derived2();
Base1 item = test.Method();

We are completely able to replace new Base2() with new Derived2() with no issues. This complies with the Liskov substitution principle.



回答2:

Since return type covariance is not supported in C#, it is impossible to violate the Liskov principle when it comes to return type covariance.

Good source is this talk of S.O.L.I.D principles in C#: https://youtu.be/gwIS9cZlrhk?t=1886



回答3:

C# do have a limited support for this feature through generics and the out generic modifier. (See: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-generic-modifier)

The support is limited, because it only works with interfaces. To rewrite the accepted answer:

public class Base1
{
}

public class Derived1 : Base1
{
}

public interface Base2<out T> where T : Base1
{
    T Method();
}

public class Derived2 : Base2<Derived1>
{
    public Derived1 Method()
    {
        return new Derived1();
    }
}

In this case Derived2 will not only implement Base2<Derived1> but it will also implement Base2<Base1>.

The principle takes effect because if you call Method through the Base2<Base1> interface it will have Base1 return type, but if you call it through Derived2 it will have Derived1 return type.

Similarly you can implement parameter contravariance with the in generic modifier: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/in-generic-modifier

Note, that it is not possible to violate the principle. If in the above example you change the out keyword to the in keyword, the source will not compile, and you will get the following error:

Error   CS1961  Invalid variance: The type parameter 'T' must be covariantly valid on 'Base2<T>.Method()'. 'T' is contravariant.