泛型的协变和逆变,从Java通配符说起

来源:互联网 发布:税控开票软件下载 编辑:程序博客网 时间:2024/04/30 02:49


Java从1.5开始支持泛型[1],Martin Odersky[2]:“当Java刚出现时,Bill Joy和James Gosling以及其他Java组成员都认为,Java应该有泛型,只是他们没有足够的时间做出详细设计。所以由于Java中没有泛型,至少最初阶段没有,他们就认为,数组不得不是协变的。例如,这意味着一个字符串(String)数组是一个对象(Object)数组的子类型。其原因是他们希望能够重写,比如,一个“通用”排序方法,采用了一个对象数组和一个用来排序该数组的比较器,然后让你传送一个字符串数组的参数给它。通常情况下这属于类型不健全。这就是为什么在Java中你会获得一个数组存储例外。这实际上也证明,这种同样的事情引起了对于数组泛型实现的需求。这就是为什么在Java中泛型并不好使。你不能定义一个字符串的列表数组,这是不可能的。你只能被迫使用难看的原始类型,永远都只能是一个列表数组。因此,这有点类似原罪。他们对此做出了非常迅速的回应,认为这是一个快速破解。但随后实际上每一个设计决定都被毁灭了。因此,为了不陷入同样的陷阱,我们不得不中断,并提出现在我们将不向上兼容Java,我们也想做一些不同的事情。”

Java中使用通配符处理泛型的variance(变体、变型、可变性)问题。“通配符无疑非常复杂:由 Java 编译器产生的一些令人困惑的错误消息都与通配符有关,Java 语言规范中最复杂的部分也与通配符有关。”[3]

  • 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
  • 如果你想把对象写入一个数据结构里,使用 ? super 通配符
  • 如果你既想存,又想取,那就别用通配符。

“这就是Maurice Naftalin在他的《Java Generics and Collections》这本书中所说的存取原则,以及Joshua Bloch在他的《Effective Java》这本书中所说的PECS法则。”[4]“即 PECS 原则 (producser-extends, consumer-super) 或者也叫 Get and Put 原则”[5]。

“清单 3. 一旦将值从 box 中取出,则不能将其放回”[3]

public void rebox(Box<?> box) {    box.put(box.get());}Rebox.java:8: put(capture#337 of ?) in Box<capture#337 of ?> cannot be applied   to (java.lang.Object)    box.put(box.get());       ^1 error

“在这种情况下,由于 ? 实际表示 “?extends Object” ,编译器已经推断出 box.get() 的类型是 Object”[3],“当与Raw类型造型时,<?>在编译器的处理方式的确与<? Extends Object>有所不同,根据场景它可能被编译器忽略掉泛型信息而直接当作Raw类型,而<? Extends Object>则不会。”[5]

“scala为了兼容java泛型通配符的形式,引入存在类型”[6]。

“Existential类型。语法是List[T] forSome { type T }.。这看起来有点繁琐。这种繁琐的语法实际上是故意的,因为它产生的Existential类型通常有点难以处理”[7]。

“24) p379 存在类型”[8]

java中的Iterator<?>在scala中可写为:Iterator[T] forSome {type T}

java中的Iterator<? extends Component> 可写为:Iterator[T] forSome {type T <: Component}

表达形式为:type forSome{ declarations}

它可以缩写,比如:

Iterator[T] forSome {type T} 可简写为:Iterator[_]

Iterator[T] forSome {type T <: Component } 可简写为:Iterator[ _ <: Component ]

这个在scala也叫通配符(wildcards),“你也可以为通配符类型变量应用边界”[9]。

“scala中的某些特性比较难懂或有些晦涩,或有些特性是试验性质。”[10]“这些特性在普通场景下不鼓励使用,但在特定场景下又可能需要。”[10]包括但不限于“存在类型(existentials)、高阶类(higherKinds)、动态类(dynamics)、反射调用(reflectiveCalls)”[10]。

“Java并不支持声明点变型(declaration-site variance,即在定义一个类型时声明它为可变型,也称definition-site),而scala支持,可以在定义类型时声明(用加号表示为协变,减号表示逆变)”[6]。而Java泛型通配符的形称之为使用点变型(use-site variance)。

Martin Odersky[7]:“相比之下,Java带有通配符的方法意味着在类中你什么都做不了。你只是写List<T>。然后,如果用户想要一个协变list,他们不写List<Fruit>,而是写List<? extends Fruit>。所以这是一个通配符。问题是,这是用户代码。这些用户通常都没有类库设计人员那么专业。此外,这些注释间一个单一的不匹配将会带来类型错误。因此,难怪你会得到大量与通配符有关的非常棘手的错误信息,我认为这是Java泛型最重要的罪魁祸首。因为这种通配符的方法对于普通人来说确实是太复杂、太难于处理。”

C#只支持declaration site variance,不支持use site variance。“协变类型参数用 out 关键字(在 Visual Basic 中为 Out 关键字,在 MSIL 汇编程序中为 +)标记”[11],“逆变类型参数用 in 关键字(在 Visual Basic 中为 In 关键字,在 MSIL 汇编程序中为 -)标记”[11]。

