.NET equivalent for Java bounded wildcard (IInterf<?>)?
Asked Answered
A

2

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.

[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://mcmap.net/q/826449/-net-equivalent-for-java-wildcard-generics-lt-gt-with-co-and-contra-variance

    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>'.
Arenaceous answered 12/1, 2013 at 2:16 Comment(7)
I don't know Java generics much. But is that Java code type-safe?Macpherson
Can you please provide how that Java code will or should be called? I still find hard to understand why would someone make such a monstrosity.Macpherson
Do note that C#'s variance constraints are intentionally more restrictive for the sake of simplicity. It's entirely possible the ones you're expressing in the Java code simply don't have a straightforward equivalent in C#.Forebrain
@Forebrain Then I guess, Yay for C# for making it harder for me to shoot myself in the foot :) (although it didn't make my life easy on this occasion)Arenaceous
@Cristi: Also your translation / understanding of the original code is wrong. In the Java Generic2.method2(), you're assigning from an IGeneric1<?> to another IGeneric1<?>. In Java this means from one unknown instantiation of IGeneric1 to another unknown, potentially different instantiation of that generic interface. (E.g. elem can be a IGeneric1<Number> and val can be an `IGeneric<String>, and this is okay because the rest of your code doesn't care about which one it is.)Forebrain
@milimoose I got that, but couldn't find a way to express it in C#. Hence reaching for help here :) - I think Jon's answer is right on the money, i.e. I should just find the least common denominator of all possible types and use that.Arenaceous
@Cristi In the C#, you're replacing both the wildcards with a type parameter, which means the meaning of the code changes to assigning between identical instantiations of IGeneric<>. This is a completely different constraint, and my guess is this is the root problem of the compiler error. Now, your assessment is right there, because C# indeed does not allow you to "not care" about which instantiation of a generic type you're using. In C#, those instantiations are first-class types in their own right. So, to assign from IGeneric<int> to IGeneric<string> would require a (failing) cast.Forebrain
F
3

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.

Felting answered 12/1, 2013 at 13:28 Comment(1)
Thanks for confirming my suspicions. I wrote a question specifically asking how I could possibly call generic methods on that field; I thought I was missing something: https://mcmap.net/q/833195/-java-help-me-understand-how-to-use-interface-methods-on-a-bounded-wildcard-field/11545. The culprit interface in the Java codebase is a public one, so I thought I might break some possible external use-case if I can't find a 1:1 correspondance in C#. Turns out there are no such possible use-cases.Arenaceous
J
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.

Jeep answered 12/1, 2013 at 12:34 Comment(2)
yep, I tried that too - but thought there may be a clever way of using the generic methods, that I was missing. I asked about it on https://mcmap.net/q/833195/-java-help-me-understand-how-to-use-interface-methods-on-a-bounded-wildcard-field/11545 but I guess your answer and Jon's clear that up.Arenaceous
Regarding your final paragraph - I completely agree. I spent way too much time just trying to figure out what the code does and how to simplify it (it's a lot more tangled than this example) to the point that I could explain the problematic bits to someone else, and to grok it myself in the process. OTOH, I must admit of having made myself guilty of similar complications in the past; it's a slippery slope.Arenaceous

© 2022 - 2024 — McMap. All rights reserved.