开篇先是举了一个Int类型栈的例子,说明如果想要一个String类型的栈,就要重写这些相同的东西。一个避免写重复代码的方法就是把类型参数化(parameterize)。泛型的写法如下:
abstract class Stack[A]{ def push(x : A) : Stack[A] = ... def isEmpty : Boolean def top : A def pop : Stack[A] }
在上面的定义中,“A”是一个类型参数,可被用在Stack类和它的子类中。类参数可以是任意(arbitrary)的名字。用[]来包围,而不是用()来包围,用以和值参数进行区别。
也可以用类型去参数化方法。例如:
def isPrefix[A](p: Stack[A] , s : Stack[A]) : Boolean = { p.isEmpty || p.top == s.top && isPrefix[A]( p.pop , s.pop) }
这个方法参数被叫作“多形性”(polymorphic)。意思为“有很多的类型(forms)”。在使用它的时候,应该把类型参数(如String,Int)和方法参数一起传过去。
本地类型推断(Inference):如果在程序中一直使用传像[Int]或是[String]这样的类型参数会变得乏味。经常性的,在一个类型参数中的信息是多余的(redundant),因为当前的类型参数可以从检查函数的值或者期望的返回值类型中得到确定。Scala中有一个强大的类型推断器,可以推断出所需的类型参数。
8.1 类型参数边界
在用类型参数定义了一个抽象类Set[A]后,在实现中要用到比较(<>),但是不能确定A的具体类型,因此不能直接使用。一个解决办法就是对合法类型进行限制,对只含有方法<>的类型放行。在标准库里有一个特质Ordered[A],用来表示可比较的类型。现在可以强制要求这个类型为Ordered的子类型。可以通过给出一个上界(upper bound)的方式来解决这个问题:
trait Set[A <: Ordered[A]] { def incl(x : A) : Set[A] def contains(x : A) : Boolean }
这样定义后,传入的A类型参数必须是Ordered[A]的子类型。也即,可以被比较。
class EmptySet[A <: Ordered[A]] extends Set[A] { def contains(x: A): Boolean = false def incl(x: A): Set[A] = new NonEmptySet(x, new EmptySet[A], new EmptySet[A]) } class NonEmptySet[A <: Ordered[A]] (elem: A, left: Set[A], right: Set[A]) extends Set[A] { def contains(x: A): Boolean = if (x < elem) left contains x else if (x > elem) right contains x else true def incl(x: A): Set[A] = if (x < elem) new NonEmptySet(elem, left incl x, right) else if (x > elem) new NonEmptySet(elem, left, right incl x) else this }
可以看到,在new NonEmptySet(...)时,没有写入类型参数。因为可以从期待的返回值类型中推断出来。
先创建一个Ordered的子类,如下 :
case class Num(value : Double) extends Ordered[Num] { //compare(that : A) : Int 是Ordered[A] 的抽象方法 def compare(that : Num) : Int = if ( this.value < that.value) -1 else if ( this.value > that.value) 1 else 0 }
一个关于参数边界的问题就是它们需要考虑到以下问题:如果Sum类不是声明为一个Ordered的子类,就不能在集合中使用它。同样,Java中的Int,Double什么的也都不是Ordered子类,所以也不能声明为这里的元素。
一个稍为复杂的设计,允许使用这些元素,就View Bounds来代替原来的类型边界(type bounds)。唯一的变化是如下:
trait Set[A <% Orfered[A]] ... class EmptySet[A <% Ordered[A]] ...
View Bounds比原来的边界要弱:<% 指明A必须可以转化为边界类型T,通过使用隐式转换。(implicit conversion)
8.2 变化型注解(variance annotation):即讨论了协变(co-variant)和逆变(contra-variant)的问题。如果Stack[String]是否是Stack[AnyRef]的子类。可以通过如下定义进行协变:
class Stack[+A]{ ... }
类型参数A前面的+表示这是一个协变的。同样,“-”表示的是逆变的。Java中的List之类的是不是协变的,但数组是协变的。这样可以通过编译,但在执行时会报错。
在纯净的函数式世界里,所有类型都是协变的。但是当引入了可变数据后,情况就变了。在Scala里默认不是协变的。协变类型应该出现在协变位置,这些位置包括:类里值的参数类型;方法的返回值类型;以及其他协变类型中的参数。放在其他地方会被拒绝。如下,则会被拒绝:
class Array[+A] { def apply(index : Int) : A def update(index : Int , elem : A) }
协变参数不能放在传值参数类型里,这样会破坏完整性。不过可以通过下界(lower bounds)来解决这个问题。
8.3 下界
已经见过上界(upper bounds)。在类型声明中声明:T >: S,则T就严格的成为了S的父类了。可以这样同时定义上界和下界:
T >:S <: U。如果做如下定义,就可以解决刚才的问题:
class Stack[+A] { def push[ B >: A](x : B) : Stack[B] = new NonEmptySet(x , this) }
现在A所在的位置就是一个协变位置。
总之,不要犹豫在数据结构中去使用变化型注解。编译器会检查潜在的健全性问题。
8.5 元组(tuples):
有时一个函数会返回多个值。可以用到元组,而不用构造一个新的类。Scala里给了一个类Tuple2,可以如下使用:
def divmod(x : Int , y : Int) = new Tuple2[Int , Int ]( x / y , x % y )
想要访问元组里的数据,可以如下:val xy = divmod(x , y ) ; xy._1 , xy._2。或者也可以用模式匹配来得到每个值。元组是个case class 。也可以直接通过(...)来表示元组。所以上面的例子也可以被定义为:
def divmod( x : Int , y : Int ) : (Int , Int) = (x / y , x % y)
8.6 函数
Scala是函数式语言,所以函数是头等值,同时它也是面向对象语言,所以所有的值都是对象。Scala里Function1 trait的定义如下:
package scala trait Function1[-A , +B]{ def apply( x : A) : B }
(又涉及到逆变的概念,不是很懂。)