Scala 泛型以及泛型约束
Scala 泛型以及泛型约束
1. 泛型类
泛型类指可以接受类型参数的类。泛型类在集合类中被广泛使用。
1.1 泛型类
- 在类声明时,定义一些泛型类型,然后在类的内部,就可以使用这些泛型类型
- 在需要对类中的某些成员,如字段或方法中的参数进行统一的类型限制时,可以使用泛型类,使得程序具有更好的健壮性和稳定性
- 在使用类的时候,将类型参数替换为实际的类型即可
- scala会自动推断泛型类型:给泛型类型的字段赋值时,scala会自动对类型进行推断
泛型函数:
- 与泛型类相似,在声明函数时指定泛型类型,然后在函数体内,多个变量或返回值,就可以使用泛型类型进行声明。
- 可以通过给使用了泛型类型的变量传递值,让scala自动推断泛型的实际类型,也可以在调用函数的时候,手动指定泛型的实际类型
1 | class Triple[X, Y, Z](val first: X, val second: Y, val thrid: Z) |
1.2 定义一个泛型类
泛型类使用方括号 [] 来接受类型参数。一个惯例是使用字母 A 作为参数标识符,当然你可以使用任何参数名称。
1 | class Stack[A] { |
上面的 Stack 类的实现中接受类型参数 A。 这表示其内部的列表,var elements: List[A] = Nil,只能够存储类型 A 的元素。方法 def push 只接受类型 A 的实例对象作为参数(注意:elements = x :: elements 将 elements 放到了一个将元素 x 添加到 elements 的头部而生成的新列表中)。
1.3 使用
要使用一个泛型类,将一个具体类型放到方括号中来代替 A。
1 | val stack = new Stack[Int] |
实例对象 stack 只能接受整型值。然而,如果类型参数有子类型,子类型可以被传入:
1 | class Fruit |
类 Apple 和类 Banana 都继承自类 Fruit,所以我们可以把实例对象 apple 和 banana 压入栈 Fruit 中。
注意:泛型类型的子类型是*不可传导*的。这表示如果我们有一个字母类型的栈 Stack[Char],那它不能被用作一个整型的栈 Stack[Int]。否则就是不安全的,因为它将使我们能够在字母型的栈中插入真正的整型值。结论就是,只有当类型 B = A 时, Stack[A] 是 Stack[B] 的子类型才成立。因为此处可能会有很大的限制,Scala 提供了一种 类型参数注释机制 用以控制泛型类型的子类型的行为。
2. 型变
型变是复杂类型的子类型关系与其组件类型的子类型关系的相关性。 Scala支持 泛型类 的类型参数的型变注释,允许它们是协变的,逆变的,或在没有使用注释的情况下是不变的。 在类型系统中使用型变允许我们在复杂类型之间建立直观的连接,而缺乏型变则会限制类抽象的重用性。
1 | class Foo[+A] // A covariant class |
2.1 协变(covariant class)
使用注释 +A,可以使一个泛型类的类型参数 A 成为协变。 对于某些类 class List[+A],使 A 成为协变意味着对于两种类型 A 和 B,如果 A 是 B 的子类型,那么 List[A] 就是 List[B] 的子类型。 这允许我们使用泛型来创建非常有用和直观的子类型关系。
考虑以下简单的类结构:
1 | abstract class 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 | object CovarianceTest extends App { |
2.1 逆变(contravariant class)
通过使用注释 -A,可以使一个泛型类的类型参数 A 成为逆变。 与协变类似,这会在类及其类型参数之间创建一个子类型关系,但其作用与协变完全相反。 也就是说,对于某个类 class Writer[-A] ,使 A 逆变意味着对于两种类型 A 和 B,如果 A 是 B 的子类型,那么 Writer[B] 是 Writer[A] 的子类型。
考虑在下例中使用上面定义的类 Cat,Dog 和 Animal :
1 | abstract class Printer[-A] { |
这里 Printer[A] 是一个简单的类,用来打印出某种类型的 A。 让我们定义一些特定的子类:
1 | class AnimalPrinter extends Printer[Animal] { |
如果 Printer[Cat] 知道如何在控制台打印出任意 Cat,并且 Printer[Animal] 知道如何在控制台打印出任意 Animal,那么 Printer[Animal] 也应该知道如何打印出 Cat 就是合理的。 反向关系不适用,因为 Printer[Cat] 并不知道如何在控制台打印出任意 Animal。 因此,如果我们愿意,我们应该能够用 Printer[Animal] 替换 Printer[Cat],而使 Printer[A] 逆变允许我们做到这一点。
1 | object ContravarianceTest extends App { |
这个程序的输出如下:
1 | The cat's name is: Boots |
2.3 不变(invariant class)
默认情况下,Scala中的泛型类是不变的。 这意味着它们既不是协变的也不是逆变的。 在下例中,类 Container 是不变的。 Container[Cat] 不是 Container[Animal],反之亦然。
1 | class Container[A](value: A) { |
可能看起来一个 Container[Cat] 自然也应该是一个 Container[Animal],但允许一个可变的泛型类成为协变并不安全。 在这个例子中,Container 是不变的非常重要。 假设 Container 实际上是协变的,下面的情况可能会发生:
1 | val catContainer: Container[Cat] = new Container(Cat("Felix")) |
幸运的是,编译器在此之前就会阻止我们。
2.4 其他例子
另一个可以帮助理解型变的例子是 Scala 标准库中的 trait Function1[-T, +R]。 Function1 表示具有一个参数的函数,其中第一个类型参数 T 表示参数类型,第二个类型参数 R 表示返回类型。 Function1 在其参数类型上是逆变的,并且在其返回类型上是协变的。 对于这个例子,我们将使用文字符号 A => B 来表示 Function1[A, B]。
假设前面使用过的类似 Cat,Dog,Animal 的继承关系,加上以下内容:
1 | abstract class SmallAnimal extends Animal |
假设我们正在处理接受动物类型的函数,并返回他们的食物类型。 如果我们想要一个 Cat => SmallAnimal(因为猫吃小动物),但是给它一个 Animal => Mouse,我们的程序仍然可以工作。 直观地看,一个 Animal => Mouse 的函数仍然会接受一个 Cat 作为参数,因为 Cat 即是一个 Animal,并且这个函数返回一个 Mouse,也是一个 SmallAnimal。 既然我们可以安全地,隐式地用前者代替后者,我们可以说 Animal => Mouse 是 Cat => SmallAnimal 的子类型。
2.5 与其他语言的比较
某些与 Scala 类似的语言以不同的方式支持型变。 例如,Scala 中的型变注释与 C# 中的非常相似,在定义类抽象时添加型变注释(声明点型变)。 但是在Java中,当类抽象被使用时(使用点型变),才会给出型变注释。
3. 类型边界(Bounds)
- <: :指明上界,表达了泛型的类型必须是”某种类型”或某种类型的”子类”
- ‘>: :指明下界,表达了泛型的类型必须是”某种类型”或某种类型的”父类”
1 | /** |
3.1 类型上界
在Scala中,类型参数和抽象类型都可以有一个类型边界约束。这种类型边界在限制类型变量实际取值的同时还能展露类型成员的更多信息。比如像T <: A这样声明的类型上界表示类型变量T应该是类型A的子类。下面的例子展示了类PetContainer的一个类型参数的类型上界。
1 | abstract class Animal { |
类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 | trait Node[+B] { |
该程序实现了一个单链表。 Nil 表示空元素(即空列表)。 class ListNode 是一个节点,它包含一个类型为 B (head) 的元素和一个对列表其余部分的引用 (tail)。 class Node 及其子类型是协变的,因为我们定义了 +B。
但是,这个程序 不能 编译,因为方法 prepend 中的参数 elem 是协变的 B 类型。 这会出错,因为函数的参数类型是逆变的,而返回类型是协变的。
要解决这个问题,我们需要将方法 prepend 的参数 elem 的型变翻转。 我们通过引入一个新的类型参数 U 来实现这一点,该参数具有 B 作为类型下界。
1 | trait Node[+B] { |
现在我们像下面这么做:
1 | trait Bird |
可以为 Node[Bird] 赋值 africanSwallowList,然后再加入一个 EuropeanSwallow。
3.3 视图界定 View Bounds
- view bounds其实就是bounds 上边界的加强版本,对bounds的补充 <变成<%
- 可以利用implicit隐式转换将实参类型转换成目标类型
1 | /** |
3.4 上下文界定Context Bounds
- 上下文界定[T : Ordering],这种写法在Spark中是广泛使用的,说明存在一个隐式的值Ordering[T]
- implicit ordered: Ordering[T]
1 | class Pair_Ordering[T : Ordering] (val first: T, val second: T) { |
3.5 ClassTag和Manifest
- 上下文界定[T : ClassTag]:相当于动态类型,记录了当前T的类型,你使用时传入什么类型就是什么类型,在实际运行的时候我们获取T具体的类型
- 主要是应用于创建泛型数组,因为数组必须有具体的类型,否则无法创建相应的数组,利用[T : ClassTag]就可以创建成功
1 | import scala.reflect.ClassTag |
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 | /** |
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