Scala 泛型

参考,

Programing in Scala

scala中的协变和逆变

----------------------------------------------------------------------------------------------------------------------------------------------------

首先Scala和Clojure虽然都是基于JVM的FP语言, 但其实差异挺大的, Scala更像Java, 而Clojure更象Lisp, base在不同的两极, 向中间靠近
所以对于Clojure而言, 你不需要太精通Java, 但Scala不行, 一个Scala工程师一定是一个资深的Java工程师

所以这里如果对Java的泛型不了解, 就很难理解Scala的泛型

 

Scala的泛型本质上和Java的没有区别, 因为本身就是基于Java泛型的
所以Scala默认也是不支持泛型的协变的

为什么不直接支持泛型的协变?
因为不是所有的情况下都存在协变的, 默认支持协变会带来问题

case1, 有可赋值字段的情况下
比如下面的例子, 如果默认支持协变, 那么下面对Cell的使用看上去就没有问题, 但实际使用会报错, 因为你set一个int, 却读出string

class Cell[T](init: T) {
    private[this] var current = init
    def get = current
    def set(x: T) { current = x }
}

val c1 = new Cell[String]("abc")
val c2: Cell[Any] = c1
c2.set(1)
val s: String = c1.get //error

 

case2, 在没有可赋值字段的情况下

class StrangeIntQueue extends Queue[Int] {
  override def append(x: Int) = {
    println(Math.sqrt(x)) //开方操作
    super.append(x)
  }
}

val x: Queue[Any] = new StrangeIntQueue
x.append("abc")  //会发生对string做开方的情况, 所以StrangeIntQueue是无法支持协变的

所以结论就是, 协变并不是适用于所有情况的, 必须开发者自己判断, 在需要和支持协变的地方加上协变标记
比如在Scala中对于List就是支持协变的(class List [+A]), 但对于可变list就是不可协变的(class MutableList [A])


数组的协变

在Java中数组是个特例容器, 它是默认支持协变的, 当然它也无法避免协变带来的问题

String[] a1 = { "abc" };
Object[] a2 = a1;
a2[0] = new Integer(17);
String s = a1[0]; // ArrayStore Exception

之所以在Java中这么设计, 因为在Java数组设计的时候, 还没有泛型, 设计者希望数组也有和类型一样的泛化效果, 所以加上了协变
但是当后来加上泛型后, 因为考虑到兼容性, 数组的协变就一直被保留下来了

而在Scala中, 没有必要继续保持这种特例
所以在Scala中, 数组也是默认不支持协变的

scala> val a1 = Array("abc")
a1: Array[java.lang.String] = Array(abc)
scala> val a2: Array[Any] = a1
<console>:5: error: type mismatch;
found : Array[java.lang.String]
required: Array[Any]
val a2: Array[Any] = a1

 

Variance

协变

[+T], covariant (or “flexible”) in its type parameter T

类似Java中的(? extends T), 即可以用T和T的子类来替换T

 

逆变

[-T], contravariant, 类似(? supers T)
if T is a subtype of type S, this would imply that Queue[S] is a subtype of Queue[T]

相对于协变,t是s的子类,那么Queue[T]也是Queue[S]的子类

逆变是反过来,当t是s的子类,而Queue[S]反而是Queue[T]的子类

看看如何理解,

Liskov Substitution Principle (里氏替换原则)

It is safe to assume that a type T is a subtype of a type U if you can substitute a value of type T wherever a value of type U is required. 
The principle holds if T supports the same operations as U and all of T’s operations require less and provide more than the corresponding operations in U.

根据里氏替换原则, 如果你可以用T替换U, 那么可以认为T是U的子类
这说明T支持U的所有接口, 并且T的操作需要的less和提供的more

所以对于逆变, 当T是S的子类的时候, 你可以用Queue[S]去替换Queue[T], 根据里氏替换原则, 意味着Queue[S]是Queue[T]的子类, 跟T和S的关系相反, 所以称为”逆”

这个比较难于理解, 在什么地方会用到?

比如下面的OutputChannel,
String是AnyRef的子类, 但是OutputChannel[AnyRef], 却是OutputChannel[String]的子类, 怎么理解?
对于OutputChannel[String], 支持的操作就是输出一个string, 同样OutputChannel[AnyRef]也一定可以支持输出一个string, 因为它支持输出任意一个AnyRef(它要求的比OutputChannel[String]少)
但反过来就不行, OutputChannel[String]只能输出String, 显然不能替换OutputChannel[AnyRef]

trait OutputChannel[-T]
{
    def write(x: T)
}

 理解这个要用广义的子类概念,即里氏替换原则,用感性你很难理解的
子类,require less,provide more

 

上界, 下界

Scala的上界和下界比较难理解, 因为和Java里面的界不是一个意思...

Java中, (? extends T), T称为上界, 比较容易理解, 代表T和T的子类, (? supers T), T称为下界

 

