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.
Test<out TIn, TOut>
. – Skate