Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type safety in CoVariance and ContraVariance

Tags:

c#

I am reading C# in Depth from Jon Skeet. Although I have understood the concept of CoVariance and ContraVariance, but I am unable to understand this line:

Well, covariance is safe when SomeType only describes operations that return the type parameter—and contravariance is safe when SomeType only describes operations that accept the type parameter.

Can someone please explain with an example for both, why both are type safe in one direction and not in the other direction?

Updated Question:

I still didn't understand from the answers given. I will try to explain my concern using the same example from the book - C# In Depth.

It explains using the following class hierarchy:

Covariance and ContraVariance

COVARIANCE is: Trying to convert from IEnumerable<Circle> to IEnumerable<IShape>, but it is mentioned that this conversion is type safe only when we are performing while returning it from some method, and not type safe when we are passing it as an IN parameter.

IEnumerable<IShape> GetShapes()
{
    IEnumerable<Circle> circles = GetEnumerableOfCircles();
    return circles; // Conversion from IEnumerable<Circle> to IEnumerable<IShape> - COVARIANCE
}

void SomeMethod()
{
    IEnumerable<Circle> circles = GetEnumerableOfCircles();
    DoSomethingWithShapes(circles); // Conversion from IEnumerable<Circle> to IEnumerable<IShape> - COVARIANCE
}

void DoSomethingWithShapes(IEnumerable<IShape> shapes) // Why this COVARIANCE is type unsafe??
{
    // do something with Shapes
}

CONTRA VARIANCE is: Trying to convert from IEnumerable<IShape> to IEnumerable<Circle>, which is mentioned to be type safe only while performing it when sending it as an IN parameter.

IEnumerable<Circle> GetShapes()
{
    IEnumerable<IShape> shapes = GetEnumerableOfIShapes();
    return shapes; // Conversion from IEnumerable<IShape> to IEnumerable<Circle> - Contra-Variance
    // Why this Contra-Variance is type unsafe??
}

void SomeMethod()
{
    IEnumerable<IShape> shapes = GetEnumerableOfIShapes();
    DoSomethingWithCircles(shapes); // Conversion from IEnumerable<IShape> to IEnumerable<Circle> - Contra-Variance
}

void DoSomethingWithCircles(IEnumerable<Circle> circles) 
{
    // do something with Circles
}
like image 378
teenup Avatar asked May 19 '15 03:05

teenup


3 Answers

Imagine you make an ILogger<in T> interface that knows how to log the details of a T. And let's say you have a Request class and an ExpeditedRequest subclass. Surely an ILogger<Request> should be convertible to an ILogger<ExpeditedRequest>. After all, it can log any request.

Interface ILogger<in T> where T: Request {
     void Log(T arg);
}

Now imagine another interface IRequestProducer<out T> that gets the next request in some queue. There are different sources of requests in your system, and of course, some of them can still have different subclasses. In this case, we can't rely on converting an IRequestProducer<Request> to an IRequestProducer<ExpeditedRequest> since it could produce a non-expedited request. But the reverse conversion would work.

Interface IRequestProducer<T> where T: Request {
    T GetNextRequest();
}
like image 44
recursive Avatar answered Oct 12 '22 01:10

recursive


I understood after reading from MSDN, the following two pages:

https://msdn.microsoft.com/en-us/library/dd469484.aspx
https://msdn.microsoft.com/en-us/library/dd469487.aspx

Actually, I think it will become clear from the book also when I will reach the C# 4 part of the book that will explain in and out keywords with Type Parameters of Generics. Right now, I was reading the Limitations of Generics in C# in the C# 1 part of book.

The statement that I wanted to understand was this:

Well, covariance is safe when SomeType only describes operations that return the type parameter—and contravariance is safe when SomeType only describes operations that accept the type parameter.

Besides being safe, it is also not possible to write the interface method in other direction as compiler will complain, as written in the above two pages on msdn:

A type can be declared contravariant in a generic interface or delegate if it is used only as a type of method arguments and not used as a method return type.

In a generic interface, a type parameter can be declared covariant if it satisfies the following conditions: The type parameter is used only as a return type of interface methods and not used as a type of method arguments.

There are now two points, that made me confuse about the statement:

First , I was misunderstanding the statement itself - I was thinking that Covairance is safe only when an instance of Inteface<T> itself is returned from some method rather than passed as an input to some method. However, it is regarding the type parameter T and interface method. Same for ContraVariance.