“协变和逆变统称为“变体”。 未标记为协变或逆变的泛型类型参数称为“固定参数”。 有关公共语言运行时中变体的事项的简短摘要:”[11]

  • 在 .NET Framework 4 版中,Variant 类型参数仅限于泛型接口和泛型委托类型。
  • 泛型接口或泛型委托类型可以同时具有协变和逆变类型参数。
  • 变体仅适用于引用类型;如果为 Variant 类型参数指定值类型,则该类型参数对于生成的构造类型是不变的。
  • 变体不适用于委托组合。 也就是说,在给定类型 Action<Derived> 和 Action<Base>(在 Visual Basic 中为 Action(Of Derived) 和 Action(Of Base))的两个委托的情况下,无法将第二个委托与第一个委托结合起来,尽管结果将是类型安全的。 变体允许将第二个委托分配给类型 Action<Derived> 的变量,但只能在这两个委托的类型完全匹配的情况下对它们进行组合。

C#定义类时变体修饰符无效,这和scala不同[12]。

“19) p256 与java不同,scala中的数组不是协变的”[8],“.NET很不幸的模仿了Java的这个特性,也把数组设计为协变的”[13],“泛型出现后,数组的这个个性已经不再有使用上的必要了(下面一部分我们会谈到这个),实际上是应该避免使用”[4]。

最后是一段与C#“委托中的变体”[14]有关的代码。

using System;namespace VarianceInDelegates{class MainClass{public static void Main (string[] args){CheckType<string> ((string a) => {});CheckType<string> (delegate(string a) {});#region 委托的逆变,不适用于匿名函数// Error CS1661: 无法将 lambda 表达式 转换为委托类型“System.Action<string>”,原因是该参数类型与委托参数类型不匹配// Error CS1678: 参数 1 已声明为类型“object”,但应为“string”/*CheckType<string> ((object o) => {});CheckType<string> (new Action<string> ((object o) => {}));*/// Error CS1661: 无法将 匿名方法 转换为委托类型“System.Action<string>”,原因是该参数类型与委托参数类型不匹配// Error CS1678: 参数 1 已声明为类型“object”,但应为“string”/*CheckType<string> (delegate(object o) {});CheckType<string> (new Action<string> (delegate(object o) {}));*/CheckType<string> (TestAction);CheckType<string> (new Action<string> (TestAction));#endregion 委托的逆变,不适用于匿名函数#region 委托的泛型逆变,项目目标平台低于.NET4.0时编译不通过// Error CS1502: 与“VarianceInDelegates.MainClass.CheckType<string>(System.Action<string>)”最匹配的重载方法具有一些无效参数// Error CS1503: 参数 1: 无法从“System.Action<object>”转换为“System.Action<string>”CheckType<string> (new Action<object> ((object o) => {}));CheckType<string> (new Action<object> (delegate(object o) {}));#endregion 委托的泛型逆变,项目目标平台低于.NET4.0时编译不通过}public static void CheckType<T> (Action<T> act){Console.WriteLine (typeof(Action<T>).ToString ());Console.WriteLine ((act is Action<T> ? "Yes, " : "No, ") + act.ToString ());}public static void TestAction (object o){}}}

运行结果:

System.Action`1[System.String]Yes, System.Action`1[System.String]System.Action`1[System.String]Yes, System.Action`1[System.String]System.Action`1[System.String]Yes, System.Action`1[System.String]System.Action`1[System.String]Yes, System.Action`1[System.String]System.Action`1[System.String]Yes, System.Action`1[System.Object]System.Action`1[System.String]Yes, System.Action`1[System.Object]


参考

[1] Java1.5泛型指南中文版(Java1.5 Generic Tutorial)
http://blog.csdn.net/explorers/article/details/454837

[2] Scala创始人:创造比Java更好的语言
http://developer.51cto.com/art/200905/124636_all.htm  

[3] Java 理论与实践: 使用通配符简化泛型使用
http://www.ibm.com/developerworks/cn/java/j-jtp04298.html

[4] Java泛型简明教程
http://www.vaikan.com/java-generics-quick-tutorial/

[5] java泛型的理解
http://hongjiang.info/java-generics/

[6] scala类型系统:15) 协变与逆变
http://hongjiang.info/scala-covariance-and-contravariance/

[7] Scala的类型系统:取代复杂的通配符
http://developer.51cto.com/art/200906/127934.htm

[8] Programming in Scala的阅读笔记
http://hongjiang.info/programming-in-scala-notes/

[9] Scala School - 类型和多态基础
http://twitter.github.io/scala_school/zh_cn/type-basics.html
http://www.importnew.com/4126.html

[10] scala2.10中采纳了SIP-18:模块化语言特性
http://hongjiang.info/scala-sip-18/

[11] 泛型中的协变和逆变
https://msdn.microsoft.com/zh-cn/library/dd799517(v=vs.100).aspx

[12] 为什么C#中型变不能直接修饰在类定义上?
https://www.zhihu.com/question/40744099

[13] 数组协变带来的静态类型漏洞
http://rednaxelafx.iteye.com/blog/379703

[14] 委托中的变体(C# 和 Visual Basic)
https://msdn.microsoft.com/zh-cn/library/dd799517(v=vs.100).aspx


0 0
原创粉丝点击