Scala 泛型

By | 12月18日
Advertisement

参考,

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...)

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

Similar Posts:

  • 泛型参数 V.S. 抽象类型成员

    by Bill Venners October 7, 2009 http://www.artima.com/weblogs/viewpost.jsp?thread=270195 摘要: 在这篇博客中,作者试图回答Scala编程中一个共同的问题:在Scala API设计时,什么时候用泛型参数,什么时候用抽象类型成员. -------------------------------------------------------------------------------------------

  • Scala学习——泛型[T]的6种使用

    package com.dtspark.scala.basics /** * 1,scala的类和方法.函数都可以是泛型. * * 2,关于对类型边界的限定分为上边界和下边界(对类进行限制) * 上边界:表达了泛型的类型必须是"某种类型"或某种类型的"子类",语法为"<:", * 下边界:表达了泛型的类型必须是"某种类型"或某种类型的"父类",语法为">:", * * 3,

  • Scala深入浅出进阶经典 第80讲:List的泛型分析以及::类和Nil对象

    Scala中List的定义: ::的源码: Nil的源码: 以上内容是从王家林老师DT大数据课程第80讲的学习笔记和个人整理. DT大数据微信公众账号:DT_Spark 王家林老师QQ:1740415547 王家林老师微信号:18610086859 第80讲视频网站地址:http://pan.baidu.com/s/1eQi2oLW

  • Scala 深入浅出实战经典 第80讲:List的泛型分析以及::类和Nil对象

    List有两个重要的子类: 空表Nil,有head和tail的非空表:: 信息来源于 DT大数据梦工厂,微信公众号:DT_Spark 视频地址:http://edu.51cto.com/lesson/id-71116.html

  • 关于Scala虚拟机的一些疑问

    我最近一直在学习scala这种被成为下一代java的语言,应该说它的一些特性挺吸引人的.但我最近注意到Eclipse推出了一个叫做Xtend的语言,在新闻中提到 Eclipse Xtend可以编译成可读的Java代码,类似CoffeeScript之与Javascript. 什么意思?就是把一种语法替换成另一种而已,其机理有点像我们使用的模版引擎,模版语言最终会被替换成目标语言执行.那么scala呢?似乎高级一点,它是直接编译成java class的(或者.net平台的CLR代码). 但很重要的一

  • 给Java开发者的Scala教程

    author:Michel Schinz,Philipp Haller 1. 简介 本文将该要的介绍Scala语言和其编译.这里假设读者已经有一定的java开发经验,需要概要的了解他们可以用Scala 做些什么. 2. 第一个例子 我们用全世界最著名的代码来作为开始.虽然没什么用,但是可以很好地直观的了解Scala: object HelloWorld { def main(args: Array[String]): Unit = { println("Hello, world!")

  • scala_集合_泛型_隐式转换

    集合 list set tuple map 访问数据应该是下划线1,2,如hostPort._1.下标是从1开始的. package cn.chinahadoop.scala class Basic5 { } //默认为val case class Book(name: String, author: String) object Basic5 extends App { // val value = 3 // //match是有返回值的 // val result = value match

  • Swift学习笔记泛型

    func swapTwoInts(inout a: Int, inout b: Int){ let temporatyA = a a = b b = temporatyA } func swapTwoValues<T>(inout a: T, inout b: T){ let temporaryA = a a = b b = temporaryA } var someInt = 3 var anotherInt = 107 swapTwoInts(&someInt, &anot

  • scala implicit 隐式转换

    我们经常在scala 源码里上看到类似implicit这个关键字. 一开始不太理解,后来看了看,发现implicit这个隐式转换是支撑scala的易用.容错以及灵活语法的基础. 我们经常使用的一些集合方法里面就用到了implicit,比如: def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit bf: CanBuildFrom[Repr, B, That]): That = { 1. 隐式转换函数的定义: 我们在scala re

  • 快学Scala

    快学Scala (Core Java作者Horstmann最新力作) (美)霍斯曼(Horstmann,C.S.)著 高宇翔译 ISBN978-7-121-18567-0 2012年10月出版 定价:79.00元 16开 408页 内 容 简 介 Scala是一门以Java虚拟机(JVM)为目标运行环境并将面向对象和函数式编程语言的最佳特性结合在一起的编程语言.你可以使用Scala编写出更加精简的程序,同时充分利用并发的威力.由于Scala运行于JVM之上,因此它可以访问任何Java类库并且与J

Tags: