Scala 泛型以及泛型约束

1. 泛型类

泛型类指可以接受类型参数的类。泛型类在集合类中被广泛使用。

1.1 泛型类

  • 在类声明时,定义一些泛型类型,然后在类的内部,就可以使用这些泛型类型
  • 在需要对类中的某些成员,如字段或方法中的参数进行统一的类型限制时,可以使用泛型类,使得程序具有更好的健壮性和稳定性
  • 在使用类的时候,将类型参数替换为实际的类型即可
  • scala会自动推断泛型类型:给泛型类型的字段赋值时,scala会自动对类型进行推断

泛型函数:

  • 与泛型类相似,在声明函数时指定泛型类型,然后在函数体内,多个变量或返回值,就可以使用泛型类型进行声明。
  • 可以通过给使用了泛型类型的变量传递值,让scala自动推断泛型的实际类型,也可以在调用函数的时候,手动指定泛型的实际类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Triple[X, Y, Z](val first: X, val second: Y, val thrid: Z)

object Hello_Type_Parameterization {
def main(args: Array[String]): Unit = {
//在定义后scala的类型推断会得出triple类型为 Triple[String, Int, Double]
val triple = new Triple("Spark", 3, 3.1415926)
//显示声明类型
val bigData = new Triple[String, String, Char]("Spark", "Hadoop", 'R')


//定义泛型函数
def getData[T](list: List[T]) = list(list.length / 2)
println(getData(List("Spark", "Hadoop", 'R'))) //Hadoop
//显式指定类型
val f = getData[Int] _ //val f: List[Int] => Int
println(f(List(1,2,3,4,5,6,7,8))) //5

//定义参数也存在上下文的约束
def foo[A, B](f: A => List[A], b: A) = f(b)
}
}

1.2 定义一个泛型类

泛型类使用方括号 [] 来接受类型参数。一个惯例是使用字母 A 作为参数标识符,当然你可以使用任何参数名称。

1
2
3
4
5
6
7
8
9
10
class Stack[A] {
private var elements: List[A] = Nil
def push(x: A) { elements = x :: elements }
def peek: A = elements.head
def pop(): A = {
val currentTop = peek
elements = elements.tail
currentTop
}
}

上面的 Stack 类的实现中接受类型参数 A。 这表示其内部的列表,var elements: List[A] = Nil,只能够存储类型 A 的元素。方法 def push 只接受类型 A 的实例对象作为参数(注意:elements = x :: elements 将 elements 放到了一个将元素 x 添加到 elements 的头部而生成的新列表中)。

1.3 使用

要使用一个泛型类,将一个具体类型放到方括号中来代替 A。

1
2
3
4
5
val stack = new Stack[Int]
stack.push(1)
stack.push(2)
println(stack.pop) // prints 2
println(stack.pop) // prints 1

实例对象 stack 只能接受整型值。然而,如果类型参数有子类型,子类型可以被传入:

1
2
3
4
5
6
7
8
9
10
class Fruit
class Apple extends Fruit
class Banana extends Fruit

val stack = new Stack[Fruit]
val apple = new Apple
val banana = new Banana

stack.push(apple)
stack.push(banana)

类 Apple 和类 Banana 都继承自类 Fruit,所以我们可以把实例对象 apple 和 banana 压入栈 Fruit 中。

注意:泛型类型的子类型是*不可传导*的。这表示如果我们有一个字母类型的栈 Stack[Char],那它不能被用作一个整型的栈 Stack[Int]。否则就是不安全的,因为它将使我们能够在字母型的栈中插入真正的整型值。结论就是,只有当类型 B = A 时, Stack[A] 是 Stack[B] 的子类型才成立。因为此处可能会有很大的限制,Scala 提供了一种 类型参数注释机制 用以控制泛型类型的子类型的行为。

2. 型变

型变是复杂类型的子类型关系与其组件类型的子类型关系的相关性。 Scala支持 泛型类 的类型参数的型变注释,允许它们是协变的,逆变的,或在没有使用注释的情况下是不变的。 在类型系统中使用型变允许我们在复杂类型之间建立直观的连接,而缺乏型变则会限制类抽象的重用性。

1
2
3
class Foo[+A] // A covariant class
class Bar[-A] // A contravariant class
class Baz[A] // An invariant class

2.1 协变(covariant class)