Second, Now When I have understood what this statement means - that it is regarding passing/returning the generic type parameter T to/from interface methods. I wanted to know why it is Type Safe only while returning T from interface method in Covariance and why it is Type Safe only while passing T as input to interface method in ContraVariance.

Why it is Type Safe only while returning T in CoVariance:

interface IBase<out T>
{
    T Return_T(); // Valid and Type Safe
    void Accept_T(T input) // Invalid and Type UnSafe
}
class Sample<T> : IBase<T> { }
class BaseClass {}
class DerivedClass : BaseClass
{}
IBase<BaseClass> ibase = new Sample<BaseClass>();
IBase<DerivedClass> iderived = new Sample<DerivedClass>();

ibase = iderived; // Can be assinged because `T` is Covariant

BaseClass b = new BaseClass();
DerivedClass d = new DerivedClass();

ibase.Return_T(); //  At runtime, this will return `DerivedClass` which can be assinged to variable of both base and derived class and is type safe
ibase.Accept_T(b); // The compiler will accept this statement, because at compile time, it accepts an instance of `BaseClass`, but at runtime, it actually needs an instance of `DerivedClass`. So, we are eventually assigning an instance of `BaseClass` to `DerivedClass` which is type unsafe.

Why it is Type Safe only while passing T as an input parameter in ContraVariance:

interface IBase<in T>
{
    T Return_T(); // Invalid and Type UnSafe
    void Accept_T(T input) // Valid and Type Safe
}
class Sample<T> : IBase<T> { }
class BaseClass {}
class DerivedClass : BaseClass
{}
IBase<BaseClass> ibase = new Sample<BaseClass>();
IBase<DerivedClass> iderived = new Sample<DerivedClass>();

iderived = ibase; // Can be assinged because `T` is Contravariant

BaseClass b = new BaseClass();
DerivedClass d = new DerivedClass();

iderived.Accept_T(d); // This is Type Safe, because both at compile time and runtime, either instance of `DerivedClass` can be assinged to `BaseClass` or instance of `BaseClass` can be assinged to `BaseClass`

DerivedClass d2 = iderived.Return_T(); // This is type unsafe, because this statement is valid at compile time, but at runtime, this will return an instance of `BaseClass` which is getting assinged to `DerivedClass`
like image 43
teenup Avatar answered Oct 12 '22 00:10

teenup


Covariance

Covariance is safe when SomeType only describes operations that return the type parameter

The IEnumerable<out T> interface is probably the most common example of covariance. It is safe because it only returns values of type T (well, specifically an IEnumerator<out T> but does not accept any T objects as parameters.

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

This works because IEnumerator<T> is also covariant and only returns T:

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    T Current { get; }
}

If you have a base class called Base and a derived class called Derived, then you can do stuff like this:

IEnumerable<Derived> derivedItems = Something();
IEnumerable<Base> baseItems = derivedItems;

This works because each item in derivedItems is also an instance of Base, so it is perfectly acceptable to assign it the way we just did. However, we cannot assign the other way:

IEnumerable<Base> baseItems = Something();
IEnumerable<Derived> derivedItems = baseItems; // No good!

This isn't safe because there is no guarantee that each instance of Base is also an instance of Derived.

Contravariance

Contravariance is safe when SomeType only describes operations that accept the type parameter

The Action<in T> delegate is a good example of contravariance.

public delegate void Action<in T>(T obj);

It is safe because it only accepts T as a parameter, but doesn't return T.

Contravariance lets you do stuff like this:

Action<Base> baseAction = b => b.DoSomething()
Action<Derived> derivedAction = baseAction;

Derived d = new Derived();
// These 2 lines do the same thing:
baseAction(d);
derivedAction(d);

This works because it is perfectly acceptable to pass an instance of Derived to baseAction. However, it doesn't work the other way around:

Action<Derived> derivedAction = d => d.DoSomething()
Action<Base> baseAction = derivedAction; // No good!

Base b = new Base();
baseAction(b);    // This is OK.
derivedAction(b); // This does not work because b may not be an instance of Derived!

This isn't safe because there is no guarantee that an instance of Base will also be an instance of Derived.

like image 177
AJ Richardson Avatar answered Oct 12 '22 00:10

AJ Richardson