Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Generics Interface Covariance

I'm not sure what's going on here but I'm getting a compiler error using the following code:

namespace SO
{
    interface IUser<PostType>
    {
        PostType Post { get; set; }
    }

    interface IPost<UserType>
    {
        UserType User { get; set; }
    }

    class User : IUser<Post>
    {
        //Implementation
    }

    class Post : IPost<User>
    {
        //Implementation
    }

    class SomeOtherClass
    {
        // Compiler Error: Cannot implicitly convert type 'SO.User' to
        // 'SO.IUser<SO.IPost<SO.User>>'. An explicit conversion exists 
        // (are you missing a cast?)
        IUser<IPost<User>> user = new User();

        //Works Fine
        IUser<Post> user = new User();
    }
}

Why am I getting an error if Post is a subtype of IPost<User>? I know in this case I could just use User instead of IUser<IPost<User>>, but I want to know why this doesn't work.

like image 963
orourkedd Avatar asked Mar 24 '13 07:03

orourkedd


1 Answers

I will try to explain it using simple example. Suppose you have one more class implementing IPost<User>:

class PicturePost : IPost<User>
{
    // Implementation
}

Then this code will not compile:

    IUser<Post> user = new User();
    user.Post = new PicturePost();

Because user.Post is of concrete class Post which is not compatible with PicturePost (they are siblings).

Then imagine that line from your question was successfully compiled:

    // C# compiler is so kind today and it compiled this.
    IUser<IPost<User>> user = new User();

Since user.Post now will be of type IPost<User> you potentially will code such lines:

    IUser<IPost<User>> user = new User();
    user.Post = new PicturePost();

And they will compile perfectly, but second line will fail with run-time error! This is because actual type of user.Post is Post not IPost or PicturePost.

So, in order to achieve type-safety, C# compiler prohibits compiling if there is a chance of such code to be written. In order to ensure that you will not write such code, Post property should be read-only:

interface IUser<PostType>
{
    PostType Post { get; } // No setter, this is readonly.
}

Now you will not be able to write evil code, and all usages of Post will be type-safe in respect of its interface, since you can just get it, and than perfectly assign to variable of its interface.

But this is not enough, to tell compiler that your interface on the light side, you need to explicitly specify that your type parameter is only out (you can use it, but you cannot to pass it into). So, having below implementation of interface (notice out keyword), your code will compile:

interface IUser<out PostType>
{
    PostType Post { get; } // No setter, this is readonly.
}

    // Both lines compile!
    IUser<IPost<User>> user = new User();
    IUser<Post> user1 = new User();

Hope I kept it simple and did not miss the point at the same time:)

like image 114
SergeyS Avatar answered Oct 20 '22 21:10

SergeyS