使用注释 +A,可以使一个泛型类的类型参数 A 成为协变。 对于某些类 class List[+A],使 A 成为协变意味着对于两种类型 A 和 B,如果 A 是 B 的子类型,那么 List[A] 就是 List[B] 的子类型。 这允许我们使用泛型来创建非常有用和直观的子类型关系。

考虑以下简单的类结构:

1
2
3
4
5
abstract class Animal {
def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

类型 Cat 和 Dog 都是 Animal 的子类型。 Scala 标准库有一个通用的不可变的类 sealed abstract class List[+A],其中类型参数 A 是协变的。 这意味着 List[Cat] 是 List[Animal],List[Dog] 也是 List[Animal]。 直观地说,猫的列表和狗的列表都是动物的列表是合理的,你应该能够用它们中的任何一个替换 List[Animal]。

在下例中,方法 printAnimalNames 将接受动物列表作为参数,并且逐行打印出它们的名称。 如果 List[A] 不是协变的,最后两个方法调用将不能编译,这将严重限制 printAnimalNames 方法的适用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
object CovarianceTest extends App {
def printAnimalNames(animals: List[Animal]): Unit = {
animals.foreach { animal =>
println(animal.name)
}
}

val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))
val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))

printAnimalNames(cats)
// Whiskers
// Tom

printAnimalNames(dogs)
// Fido
// Rex
}

2.1 逆变(contravariant class)

通过使用注释 -A,可以使一个泛型类的类型参数 A 成为逆变。 与协变类似,这会在类及其类型参数之间创建一个子类型关系,但其作用与协变完全相反。 也就是说,对于某个类 class Writer[-A] ,使 A 逆变意味着对于两种类型 A 和 B,如果 A 是 B 的子类型,那么 Writer[B] 是 Writer[A] 的子类型。

考虑在下例中使用上面定义的类 Cat,Dog 和 Animal :

1
2
3
abstract class Printer[-A] {
def print(value: A): Unit
}

这里 Printer[A] 是一个简单的类,用来打印出某种类型的 A。 让我们定义一些特定的子类:

1
2
3
4
5
6
7
8
9
class AnimalPrinter extends Printer[Animal] {
def print(animal: Animal): Unit =
println("The animal's name is: " + animal.name)
}

class CatPrinter extends Printer[Cat] {
def print(cat: Cat): Unit =
println("The cat's name is: " + cat.name)
}

如果 Printer[Cat] 知道如何在控制台打印出任意 Cat,并且 Printer[Animal] 知道如何在控制台打印出任意 Animal,那么 Printer[Animal] 也应该知道如何打印出 Cat 就是合理的。 反向关系不适用,因为 Printer[Cat] 并不知道如何在控制台打印出任意 Animal。 因此,如果我们愿意,我们应该能够用 Printer[Animal] 替换 Printer[Cat],而使 Printer[A] 逆变允许我们做到这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
object ContravarianceTest extends App {
val myCat: Cat = Cat("Boots")

def printMyCat(printer: Printer[Cat]): Unit = {
printer.print(myCat)
}

val catPrinter: Printer[Cat] = new CatPrinter
val animalPrinter: Printer[Animal] = new AnimalPrinter

printMyCat(catPrinter)
printMyCat(animalPrinter)
}

这个程序的输出如下:

1
2
The cat's name is: Boots
The animal's name is: Boots

2.3 不变(invariant class)

默认情况下,Scala中的泛型类是不变的。 这意味着它们既不是协变的也不是逆变的。 在下例中,类 Container 是不变的。 Container[Cat] 不是 Container[Animal],反之亦然。

1
2
3
4
5
6
7
class Container[A](value: A) {
private var _value: A = value
def getValue: A = _value
def setValue(value: A): Unit = {
_value = value
}
}

可能看起来一个 Container[Cat] 自然也应该是一个 Container[Animal],但允许一个可变的泛型类成为协变并不安全。 在这个例子中,Container 是不变的非常重要。 假设 Container 实际上是协变的,下面的情况可能会发生:

1
2
3
4
val catContainer: Container[Cat] = new Container(Cat("Felix"))
val animalContainer: Container[Animal] = catContainer
animalContainer.setValue(Dog("Spot"))
val cat: Cat = catContainer.getValue // 糟糕,我们最终会将一只狗作为值分配给一只猫

幸运的是,编译器在此之前就会阻止我们。

2.4 其他例子

