6

我一直在尝试将一些使用(有界)通配符泛型的 Java 代码转换为 C#。我的问题是,Java 似乎允许泛型类型在与通配符一起使用时既是协变的又是逆变的。

[这是处理更简单的有界通配符情况的前一个问题的衍生产品]

Java - 作品:

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# - 不编译...

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

如果我更改interface IGeneric1<T>interface IGeneric1<out T> 上述错误消失,但method1WithParam(T)抱怨方差:

Parameter must be input-safe. Invalid variance: The type parameter 'T' must be
contravariantly valid on 'IGeneric1<out T>'.
4

2 回答 2

3

让我首先说,这肯定开始看起来像一个设计审查是有序的。原始的 Java 类聚合了一个IGeneric1<?>成员,但在不知道其类型参数的情况下,不可能以method1WithParam类型安全的方式调用它。

这意味着elem只能用于调用其method1成员,其签名不依赖于 的类型参数IGeneric1。因此method1可以分解为非通用接口:

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

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

在此之后,可以改为class Generic2聚合一个成员:INotGeneric1

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

当然,现在你不能调用elem.method1WithParam,除非你求助于强制转换或反射,即使已知这样的方法存在并且它是通用的,带有一些未知类型X作为类型参数。但是,这与 Java 代码的限制相同;只是 C# 编译器不会接受此代码,而 Java 只会在您尝试调用method1WithParam1.

于 2013-01-12T13:28:41.643 回答
2

Java 不允许类型既是变体又是协变的。你所拥有的是一种错觉,因为当你IGeneric1<?> elem在类中声明时Generic2,你没有使用它的方法T method1WithParam(T val);;因此 Java 认为这个声明没有任何问题。但是,一旦您尝试通过elem.

为了说明这一点,下面将一个函数添加test()Generic2类中,该函数将尝试调用该elem.method1WithParam()函数,但这会导致编译器错误。攻击行已被注释掉,所以需要重新安装才能重现错误:

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

Java 编译器的这个错误证明我们不能将泛型类型同时用作协变和逆变。即使某些声明似乎证明相反。使用 C# 编译器,在出现编译错误之前,您甚至没有机会接近:如果您尝试将接口声明IGeneric1<T extends Impl>为变体IGeneric1<out T extends Impl>; 你会自动得到一个编译错误T method1WithoutParam();

其次,我查看了Java 通配符泛型 <?> 的参考 .NET 等价物与协变和反变?但我必须承认,我不明白为什么这可以被视为一种解决方案。类型限制,例如<T extends Impl>与无界通配符参数化类型 ( <?>) 或方差 ( <? extends Impl>) 无关,我不明白如何将秒替换为第一个可以被视为通用解决方案。但是,在某些情况下,如果您确实不需要使用通配符参数化类型 ( <?>) 或变体类型而不是 yes,则可以进行此转换。但是,如果您没有真正在 Java 代码中使用它们,那么也应该更正这一点。

使用 Java 泛型,您可能会引入很多不精确性,但使用 C# 编译器不会有这种机会。考虑到在 C# 中,类和结构是完全可具体化的,因此不支持方差(协变和逆变),这一点尤其正确。您只能将其用于接口的声明和委托;如果我没记错的话。

最后,当涉及到多态性时,通常会倾向于使用不必要的泛型类型;有或没有通配符参数化类型和方差。这通常会导致代码冗长而复杂;难以阅读和使用,甚至更难编写。我强烈建议您查看所有这些 Java 代码,看看是否真的有必要拥有所有这些东西,而不是只有多态性或多态性与泛型但没有方差或通配符参数化类型的组合的更简单的代码。

于 2013-01-12T12:34:12.797 回答