Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Covariance and Contravariance with Func in generics

I need more information about variance in generics and delegates. The following code snippet does not compile:

Error CS1961 Invalid variance: The type parameter 'TIn' must be covariantly valid on 'Test.F(Func)'. 'TIn' is contravariant.

public interface Test<in TIn, out TOut>
{
    TOut F (Func<TIn, TOut> transform);
}

The .net Func definition is as follows:

public delegate TResult Func<in T, out TResult> (T arg);

Why the compiler complains about TIn being contravariant and TOut - covariant while the Func expects exactly the same variance?

EDIT

The main constraint for me is that I want my Test interface to have TOut as covariant in order to use it something like this:

public Test<SomeClass, ISomeInterface> GetSomething ()
{
    return new TestClass<SomeClass, AnotherClass> ();
}

Given that public class AnotherClass : ISomeInterface.

like image 755
Robertas Avatar asked Jan 09 '18 22:01

Robertas


People also ask

What is covariance and contravariance in generics in Java?

Covariance can be translated as "different in the same direction," or with-different, whereas contravariance means "different in the opposite direction," or against-different. Covariant and contravariant types are not the same, but there is a correlation between them. The names imply the direction of the correlation.

What is difference between covariance and contravariance?

In C#, covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type arguments. Covariance preserves assignment compatibility and contravariance reverses it.

What is the difference between covariance and contravariance in delegates?

Covariance permits a method to have return type that is more derived than that defined in the delegate. Contravariance permits a method that has parameter types that are less derived than those in the delegate type.

What is the difference between corn variance and contravariance?

Covariance permits a method to have a return type that is a subtype of the one defined in the delegate. Contravariance permits a method to have a parameter type that is a base type of the one defined in the delegate type.


2 Answers

I need more information about variance in generics and delegates.

I wrote an extensive series of blog articles on this feature. Though some of it is out of date -- since it was written before the design was finalized -- there's lots of good information there. In particular if you need a formal definition of what variance validity is, you should carefully read this:

https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/

See my other articles on my MSDN and WordPress blogs for related topics.


Why the compiler complains about TIn being contravariant and TOut - covariant while the Func expects exactly the same variance?

Let's slightly rewrite your code and see:

public delegate R F<in T, out R> (T arg);
public interface I<in A, out B>{
  B M(F<A, B> f);
}

The compiler must prove that this is safe, but it is not.

We can illustrate that it is not safe by supposing that it is, and then discovering how it can be abused.

Let's suppose we have an Animal hierarchy with the obvious relationships, eg, Mammal is an Animal, Giraffe is a Mammal, and so on. And let's suppose that your variance annotations are legal. We should be able to say:

class C : I<Mammal, Mammal>
{
  public Mammal M(F<Mammal, Mammal> f) {
    return f(new Giraffe());
  }
}

I hope you agree this is a perfectly valid implementation. Now we can do this:

I<Tiger, Animal> i = new C();

C implements I<Mammal, Mammal>, and we've said that the first one can get more specific, and the second can get more general, so we've done that.

Now we can do this:

Func<Tiger, Animal> f = (Tiger t) => new Lizard();

That's a perfectly legal lambda for this delegate, and it matches the signature of:

i.M(f);

And what happens? C.M is expecting a function that takes a giraffe and returns a mammal, but it's been given a function that takes a tiger and returns a lizard, so someone is going to have a very bad day.

Plainly this must not be allowed to happen, but every step along the way was legal. We must conclude that the variance itself was not provably safe, and indeed, it was not. The compiler is right to reject this.

Getting variance right takes more than simply matching the in and out annotations. You've got to do so in a manner that does not allow this sort of defect to exist.

That explains why this is illegal. To explain how it is illegal, the compiler must check that the following is true of B M(F<A, B> f);:

  • B is valid covariantly. Since it is declared "out", it is.
  • F<A, B> is valid contravariantly. It is not. The relevant portion of the definition of "valid contravariantly" for a generic delegate is: If the ith type parameter was declared as contravariant, then Ti must be valid covariantly. OK. The first type parameter, T, was declared as contravariant. Therefore the first type argument A must be valid covariantly. But it is not valid covariantly, because it was declared contravariant. And that's the error you're getting. Similarly, B is also bad because it must be valid contravariantly, but B is covariant. The compiler does not go on to find additional errors after it finds the first problem here; I considered it but rejected it as being a too-complex error message.

I note also that you would still have this problem even if the delegate were not variant; nowhere in my counterexample did we use the fact that F is variant in its type parameters. A similar error would be reported if we tried

