I'm stuck trying to translate some Java code that uses (bounded) wildcard generics to C#. My problem is, Java seems to allow a generic type to be both covariant and contravariant when used with a wildcard.
[This is a spin-off from a previous question dealing with a simpler case of bounded-wildcards]
Java - works:
class Impl { }
interface IGeneric1<T extends Impl> {
void method1(IGeneric2<?> val);
T method1WithParam(T val);
}
interface IGeneric2<T extends Impl> {
void method2(IGeneric1<?> val);
}
abstract class Generic2<T extends Impl> implements IGeneric2<T> {
// !! field using wildcard
protected IGeneric1<?> elem;
public void method2(IGeneric1<?> val1) {
val1.method1(this);
//assignment from wildcard to wildcard
elem = val1;
}
}
abstract class Generic<T extends Impl> implements IGeneric1<T>, IGeneric2<T> {
public void method1(IGeneric2<?> val2) {
val2.method2(this);
}
}
C# - doesn't compile...
class Impl { }
interface IGeneric1<T> where T:Impl {
//in Java:
//void method1(IGeneric2<?> val);
void method1<U>(IGeneric2<U> val) where U : Impl; //see this Q for 'why'
// https://stackoverflow.com/a/14277742/11545
T method1WithParam(T to);
}
interface IGeneric2<T>where T:Impl {
void method2<U>(IGeneric1<U> val) where U : Impl;
}
abstract class Generic2<T, TU>: IGeneric2<T> //added new type TU
where T : Impl
where TU : Impl
{
//in Java:
//protected IGeneric1<?> elem;
protected IGeneric1<TU> elem;
//in Java:
//public void method2(IGeneric1<?> val1)
public void method2<U>(IGeneric1<U> val)
where U : TU //using TU as constraint
{
elem = val; //Cannot convert source type 'IGeneric1<U>'
//to target type 'IGeneric1<TU>'
}
public abstract void method1WithParam(T to);
}
abstract class Generic<T> : IGeneric1<T>, IGeneric2<T> where T : Impl
{
//in Java:
//public void method1(IGeneric2<?> val2)
public void method1<U>(IGeneric2<U> val2) where U : Impl
{
val2.method2(this);
}
public abstract T method1WithParam(T to);
public abstract void method2<U>(IGeneric1<U> val) where U : Impl;
public abstract void nonGenericMethod();
}
If I change interface IGeneric1<T>
to interface IGeneric1<out T>
the above error goes away, but method1WithParam(T)
complains about variance:
Parameter must be input-safe. Invalid variance: The type parameter 'T' must be
contravariantly valid on 'IGeneric1<out T>'.
Java doesn't allow a type to be both variant and covariant. What you have is an illusion stemming from the fact that while you are declaring
IGeneric1<?> elem
in the classGeneric2
, you don't use its methodT method1WithParam(T val);
; therefore Java don't see any problem with this declaration. It will however flag an error as soon as you will try to use it throughelem
.To illustrate this, the following add a function
test()
to theGeneric2
class which will try to call theelem.method1WithParam()
function but this leads to a compilator error. The offensive line has been commented out, so you need to re-install it in order to reproduce the error:This error from the Java compiler proves that we cannot use a generic type as both covariant and contravariant and this; even if some declaration seems to prove the contrary. With the C# compiler, you don't even have a chance to get that close before getting a compilation error: if you try to declare the interface
IGeneric1<T extends Impl>
to be variant withIGeneric1<out T extends Impl>
; you automatically get a compilation error forT method1WithoutParam();
Second, I took a look at the reference .NET equivalent for Java wildcard generics <?> with co- and contra- variance? but I must admit that I don't understand why this can be seen as a solution. Type restriction such as
<T extends Impl>
has nothing to do with unbounded wildcard parameterized type (<?>
) or variance (<? extends Impl>
) and I don't see how replacing the seconds with the first could be seen as a general solution. However, on some occasions, if you don't really need to use a wildcard parameterized type (<?>
) or a variance type than yes, you can make this conversion. However, if you don't really use them in your Java code, this one should also be corrected, too.With Java generics, you can introduce a lot of imprecision but you won't get that chance with the C# compiler. This is especially true considering that in C#, classes and structs are fully reifiable and therefore, do not support variance (both covariance and contravariance). You can use that only for the declaration of an interface and for delegates; if I remember correctly.
Finally, when polymorphism is involved, there is often a bad tendency to use unnecessary generic types; with or without wildcard parameterized types and variance. This often lead to a long and complex code; hard to read and use and even harder to write. I will strongly suggest you to look at all this Java code and see if it's really necessary to have all this stuff instead of a much simpler code with only polymorphism or a combination of polymorphism with generic but without variance or wildcard parameterized type.
Let me start by saying that is definitely starting to look like a design review is in order. The original Java class aggregates an
IGeneric1<?>
member, but without knowing its type argument there's no possibility to callmethod1WithParam
on it in a type-safe manner.This means that
elem
can be used only to call itsmethod1
member, whose signature does not depend on the type parameter ofIGeneric1
. It follows thatmethod1
can be broken out into a non-generic interface:After this,
class Generic2
can aggregate anINotGeneric1
member instead:Of course now you cannot call
elem.method1WithParam
unless you resort to casts or reflection, even though it is known that such a method exists and it is generic with some unknown typeX
as a type argument. However, that is the same restriction as the Java code has; it's just that the C# compiler will not accept this code while Java will only complain if you do try to callmethod1WithParam1
.