另一个可以帮助理解型变的例子是 Scala 标准库中的 trait Function1[-T, +R]。 Function1 表示具有一个参数的函数,其中第一个类型参数 T 表示参数类型,第二个类型参数 R 表示返回类型。 Function1 在其参数类型上是逆变的,并且在其返回类型上是协变的。 对于这个例子,我们将使用文字符号 A => B 来表示 Function1[A, B]。

假设前面使用过的类似 Cat,Dog,Animal 的继承关系,加上以下内容:

1
2
abstract class SmallAnimal extends Animal
case class Mouse(name: String) extends SmallAnimal

假设我们正在处理接受动物类型的函数,并返回他们的食物类型。 如果我们想要一个 Cat => SmallAnimal(因为猫吃小动物),但是给它一个 Animal => Mouse,我们的程序仍然可以工作。 直观地看,一个 Animal => Mouse 的函数仍然会接受一个 Cat 作为参数,因为 Cat 即是一个 Animal,并且这个函数返回一个 Mouse,也是一个 SmallAnimal。 既然我们可以安全地,隐式地用前者代替后者,我们可以说 Animal => Mouse 是 Cat => SmallAnimal 的子类型。

2.5 与其他语言的比较

某些与 Scala 类似的语言以不同的方式支持型变。 例如,Scala 中的型变注释与 C# 中的非常相似,在定义类抽象时添加型变注释(声明点型变)。 但是在Java中,当类抽象被使用时(使用点型变),才会给出型变注释。

3. 类型边界(Bounds)

  • <: :指明上界,表达了泛型的类型必须是”某种类型”或某种类型的”子类”
  • ‘>: :指明下界,表达了泛型的类型必须是”某种类型”或某种类型的”父类”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 这里的[T <: Comparable[T]] 表示类型T必须是Comparable[T]的子类
* 如果T为Comparable[T]的子类了,那么T一定会有compareTo这个方法,这是一个java的方法
*/
class Pair[T <: Comparable[T]](val first: T, val second: T) {
def bigger = if(first.compareTo(second) > 0) first else second
}


object Type_Variable_Bounds {

def main(args: Array[String]): Unit = {
val pair = new Pair("Spark", "Hadoop")
println(pair.bigger) //Spark

}
}

3.1 类型上界

在Scala中,类型参数和抽象类型都可以有一个类型边界约束。这种类型边界在限制类型变量实际取值的同时还能展露类型成员的更多信息。比如像T <: A这样声明的类型上界表示类型变量T应该是类型A的子类。下面的例子展示了类PetContainer的一个类型参数的类型上界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
abstract class Animal {
def name: String
}

abstract class Pet extends Animal {}

class Cat extends Pet {
override def name: String = "Cat"
}

class Dog extends Pet {
override def name: String = "Dog"
}

class Lion extends Animal {
override def name: String = "Lion"
}

class PetContainer[P <: Pet](p: P) {
def pet: P = p
}

val dogContainer = new PetContainer[Dog](new Dog)
val catContainer = new PetContainer[Cat](new Cat)
// this would not compile
val lionContainer = new PetContainer[Lion](new Lion)

类PetContainer接受一个必须是Pet子类的类型参数P。因为Dog和Cat都是Pet的子类,所以可以构造PetContainer[Dog]和PetContainer[Cat]。但在尝试构造PetContainer[Lion]的时候会得到下面的错误信息:

1
type arguments [Lion] do not conform to class PetContainer's type parameter bounds [P <: Pet]

这是因为Lion并不是Pet的子类。

3.2 类型下界

类型上界 将类型限制为另一种类型的子类型,而 类型下界 将类型声明为另一种类型的超类型。 术语 B >: A 表示类型参数 B 或抽象类型 B 是类型 A 的超类型。 在大多数情况下,A 将是类的类型参数,而 B 将是方法的类型参数。

下面看一个适合用类型下界的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Node[+B] {
def prepend(elem: B): Node[B]
}

case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {
def prepend(elem: B): ListNode[B] = ListNode(elem, this)
def head: B = h
def tail: Node[B] = t
}

case class Nil[+B]() extends Node[B] {
def prepend(elem: B): ListNode[B] = ListNode(elem, this)
}

该程序实现了一个单链表。 Nil 表示空元素(即空列表)。 class ListNode 是一个节点,它包含一个类型为 B (head) 的元素和一个对列表其余部分的引用 (tail)。 class Node 及其子类型是协变的,因为我们定义了 +B。

