.NET equivalent for Java bounded wildcard (IInterf

2019-03-20 16:10发布

问题:

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>'.

回答1:

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 call method1WithParam on it in a type-safe manner.

This means that elem can be used only to call its method1 member, whose signature does not depend on the type parameter of IGeneric1. It follows that method1 can be broken out into a non-generic interface:

// C# code:
interface INotGeneric1 {
    void method1<T>(IGeneric2<T> val) where T : Impl;
}

interface IGeneric1<T> : INotGeneric1 where T : Impl {
    T method1WithParam(T to);
}

After this, class Generic2 can aggregate an INotGeneric1 member instead:

abstract class Generic2<T>: IGeneric2<T> where T : Impl
{
    protected INotGeneric1 elem;

    // It's highly likely that you would want to change the type of val
    // to INotGeneric1 as well, there's no obvious reason to require an
    // IGeneric1<U>
    public void method2<U>(IGeneric1<U> val) where U : Impl
    {
        elem = val; // this is now OK
    }
}

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 type X 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 call method1WithParam1.



回答2:

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 class Generic2, you don't use its method T 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 through elem.

To illustrate this, the following add a function test() to the Generic2 class which will try to call the elem.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:

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;
    }

    public void test() {
        Impl i = new Impl();

                // The following line will generate a compiler error:
        // Impl i2 = elem.method1WithParam(i); // 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 with IGeneric1<out T extends Impl>; you automatically get a compilation error for T 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.