.NET equivalent for Java wildcard generics <?> with co- and contra- variance?
Asked Answered
C

1

6

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. For instance:

Java:

interface IInterf { }

class Impl implements IInterf { }

interface IGeneric1<T extends Impl> {
    void method1(IGeneric2<?> val);
    void method1WithParam(T val);
}

interface IGeneric2<T extends Impl> {
    void method2(IGeneric1<?> val);
}

abstract class Generic<T extends Impl> implements IGeneric1<T>, IGeneric2<T> {
    public void method1(IGeneric2<?> val2) {
        val2.method2(this);
    }
}

...works.

C# equivalent (?)

interface IInterf { }

class Impl : IInterf { }

interface IGeneric1<T> where T:Impl {
  //Java was: 
  //void method1(IGeneric2<?> val2);
    void method1(IGeneric2<Impl> val);
    void method1WithParam(T to);
}

interface IGeneric2<T>where T:Impl {
    void method2(IGeneric1<Impl> val);
}

abstract class Generic<T> : IGeneric1<T>, IGeneric2<T> where T : Impl
{
  //Java was: 
  //public void method1(IGeneric2<?> val2) {
    public void method1(IGeneric2<Impl> val2)
    {
         val2.method2(this); //'this': Argument type 'Generic<T>' is not 
                             //assignable to parameter type 'IGeneric1<Impl>'
    }

    public abstract void method1WithParam(T to);
    public abstract void method2(IGeneric1<Impl> val);
}

...fails to compile - see the error in the comment. Which is to be expected, since IGeneric's generic parameter is not marked 'out' for covariance.

If I change this:

interface IGeneric1<T> where T:Impl {

to this

interface IGeneric1<out T> where T:Impl 

the error goes away, but another one appears, for the declaration of the method that takes a generic parameter inside the same interface:

interface IGeneric1<T> where T:Impl {
    void method1WithParam(T val);  //Parameter must be input-safe. 
                      //Invalid variance: The type parameter 'T' must be
                      //contravariantly valid on 'IGeneric1<out T>'.

Suggestions?

[Also see the follow-up question for a somewhat harder scenario]

Caryloncaryn answered 11/1, 2013 at 11:40 Comment(2)
What is the meaning of the question mark inside the angle brackets in Java?Wordy
"Any type goes" (or in this case, any type derived from Impl goes). It's called "wildcard". docs.oracle.com/javase/tutorial/extra/generics/wildcards.htmlCaryloncaryn
A
7

You need to translate the Java wildcard generic methods to C# methods that are generic in their own right. For example, this:

interface IGeneric2<T extends Impl> {
    void method2(IGeneric1<?> val);
}

should be translated to

interface IGeneric2<T>where T:Impl {
    void method2<U>(IGeneric1<U> val) where U:Impl;
}

It is necessary to repeat the type constraint for T specified by IGeneric1<T> as the type constraint for U.

The reason for this is that in the Java version there are implicit constraints for the type arguments of the parameters of method1 and method2: if the parameter must be some kind of IGeneric1<X> then X must obviously be an Impl because otherwise it could not possibly implement IGeneric1 for that type.

In C# the constraints must be explicit, so you repeat what IGeneric1<T> and IGeneric2<T> require of T.

So the equivalent code would be:

interface IInterf { }

class Impl : IInterf { }

interface IGeneric1<T> where T:Impl {
    void method1<U>(IGeneric2<U> val) where U:Impl;
    void method1WithParam(T to);
}

interface IGeneric2<T>where T:Impl {
    void method2<U>(IGeneric1<U> val) where U:Impl;
}

abstract class Generic<T> : IGeneric1<T>, IGeneric2<T> where T : Impl
{
    public void method1<U>(IGeneric2<U> val2) where U:Impl
    {
        val2.method2(this);
    }

    public abstract void method1WithParam(T to);
    public abstract void method2<U>(IGeneric1<U> val) where U:Impl;
}
Adel answered 11/1, 2013 at 11:57 Comment(10)
Is the Java version of method2 actually bounded? According to the documentation the OP posted, a bounded wildcard declares the constraint inline. There is no such constraint in the Java code of the OP. I assume calling method2 with an IGeneric1<string> would be valid.Wordy
@DanielHilgarth: There is an implicit constraint in that IGeneric1<T> and IGeneric2<T> require that T extends Impl. method1 and method2 say "I 'll take any kind of IGenericX", but it is not possible to actually have a type implement IGenericX for just any value of the generic type parameter. Unless I am horribly mistaken, because my Java is sub-par.Adel
Of course, you are correct - that's where the constraint comes from. In C# you have to explicitly "repeat" that constraint on the methods, while in Java this is implicit. Thanks.Wordy
@DanielHilgarth: It does need the constraint because IGenericX<T> requires it. The point is that T in Generic<T> and U in method1<U> need not be the same type.Adel
I think you answered to an old version of my comment :-)Wordy
@DanielHilgarth: You type too fast ;-)Adel
Yes, and I thought too slow to keep up with the typing ;-)Wordy
@Adel Your suggestion worked great - for most part of the codebase, of which my example was just a small part. But I hit another roadblock. Would you mind taking a look at the updated question? ThxCaryloncaryn
@CristiDiaconescu: Your update is another question altogether. Please post it as such and reference this one in it.Wordy
@Daniel Good point. Question restored to initial state. Here's the new scenario: #14290018Caryloncaryn

© 2022 - 2024 — McMap. All rights reserved.