但是,这个程序 不能 编译,因为方法 prepend 中的参数 elem 是协变的 B 类型。 这会出错,因为函数的参数类型是逆变的,而返回类型是协变的。

要解决这个问题,我们需要将方法 prepend 的参数 elem 的型变翻转。 我们通过引入一个新的类型参数 U 来实现这一点,该参数具有 B 作为类型下界。

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Node[+B] {
def prepend[U >: B](elem: U): Node[U]
}

case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {
def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem, this)
def head: B = h
def tail: Node[B] = t
}

case class Nil[+B]() extends Node[B] {
def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem, this)
}

现在我们像下面这么做:

1
2
3
4
5
6
7
trait Bird
case class AfricanSwallow() extends Bird
case class EuropeanSwallow() extends Bird

val africanSwallowList= ListNode[AfricanSwallow](AfricanSwallow(), Nil())
val birdList: Node[Bird] = africanSwallowList
birdList.prepend(EuropeanSwallow())

可以为 Node[Bird] 赋值 africanSwallowList,然后再加入一个 EuropeanSwallow。

3.3 视图界定 View Bounds

  • view bounds其实就是bounds 上边界的加强版本,对bounds的补充 <变成<%
  • 可以利用implicit隐式转换将实参类型转换成目标类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
当给下面这个类传入3、5时会报错。因为3、5不是Comparable[T]的子类
class Pair_NotPerfect[T <: Comparable[T]](val first: T, val second: T) {
def bigger = if (first.compareTo(second) > 0) first else second
*/

/*
*将<:改成 <%就是视图界定 这样就可以传递3和5了,就不会报错了
*我们可以把传入的T类型的实例隐式的转换成Comparable[T]类型
*/
class Pair_NotPerfect[T <% Comparable[T]](val first: T, val second: T) {
def bigger = if (first.compareTo(second) > 0) first else second
}

Ordered视图界定
/**
*
*上面这种方式的12行first.compareTo(second) > 0 通过compareTo来比较 但是不能直观的像数学比较那样清晰
* Scala提供了Ordered视图界定,Ordered在Comparable上提供一些关系型的操作符 < > <= >=等
*
*/
class Pair_Batter[T <% Ordered[T]](val first: T, val second: T) {
//这里的 > 是因为Ordered中提供的方法
def bigger = if (first > second) first else second
}



object View_Bounds {
def main(args: Array[String]): Unit = {
var pair = new Pair_NotPerfect("Spark", "Hadoop")
println(pair.bigger) //Spark

/*
* 当类型界定为Pair_NotPerfect[T <: Comparable[T]]报错 因为Int本身不是Comparable的子类
*
* 当类型界定为视图界定时Pair_NotPerfect[T <% Comparable[T]] 就可以正常运行
* 是因为Int本身不是Comparable的子类型 Scala通过"隐式转换"将Int转换成RichInt 而这个类型是Comparable的子类
*/
var pairInt = new Pair_NotPerfect(3, 5) //Int -> RichInt
println(pairInt.bigger) //5

/**
* 注意:这样定义不是因为String的上界是Ordered[String]
* 当使用视图界定时 会发生"隐式转换" 把String --> RichString
* 而RichString是Ordered[RichString]的子类型 RichString中是实现了这样的 < > <= >=等方法
* 从而真正是让String类型完成视图界定
*/
var pair_Batter_String = new Pair_Batter("Java", "Scala")
println(pair_Batter_String.bigger) //Scala

val pair_Batter_Int = new Pair_Batter(20, 12)
println(pair_Batter_Int.bigger) //20
}
}

3.4 上下文界定Context Bounds

  • 上下文界定[T : Ordering],这种写法在Spark中是广泛使用的,说明存在一个隐式的值Ordering[T]
    • implicit ordered: Ordering[T]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Pair_Ordering[T : Ordering] (val first: T, val second: T) {

//这是一个隐式转换的显式定义,这个函数没有参数,当时函数执行的时候 这个隐式值就会自动传进来
def bigger(implicit ordered: Ordering[T]) = {
if (ordered.compare(first, second) > 0) first else second
}
}


object Context_Bounds {
def main(args: Array[String]): Unit = {
val pair = new Pair_Ordering("Spark", "Hadoop")
println(pair.bigger) //Spark

val pairInt = new Pair_Ordering(3, 5)
println(pairInt.bigger) //5
}
}

3.5 ClassTag和Manifest