Scala中, 界却用于泛型类中的方法的参数类型上, 如下面的例子,
对于Queue中的append的类型参数直接写T, 会报错 (error: covariant type T occurs in contravariant position in type T of value x)
这个地方比较复杂, 简单的说就是Scala内部实现是, 把类中的每个可以放类型的地方都做了分类(+, –, 中立), 具体分类规则不说了
对于这里最外层类[+T]是协变, 但是到了方法的类型参数时, 该位置发生了翻转, 成为-逆变的位置, 所以你把T给他, 就会报错说你把一个协变类型放到了一个逆变的位置上

所以这里的处理的方法就是, 他要逆变, 就给他个逆变, 使用[U >: T], 其中T为下界, 表示T或T的超类, 这样Scala编译器就不报错了

class Queue[+T] (private val leading: List[T],
                 private val trailing: List[T] ) {
  def append[U >: T](x: U) = new Queue[U](leading, x :: trailing) //使用T的超类U来替换T
}

同样对于上界也是,

trait OutputChannel[-T]
{
    def write [U<:T] (x: U) //使用T的子类U来替换T
}

 

泛型数组, ClassManifest

Scala 2.8中新的数组

http://www.scala-blogs.org/2008/10/manifests-reified-types.html

http://www.scala-lang.org/api/2.9.2/scala/reflect/ClassManifest.html

http://www.scala-lang.org/api/current/#scala.reflect.Manifest

What is a Manifest in Scala and when do you need it?

http://scala-programming-language.1934581.n4.nabble.com/What-s-the-difference-between-ClassManifest-and-Manifest-td2125122.html

http://blogs.atlassian.com/2012/12/scala-and-erasure/

 

又是一个晦涩的问题
Mention generics to anyone who knows much about them and they’ll usually have an opinion on type reification (具体化) and erasure (擦除).
Different platforms have different strategies for their generics implementations. C++使用的reification
而Java的泛型系统使用的是erasure,

当然泛型擦除的最大问题是, it doesn’t tell you at runtime what its actual type is (在运行期无法知道类型信息), 除了手工的将java.lang.Class<T> objects作为参数传入

public class ClassTypeCapture<T> {
    Class<T> kind; 
    public ClassTypeCapture(Class<T> kind) { //T本身无法保留到运行期, 故传入class对象
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg); //使用class.isInstance
    }
}

当然这个不是优雅的方案
Scala has the ability to automagically produce values via implicit resolution. It also (in 2.8/2.9) has a feature known as Class Manifests.

Manifest. An object of type Manifest[T] provides complete information about the type T

Starting with version 2.7.2, Scala has added manifests, an undocumented (and still experimental) feature for reifying types.
They take advantage of a pre-existing Scala feature: implicit parameters.

def arr[T] = new Array[T](0)    // does not compile, 运行时无法知道什么是T
def arr[T](implicit m: Manifest[T]) = new Array[T](0)  // compiles, 将Manifest[T]当作隐含参数传入
def arr[T: Manifest] = new Array[T](0)    // shorthand for the preceding, 缩写

Scala的方案比Java的优雅一些, 但本质是一样的, 因为利用了implicit parameters特性, 这个manifest对象(类似class对象)会由编译器自动作为隐式参数加入 

 

<P extends Page> P visit(Class<P> pageClass) {…} //Java
def goto[P <: Page: Manifest]: P = app.visit(manifest[P].erasure.asInstanceOf[Class[P]]) //Scala

a. using manifest[P], a shorthand for implicitly[Manifest[P]]
b. def erasure: Class[_] //2.10中被runtimeClass: Class[_]替换, 返回Class对象,其中泛型信息已经被擦除

this says, given some type parameter P that is a subtype of Page and has-a Manifest[P], call app.visit and pass the class instance that we implicitly summon. Lastly (and unfortunately) we don’t in the type know the erased class’s type, but by construction we know it is correct so we cast using asInstanceOf[Class[P]].

 

ClassManifest, weaker form which can be constructed from knowing just the top-level class of a type, without necessarily knowing all its argument types.

A ClassManifest[T] is an opaque descriptor for type T.
It is used by the compiler to preserve information necessary for instantiating Arrays in those cases where the element type is unknown at compile time.
编译器用于保留必要的数组的类型信息到运行期, 以便于在运行期实例化泛型数组
对于泛型的运行期实例化, ClassManifest就足够, 不需要Manifest

Manifest和ClassManifest的区别

type T = Foo[Bar[Baz]]

then Manifest[T] represents the entire Foo[Bar[Baz]] type, whereas ClassManifest[T] only represents Foo[_].

For instantiating Arrays, you only need to know whether it's a primitive array or a reference array, so only the ClassManifest is needed. (But a Manifest is also a ClassManifest...)

虽然不完全准确, 但这样便于理解

posted on 2013-12-18 15:32 fxjwind 阅读(...) 评论(...) 编辑 收藏

统计