我一直试图将一些使用(有界)通配符泛型的Java代码翻译成C#。我的问题是,Java似乎允许泛型类型在与通配符一起使用时既是协变的又是逆变的。
[这是前一个question分拆处理更简单的有界通配符的情况]
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>'.
答案 0 :(得分: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
时抱怨。
答案 1 :(得分: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();
其次,我看了参考.NET equivalent for Java wildcard generics <?> with co- and contra- variance?,但我必须承认,我不明白为什么这可以被视为一种解决方案。类型限制(例如<T extends Impl>
)与无界通配符参数化类型(<?>
)或方差(<? extends Impl>
)无关,我看不到如何用第一个替换秒数作为一般解决方案。但是,在某些情况下,如果您不需要使用通配符参数化类型(<?>
)或方差类型而不是,则可以进行此转换。但是,如果您在Java代码中没有真正使用它们,那么也应该更正此代码。
使用Java泛型,你可以引入很多不精确但你不会用C#编译器获得这个机会。考虑到在C#中,类和结构是完全可恢复的,因此不支持方差(协方差和逆变),这尤其正确。您只能将其用于声明接口和委托;如果我没记错的话。
最后,当涉及多态性时,通常存在使用不必要的泛型类型的不良倾向;有或没有通配符参数化类型和方差。这通常会导致代码冗长而复杂;难以阅读和使用,甚至更难写。我强烈建议你查看所有这些Java代码,看看是否真的有必要拥有所有这些东西而不是更简单的代码,只有多态或多态与通用但没有方差或通配符参数化类型的组合。