  • 上下文界定[T : ClassTag]:相当于动态类型,记录了当前T的类型,你使用时传入什么类型就是什么类型,在实际运行的时候我们获取T具体的类型
  • 主要是应用于创建泛型数组,因为数组必须有具体的类型,否则无法创建相应的数组,利用[T : ClassTag]就可以创建成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import scala.reflect.ClassTag

object Manifest_ClassTag {
def main(args: Array[String]): Unit = {

/**
* Q: 可以创建泛型数组吗?
*理论上是不可以的,因为没有指定具体的,在Scala程序运行中,数组必须有具体的类型,没有否无法创建的相应的数组
*引出Manifest的概念可以创建泛型数组
*[T : Manifest]这样的写法被称之为Manifest上下文界定 实质上这是需要一个Manifest[T]类型的隐式对象 这又是一个"隐式转换"的过程,有这样的一个隐式转换来辅助我们构建Manifest[T]来确定T的类型
* 通过这个隐式的值来辅助构建泛型数组,来确定T的具体类型
* 所以在创建泛型函数时 需要Manifest的类型来辅助构建泛型数组,借助Manifest类型对象来指定泛型数组具体的类型
*
* 通过Manifest[T]可以记录T的类型 在实际运行的时候我们获取T具体的类型
* */
def arrayMake[T : Manifest](first: T, second: T) = {
val r = new Array[T](2)
r(0) = first
r(1) = second
r
}
arrayMake(1, 2).foreach(println) //1 2

/**
* Manifest的原生写法 不推荐
*/
def manif[T](x: List[T])(implicit m: Manifest[T]) = {
if (m <:< manifest[String]) //<:< 表示 m是manifest[String]类型
println("List Strings")
else
println("Some other type")
}
manif(List("Spark", "Hadoop")) //List Strings
manif(List(1, 2)) //Some other type
manif(List("Scala", 3)) //Some other type


/**
* [T : ClassTag]这种写法说明:当这个函数在运行时时 对存在一个ClassTag[T]一个隐式值 这种方式是最常用的
主要是在运行时指定,在编译时无法确定的type的信息
编写编译的时候没有具体类型,运行的时候必须要有具体的类型,所以需要一种机制运行的时候会根据类型进行推断类型,classTag会帮我们存储这个类的信息,然后交给虚拟机
*/
def mkArray[T : ClassTag](elems: T*) = Array[T](elems: _*)
mkArray(42, 13).foreach(println) //42 13
mkArray("Japan", "Brazil", "Germany").foreach(println) //"Japan", "Brazil", "Germany"

}
}

3.6 Scala多重界定

  • T <: A with B:T是A或者B的子类
    • T >: A with B:A或者B是T的子类
    • T >: A <: B:T同时拥有下界A和上界B(也就是说A必为B的子类型,下界必须写在前面,上界必须写在后面)
  • T : A : B : 上下文界定
  • T <% A <% B:视图界定,T可以同时拥有多个视图界定,必须能够同时转化为A和B的要求

3.7 Scala类型约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* A =:= B 表示A类型等同于B类型
* A <:< B 表示A类型是B类型的子类型
*/
object Type_Contraints {
def main(args: Array[String]): Unit = {

//隐式参数是从哪里传进来的?后面有一些列的判断
def rocky[T](i: T)(implicit ev: T <:< java.io.Serializable) {
println(ev) //<function1>
println("Life is short, you need Spark!")
}
rocky("spark")
//rocky(100) error:Cannot prove that Int <:< java.io.Serializable.
}
}

3.8 总结

  • 边界(Bounds)
    • [T <: Comparable[T]]
    • <: :指明上界,表达了泛型的类型必须是”某种类型”或某种类型的”子类”
    • ‘>: :指明下界,表达了泛型的类型必须是”某种类型”或某种类型的”父类”
  • 视图界定 View Bounds
    • [T <% Comparable[T]]
    • <% : 对上边界的加强版,可以利用implicit隐式转换将实参类型转换成目标类型
  • 上下文界定Context Bounds
    • [T : Ordering]
    • 说明存在一个隐式的值Ordering[T](implicit ordered: Ordering[T])
    • 针对创建泛型数组的上下文界定:
      • [T : ClassTag]
      • [T : Manifest]

[1] 何苦要学Scala

[2] https://docs.scala-lang.org/zh-cn/tour/generic-classes.html