泛型的协变和逆变,从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
- 泛型的协变和逆变,从Java通配符说起
- JAVA泛型杂谈--擦除,协变,逆变,通配符等
- 泛型类型的转换,协变和逆变
- C# 泛型的协变和逆变
- C# 泛型的协变和逆变
- C# 泛型的协变和逆变
- Kotlin 泛型的协变和逆变
- C# 泛型的协变和逆变
- 泛型的协变,逆变
- 使用泛型实现类型转化-使用通配符上限下限区分协变和逆变采取不同的转化方式
- Java协变和逆变
- Java泛型里的协变和逆变
- C#的协变和逆变
- java协变,逆变,不可变
- C#泛型的协变和抗变
- 泛型的逆变和抗变
- 型变的理解,逆变与协变
- 协变和逆变
- Android,水波进度条
- 读《解忧杂货店》
- Java设计模式:策略模式 Strategy
- 深入理解HTTP协议
- 多文件上传插件
- 泛型的协变和逆变,从Java通配符说起
- shell 脚本格式化输出
- Espresso ui单元测试框架初探
- 多态之虚函数
- TreeSet的使用方法和案例详解
- TCP和UDP的区别
- MySQL笔记——主键使用的好习惯
- bzoj2302 problem c 递推
- 如何将MATLAB程序发布为独立的不依赖MATLAB环境可执行的程序包(基于Matlab R2015b版 )