public delegate R F<T, R> (T arg);

instead.

like image 161
Eric Lippert Avatar answered Oct 11 '22 18:10

Eric Lippert


Variance is about being able to replace type parameters with either more or less derived types than originally declared. For example, IEnumerable<T> is covariant for T, meaning if you start with a reference to a IEnumerable<U> object, you can assign that reference to a variable having type IEnumerable<V>, where V is assignable from U (e.g. U inherits V). This works, because any code trying to use the IEnumerable<V> wants to receive only values of V, and since V is assignable from U, receiving only values of U is also valid.

For covariant parameters like T, you have to assign to a type where the destination type is the same as T, or assignable from T. For contravariant parameters, it has to go the other way. The destination type has to be the same as, or assignable to, the type parameter.

So, how does the code you are trying to write work in that respect?

When you declare Test<in TIn, out TOut>, you are promising that it will be valid to assign an instance of that interface Test<TIn, TOut> to any destination having the type Test<U, V> where U can be assigned to TIn and TOut can be assigned to V (or they are identical, of course).

At the same time, let's consider what your transform delegate is to expect. The Func<T, TResult> type variance requires that if you want to assign that value to something else, it also meets the variance rules. That is, a destination Func<U, V> must have U assignable from T, and TResult assignable from V. This ensures that your delegate target method which is expecting to receive a value of U will get one of those, and the value returned by the method, having type V, can be accepted by the code receiving it.

Importantly, your interface method F() is the one doing the receiving! The interface declaration promises that TOut will be used only as output from the interface members. But through the use of the transform delegate, the method F() will receive a value of TOut, making that input to the method. Likewise, the method F() is allowed to pass a value of TIn to the transform delegate, making that an output of your interface implementation, even though you've promised that TIn is used only as input.

In other words, every layer of call reverses the sense of the variance. Members in the interface have to use covariant type parameters as output only and contravariant parameters as input only. But those parameters become reversed in sense when they are used in delegate types passed to or returned from interface members, and have to comply with the variance in that respect.

A concrete example:

Suppose we have an implementation of your interface, Test<object, string>. If the compiler were to allow your declaration, you'd be permitted to assign a value of that implementation Test<object, string> to a variable having the type Test<string, object>. That is, the original implementation promises to allow as input any thing having type object and return only values having the type string. It's safe for code declared as Test<string, object> to work with this, because it will pass string objects to an implementation that requires objects values (string is an object), and it will receive values having the type object from an implementation that returns string values (again, string is an object, so also safe).

But your interface implementation expects code to pass a delegate of type Func<object, string>. If you were allowed to treat (as above) your interface implementation as a Test<string, object> instead, then the code using your re-cast implementation would be able to pass a delegate of Func<string, object> to the method F(). The method F() in the implementation is allowed to pass any value of type object to the delegate, but that delegate, being of type Func<string, object>, is expecting only values having the type string to be passed to it. If F() passes something else, e.g. just a plain old new object(), the delegate instance won't be able to use it. It's expecting a string!

So, in fact, the compiler is doing exactly what it's supposed to: it's preventing you from writing code that is not type-safe. As declared, if you were permitted to use that interface in a variant way, you would in fact be able to write code that while allowed at compile-time, could break at run-time. Which is the exact opposite of the whole point of generics: to be able to determine at compile-time that the code is type-safe!

Now, how to solve the dilemma. Unfortunately, there's not enough context in your question to know what the right approach is. It's possible that you simply need to give up on variance. Often, there's not actually any need to make types variant; it's a convenience in some cases, but not required. If that's the case, then just don't make the interface's parameters variant.

Alternatively, it's possible you really do want the variance and thought it would be safe to use the interface in a variant way. That's harder to solve, because your fundamental assumption was just incorrect and you will need to implement the code some other way. The code would compile if you could reverse the parameters in the Func<T, TResult>. I.e. make the method F(Func<TOut, TIn> transform). But there's not anything in your question that suggests that's actually possible in your scenario.

Again, without more context it's impossible to say what "other way" would work for you. But, hopefully now that you understand the hazard in the code the way you've written it now, you can revisit the design decision that led you to this not-type-safe interface declaration, and can come up with something that works. If you have trouble with that, post a new question that provides more detail as to why you thought this would be safe, how you're going to use the interface, what alternatives you've considered, and why none of those work for you.

like image 32
Peter Duniho Avatar answered Oct 11 '22 17:10

Peter Duniho