w3cschool-Scala 教程

https://www.w3cschool.cn/scala/

Scala 教程
关于
基础
基础知识(续)
Finagle 介绍
集合
Searchbird
模式匹配与函数组合
类型和多态基础
高级类型
简单构建工具
更多的集合
使用 specs 测试
Scala 并发编程
Java 与 Scala
Scala 相关教程
Scala 拓展教程

为什么选择 Scala?

  • 表达能力
    • 函数是一等公民
    • 闭包
  • 简洁
    • 类型推断
    • 函数创建的文法支持
  • Java互操作性
    • 可重用 Java 库
    • 可重用 Java 工具
    • 没有性能惩罚

Scala 如何工作?

  • 编译成 Java 字节码
  • 可在任何标准 JVM 上运行
    • 甚至是一些不规范的JVM上,如 Dalvik
    • Scala 编译器是 Java 编译器的作者写的

用 Scala 思考

Scala 不仅仅是更好的 Java。你应该用全新的头脑来学习它,你会从这些课程中认识到这一点的。

安装 Scala 请看:Scala 安装及环境配置

启动解释器

使用自带的 sbt console 启动。

$ sbt console[...]
Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_20).
Type in expressions to have them evaluated.Type :help for more information.

scala>

表达式

scala> 1 + 1
res0: Int = 2

res0 是解释器自动创建的变量名称,用来指代表达式的计算结果。它是 Int 类型,值为 2。

Scala 中(几乎)一切都是表达式。

你可以给一个表达式的结果起个名字赋成一个不变量(val)。

scala> val two = 1 + 1
two: Int = 2

你不能改变这个不变量的值。

变量

如果你需要修改这个名称和结果的绑定,可以选择使用 var

scala> var name = "steve"
name: java.lang.String = steve

scala> name = "marius"
name: java.lang.String = marius

函数

你可以使用 def 创建函数.

scala> def addOne(m: Int): Int = m + 1
addOne: (m: Int)Int

在 Scala 中,你需要为函数参数指定类型签名。

scala> val three = addOne(2)
three: Int = 3

如果函数不带参数,你可以不写括号。

scala> def three() = 1 + 2
three: ()Int

scala> three()
res2: Int = 3

scala> three
res3: Int = 3

匿名函数

你可以创建匿名函数。

scala> (x: Int) => x + 1
res2: (Int) => Int = <function1>

这个函数为名为 x 的 Int 变量加 1。

scala> res2(1)
res3: Int = 2

你可以传递匿名函数,或将其保存成不变量。

scala> val addOne = (x: Int) => x + 1
addOne: (Int) => Int = <function1>

scala> addOne(1)
res4: Int = 2

如果你的函数有很多表达式,可以使用 {} 来格式化代码,使之易读。

def timesTwo(i: Int): Int = { 
    println("hello world")  i * 2
}

对匿名函数也是这样的。

scala> { i: Int =>  
    println("hello world")  
    i * 2
}
res0: (Int) => Int = <function1>

在将一个匿名函数作为参数进行传递时,这个语法会经常被用到。

部分应用(Partial application)

你可以使用下划线“_”部分应用一个函数,结果将得到另一个函数。Scala 使用下划线表示不同上下文中的不同事物,你通常可以把它看作是一个没有命名的神奇通配符。在`{ + 2 }`的上下文中,它代表一个匿名参数。你可以这样使用它:

scala> def adder(m: Int, n: Int) = m + nadder: (m: Int,n: Int)Int

scala> val add2 = adder(2, _:Int)
add2: (Int) => Int = <function1>

scala> add2(3)
res50: Int = 5

你可以部分应用参数列表中的任意参数,而不仅仅是最后一个。

柯里化函数

有时会有这样的需求:允许别人一会在你的函数上应用一些参数,然后又应用另外的一些参数。

例如一个乘法函数,在一个场景需要选择乘数,而另一个场景需要选择被乘数。

scala> def multiply(m: Int)(n: Int): Int = m * nmultiply: (m: Int)(n: Int)Int

你可以直接传入两个参数。

scala> multiply(2)(3)
res0: Int = 6

你可以填上第一个参数并且部分应用第二个参数。

scala> val timesTwo = multiply(2) _
timesTwo: (Int) => Int = <function1>

scala> timesTwo(3)
res1: Int = 6

你可以对任何多参数函数执行柯里化。例如之前的 adder 函数

第一次传参是一个加数,返回一个函数,调用第二个函数传参,另一个数,得出结果是和

scala> (adder _).curried
res1: (Int) => (Int) => Int = <function1>

scala> res1(2)

res2: (Int) => Int = <function1>

scala> res2(3)

res3: Int = 5

可变长度参数

这是一个特殊的语法,可以向方法传入任意多个同类型的参数。例如要在多个字符串上执行 String 的 capitalize 函数,可以这样写:

def capitalizeAll(args: String*) = {
    args.map { arg =>
    arg.capitalize
  }
}

scala> capitalizeAll("rarity", "applejack")
res2: Seq[String] = ArrayBuffer(Rarity, Applejack)

scala> class Calculator {
     |   val brand: String = "HP"
         |   def add(m: Int, n: Int): Int = m + n
     | }
defined class Calculator

scala> val calc = new Calculator
calc: Calculator = Calculator@e75a11

scala> calc.add(1, 2)
res1: Int = 3

scala> calc.brand
res2: String = "HP"

上面的例子展示了如何在类中用 def 定义方法和用 val 定义字段值。方法就是可以访问类的状态的函数。

构造函数

构造函数不是特殊的方法,他们是除了类的方法定义之外的代码。让我们扩展计算器的例子,增加一个构造函数参数,并用它来初始化内部状态。

class Calculator(brand: String) {
  /**
   * A constructor.
  */
  val color: String = if (brand == "TI") {
    "blue"
  } else if (brand == "HP") {
    "black"
  } else {
    "white"
  }
  // An instance method.
  def add(m: Int, n: Int): Int = m + n
}

注意两种不同风格的评论。

你可以使用构造函数来构造一个实例:

scala> val calc = new Calculator("HP")
calc: Calculator = Calculator@1e64cc4d

scala> calc.color
res0: String = black

表达式

上文的 Calculator 例子说明了 Scala 是如何面向表达式的。颜色的值就是绑定在一个if/else表达式上的。Scala 是高度面向表达式的:大多数东西都是表达式而非指令。

旁白: 函数 vs 方法

函数和方法在很大程度上是可以互换的。由于函数和方法是如此的相似,你可能都不知道你调用的东西是一个函数还是一个方法。而当真正碰到的方法和函数之间的差异的时候,你可能会感到困惑。

scala> class C {
     |   var acc = 0
     |   def minc = { acc += 1 }
     |   val finc = { () => acc += 1 }
     | }
defined class C

scala> val c = new C
c: C = C@1af1bd6

scala> c.minc // calls c.minc()

scala> c.finc // returns the function as a value:
res2: () => Unit = <function0>

当你可以调用一个不带括号的“函数”,但是对另一个却必须加上括号的时候,你可能会想哎呀,我还以为自己知道 Scala 是怎么工作的呢。也许他们有时需要括号?你可能以为自己用的是函数,但实际使用的是方法。

在实践中,即使不理解方法和函数上的区别,你也可以用 Scala 做伟大的事情。如果你是 Scala 新手,而且在读两者的差异解释,你可能会跟不上。不过这并不意味着你在使用 Scala 上有麻烦。它只是意味着函数和方法之间的差异是很微妙的,只有深入语言内部才能清楚理解它。

继承

class ScientificCalculator(brand: String) extends Calculator(brand) {
  def log(m: Double, base: Double) = math.log(m) / math.log(base)
}

参考 Effective Scala 指出如果子类与父类实际上没有区别,类型别名是优于继承的。A Tour of Scala 详细介绍了子类化

重载方法

class EvenMoreScientificCalculator(brand: String) extends ScientificCalculator(brand) {
  def log(m: Int): Double = log(m, math.exp(1))
}

抽象类

你可以定义一个抽象类,它定义了一些方法但没有实现它们。取而代之是由扩展抽象类的子类定义这些方法。你不能创建抽象类的实例。

scala> abstract class Shape {
     |   def getArea():Int    //子类应定义为这个
     | }
defined class Shape

scala> class Circle(r: Int) extends Shape {
     |   def getArea():Int = { r * r * 3 }
     | }
defined class Circle

scala> val s = new Shape
<console>:8: error: class Shape is abstract; cannot be instantiated
       val s = new Shape
               ^
scala> val c = new Circle(2)
c: Circle = Circle@65c0035b

特质(Traits)

特质是一些字段和行为的集合,可以扩展或混入(mixin)你的类中。

trait Car {
  val brand: String
}

trait Shiny {
  val shineRefraction: Int
}

class BMW extends Car {
  val brand = "BMW"
}

通过 with 关键字,一个类可以扩展多个特质:

class BMW extends Car with Shiny {
  val brand = "BMW"
  val shineRefraction = 12
}

参考 Effective Scala 对特质的观点

什么时候应该使用特质而不是抽象类? 如果你想定义一个类似接口的类型,你可能会在特质和抽象类之间难以取舍。这两种形式都可以让你定义一个类型的一些行为,并要求继承者定义一些其他行为。一些经验法则:

  • 优先使用特质。一个类扩展多个特质是很方便的,但却只能扩展一个抽象类。
  • 如果你需要构造函数参数,使用抽象类。因为抽象类可以定义带参数的构造函数,而特质不行。例如,你不能说trait t(i: Int) {},参数i是非法的。

类型

此前,我们定义了一个函数的参数为 Int,表示输入是一个数字类型。其实函数也可以是泛型的,来适用于所有类型。当这种情况发生时,你会看到用方括号语法引入的类型参数。下面的例子展示了一个使用泛型键和值的缓存。

trait Cache[K, V] {
  def get(key: K): V
  def put(key: K, value: V)
  def delete(key: K)
}

方法也可以引入类型参数。

def remove[K](key: K)

集合

基本数据结构

Scala 提供了一些不错的集合。

参考 Effective Scala 对怎样使用集合的观点

列表 List

scala> val numbers = List(1, 2, 3, 4)
numbers: List[Int] = List(1, 2, 3, 4)

集 Set

集没有重复

scala> Set(1, 1, 2)
res0: scala.collection.immutable.Set[Int] = Set(1, 2)

元组 Tuple

元组是在不使用类的前提下,将元素组合起来形成简单的逻辑集合。

scala> val hostPort = ("localhost", 80)
hostPort: (String, Int) = (localhost, 80)

与样本类不同,元组不能通过名称获取字段,而是使用位置下标来读取对象;而且这个下标基于 1,而不是基于 0。

scala> hostPort._1
res0: String = localhost

scala> hostPort._2
res1: Int = 80

元组可以很好得与模式匹配相结合。

hostPort match {
  case ("localhost", port) => ...
  case (host, port) => ...
}

在创建两个元素的元组时,可以使用特殊语法:->

scala> 1 -> 2
res0: (Int, Int) = (1,2)

参考 Effective Scala 对 [解构绑定](http://twitter.github.com/effectivescala/#Functional programming-Destructuring bindings) (“拆解”一个元组)的观点。

映射 Map

它可以持有基本数据类型。

Map(1 -> 2)
Map("foo" -> "bar")

这看起来像是特殊的语法,不过不要忘了上文讨论的->可以用来创建二元组。

Map()方法也使用了从第一节课学到的变参列表:Map(1 -> "one", 2 -> "two")将变为 Map((1, "one"), (2, "two")),其中第一个参数是映射的键,第二个参数是映射的值。

映射的值可以是映射甚或是函数。

Map(1 -> Map("foo" -> "bar"))
Map("timesTwo" -> { timesTwo(_) })

选项 Option

Option 是一个表示有可能包含值的容器。

Option基本的接口是这样的:

trait Option[T] {
  def isDefined: Boolean
  def get: T
  def getOrElse(t: T): T
}

Option 本身是泛型的,并且有两个子类: Some[T] 或 None

我们看一个使用 Option 的例子:

Map.get 使用 Option 作为其返回值,表示这个方法也许不会返回你请求的值。

scala> val numbers = Map("one" -> 1, "two" -> 2)
numbers: scala.collection.immutable.Map[java.lang.String,Int] = Map(one -> 1, two -> 2)

scala> numbers.get("two")
res0: Option[Int] = Some(2)

scala> numbers.get("three")
res1: Option[Int] = None

现在我们的数据似乎陷在 Option 中了,我们怎样获取这个数据呢?

直觉上想到的可能是在 isDefined 方法上使用条件判断来处理。

// We want to multiply the number by two, otherwise return 0.
val result = if (res1.isDefined) {
  res1.get * 2
} else {
  0
}

我们建议使用 getOrElse 或模式匹配处理这个结果。

getOrElse 让你轻松地定义一个默认值。

val result = res1.getOrElse(0) * 2

模式匹配能自然地配合 Option 使用。

val result = res1 match {
  case Some(n) => n * 2
  case None => 0
}

参考 Effective Scala 对使用 [Options](http://twitter.github.com/effectivescala/#Functional programming-Options) 的意见。

函数组合子

List(1, 2, 3) map squared 对列表中的每一个元素都应用了squared 平方函数,并返回一个新的列表 List(1, 4, 9)。我们称这个操作 map 组合子。 (如果想要更好的定义,你可能会喜欢 Stackoverflow 上对组合子的说明。)他们常被用在标准的数据结构上。

map

map 对列表中的每个元素应用一个函数,返回应用后的元素所组成的列表。

scala> numbers.map((i: Int) => i * 2)
res0: List[Int] = List(2, 4, 6, 8)

或传入一个部分应用函数

scala> def timesTwo(i: Int): Int = i * 2
timesTwo: (i: Int)Int

scala> numbers.map(timesTwo _)
res0: List[Int] = List(2, 4, 6, 8)

foreach

foreach 很像 map,但没有返回值。foreach 仅用于有副作用[side-effects]的函数。

scala> numbers.foreach((i: Int) => i * 2)

什么也没有返回。

你可以尝试存储返回值,但它会是 Unit 类型(即void)

scala> val doubled = numbers.foreach((i: Int) => i * 2)
doubled: Unit = ()

filter

filter 移除任何对传入函数计算结果为 false 的元素。返回一个布尔值的函数通常被称为谓词函数[或判定函数]

scala> numbers.filter((i: Int) => i % 2 == 0)
res0: List[Int] = List(2, 4)
scala> def isEven(i: Int): Boolean = i % 2 == 0
isEven: (i: Int)Boolean

scala> numbers.filter(isEven _)
res2: List[Int] = List(2, 4)

zip

zip 将两个列表的内容聚合到一个对偶列表中。

scala> List(1, 2, 3).zip(List("a", "b", "c"))
res0: List[(Int, String)] = List((1,a), (2,b), (3,c))

partition

partition 将使用给定的谓词函数分割列表。

scala> val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> numbers.partition(_ % 2 == 0)
res0: (List[Int], List[Int]) = (List(2, 4, 6, 8, 10),List(1, 3, 5, 7, 9))

find

find 返回集合中第一个匹配谓词函数的元素。

scala> numbers.find((i: Int) => i > 5)
res0: Option[Int] = Some(6)

drop & dropWhile

drop 将删除前 i 个元素

scala> numbers.drop(5)
res0: List[Int] = List(6, 7, 8, 9, 10)

dropWhile 将删除元素直到找到第一个匹配谓词函数的元素。例如,如果我们在 numbers 列表上使用 dropWhile 奇数的函数, 1 将被丢弃(但 3 不会被丢弃,因为他被 2 “保护”了)。

scala> numbers.dropWhile(_ % 2 != 0)
res0: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

foldLeft

scala> numbers.foldLeft(0)((m: Int, n: Int) => m + n)
res0: Int = 55

0 为初始值(记住 numbers 是 List[Int] 类型),m 作为一个累加器。

直接观察运行过程:

scala> numbers.foldLeft(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }
m: 0 n: 1
m: 1 n: 2
m: 3 n: 3
m: 6 n: 4
m: 10 n: 5
m: 15 n: 6
m: 21 n: 7
m: 28 n: 8
m: 36 n: 9
m: 45 n: 10
res0: Int = 55

foldRight

和 foldLeft 一样,只是运行过程相反。

scala> numbers.foldRight(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }
m: 10 n: 0
m: 9 n: 10
m: 8 n: 19
m: 7 n: 27
m: 6 n: 34
m: 5 n: 40
m: 4 n: 45
m: 3 n: 49
m: 2 n: 52
m: 1 n: 54
res0: Int = 55

flatten

flatten 将嵌套结构扁平化为一个层次的集合。

scala> List(List(1, 2), List(3, 4)).flatten
res0: List[Int] = List(1, 2, 3, 4)

flatMap

flatMap 是一种常用的组合子,结合映射 [mapping] 和扁平化 [flattening]。flatMap 需要一个处理嵌套列表的函数,然后将结果串连起来。

scala> val nestedNumbers = List(List(1, 2), List(3, 4))
nestedNumbers: List[List[Int]] = List(List(1, 2), List(3, 4))

scala> nestedNumbers.flatMap(x => x.map(_ * 2))
res0: List[Int] = List(2, 4, 6, 8)

可以把它看做是“先映射后扁平化”的快捷操作:

scala> nestedNumbers.map((x: List[Int]) => x.map(_ * 2)).flatten
res1: List[Int] = List(2, 4, 6, 8)

这个例子先调用 map,然后可以马上调用 flatten,这就是“组合子”的特征,也是这些函数的本质。

参考 Effective Scala 对 [flatMap](http://twitter.github.com/effectivescala/#Functional programming-flatMap) 的意见。

扩展函数组合子

现在我们已经学过集合上的一些函数。

我们将尝试写自己的函数组合子。

有趣的是,上面所展示的每一个函数组合子都可以用 fold 方法实现。让我们看一些例子。

def ourMap(numbers: List[Int], fn: Int => Int): List[Int] = {
  numbers.foldRight(List[Int]()) { (x: Int, xs: List[Int]) =>
    fn(x) :: xs
  }
}

scala> ourMap(numbers, timesTwo(_))
res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

为什么是List[Int]()?Scala没有聪明到理解你的目的是将结果积聚在一个空的 Int 类型的列表中。

Map?

所有展示的函数组合子都可以在 Map 上使用。Map 可以被看作是一个二元组的列表,所以你写的函数要处理一个键和值的二元组。

scala> val extensions = Map("steve" -> 100, "bob" -> 101, "joe" -> 201)
extensions: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101), (joe,201))

现在筛选出电话分机号码低于 200 的条目。

scala> extensions.filter((namePhone: (String, Int)) => namePhone._2 < 200)
res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101))

因为参数是元组,所以你必须使用位置获取器来读取它们的键和值。

幸运的是,我们其实可以使用模式匹配更优雅地提取键和值。

scala> extensions.filter({case (name, extension) => extension < 200})
res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101))

模式匹配与函数组合

函数组合

让我们创建两个函数:

scala> def f(s: String) = "f(" + s + ")"
f: (String)java.lang.String

scala> def g(s: String) = "g(" + s + ")"
g: (String)java.lang.String

compose

compose 组合其他函数形成一个新的函数 f(g(x))

scala> val fComposeG = f _ compose g _
fComposeG: (String) => java.lang.String = <function>

scala> fComposeG("yay")
res0: java.lang.String = f(g(yay))

andThen

andThen 和 compose很像,但是调用顺序是先调用第一个函数,然后调用第二个,即g(f(x))

scala> val fAndThenG = f _ andThen g _
fAndThenG: (String) => java.lang.String = <function>

scala> fAndThenG("yay")
res1: java.lang.String = g(f(yay))

柯里化 vs 偏应用

case 语句

那么究竟什么是 case 语句?

这是一个名为 PartialFunction 的函数的子类。

多个 case 语句的集合是什么?

他们是共同组合在一起的多个 PartialFunction。

理解 PartialFunction(偏函数)

对给定的输入参数类型,函数可接受该类型的任何值。换句话说,一个(Int) => String的函数可以接收任意 Int 值,并返回一个字符串。

对给定的输入参数类型,偏函数只能接受该类型的某些特定的值。一个定义为(Int) => String 的偏函数可能不能接受所有 Int 值为输入。

isDefinedAt 是 PartialFunction 的一个方法,用来确定 PartialFunction 是否能接受一个给定的参数。

注意:偏函数 PartialFunction 和我们前面提到的部分应用函数是无关的。

参考 Effective Scala 对 [PartialFunction](http://twitter.github.com/effectivescala/#Functional programming-Partial functions) 的意见。

scala> val one: PartialFunction[Int, String] = { case 1 => "one" }
one: PartialFunction[Int,String] = <function1>

scala> one.isDefinedAt(1)
res0: Boolean = true

scala> one.isDefinedAt(2)
res1: Boolean = false

您可以调用一个偏函数。

scala> one(1)
res2: String = one

PartialFunctions 可以使用 orElse 组成新的函数,得到的 PartialFunction 反映了是否对给定参数进行了定义。

scala> val two: PartialFunction[Int, String] = { case 2 => "two" }
two: PartialFunction[Int,String] = <function1>

scala> val three: PartialFunction[Int, String] = { case 3 => "three" }
three: PartialFunction[Int,String] = <function1>

scala> val wildcard: PartialFunction[Int, String] = { case _ => "something else" }
wildcard: PartialFunction[Int,String] = <function1>

scala> val partial = one orElse two orElse three orElse wildcard
partial: PartialFunction[Int,String] = <function1>

scala> partial(5)
res24: String = something else

scala> partial(3)
res25: String = three

scala> partial(2)
res26: String = two

scala> partial(1)
res27: String = one

scala> partial(0)
res28: String = something else

case 之谜

上周我们看到一些新奇的东西。我们在通常应该使用函数的地方看到了一个 case 语句。

scala> case class PhoneExt(name: String, ext: Int)
defined class PhoneExt

scala> val extensions = List(PhoneExt("steve", 100), PhoneExt("robey", 200))
extensions: List[PhoneExt] = List(PhoneExt(steve,100), PhoneExt(robey,200))

scala> extensions.filter { case PhoneExt(name, extension) => extension < 200 }
res0: List[PhoneExt] = List(PhoneExt(steve,100))

为什么这段代码可以工作?

filter 使用一个函数。在这个例子中是一个谓词函数(PhoneExt) => Boolean

类型和多态基础

什么是静态类型?

按 Pierce 的话讲:“类型系统是一个语法方法,它们根据程序计算的值的种类对程序短语进行分类,通过分类结果错误行为进行自动检查。”

类型允许你表示函数的定义域和值域。例如,从数学角度看这个定义:

f: R -> N

它告诉我们函数“f”是从实数集到自然数集的映射。

抽象地说,这就是具体类型的准确定义。类型系统给我们提供了一些更强大的方式来表达这些集合。

鉴于这些注释,编译器可以静态地 (在编译时)验证程序是合理的。也就是说,如果值(在运行时)不符合程序规定的约束,编译将失败。

一般说来,类型检查只能保证不合理的程序不能编译通过。它不能保证每一个合理的程序都可以编译通过。

随着类型系统表达能力的提高,我们可以生产更可靠的代码,因为它能够在我们运行程序之前验证程序的不变性(当然是发现类型本身的模型 bug!)。学术界一直很努力地提高类型系统的表现力,包括值依赖(value-dependent)类型!

需要注意的是,所有的类型信息会在编译时被删去,因为它已不再需要。这就是所谓的擦除。

Scala 中的类型

Scala 强大的类型系统拥有非常丰富的表现力。其主要特性有:

  • 参数化多态性 粗略地说,就是泛型编程
  • (局部)类型推断 粗略地说,就是为什么你不需要这样写代码 val i: Int = 12: Int
  • 存在量化 粗略地说,为一些没有名称的类型进行定义
  • 视窗 我们将下周学习这些;粗略地说,就是将一种类型的值“强制转换”为另一种类型

参数化多态性

多态性是在不影响静态类型丰富性的前提下,用来(给不同类型的值)编写通用代码的。

例如,如果没有参数化多态性,一个通用的列表数据结构总是看起来像这样(事实上,它看起来很像使用泛型前的Java):

scala> 2 :: 1 :: "bar" :: "foo" :: Nil
res5: List[Any] = List(2, 1, bar, foo)

现在我们无法恢复其中成员的任何类型信息。

scala> res5.head
res6: Any = 2

所以我们的应用程序将会退化为一系列类型转换(“asInstanceOf[]”),并且会缺乏类型安全的保障(因为这些都是动态的)。

多态性是通过指定 类型变量 实现的。

scala> def drop1[A](l: List[A]) = l.tail
drop1: [A](l: List[A])List[A]

scala> drop1(List(1,2,3))
res1: List[Int] = List(2, 3)

Scala 有秩 1 多态性

粗略地说,这意味着在 Scala 中,有一些你想表达的类型概念“过于泛化”以至于编译器无法理解。假设你有一个函数

def toList[A](a: A) = List(a)

你希望继续泛型地使用它:

def foo[A, B](f: A => List[A], b: B) = f(b)

这段代码不能编译,因为所有的类型变量只有在调用上下文中才被固定。即使你“钉住”了类型 B:

def foo[A](f: A => List[A], i: Int) = f(i)

…你也会得到一个类型不匹配的错误。

类型推断

静态类型的一个传统反对意见是,它有大量的语法开销。Scala 通过 类型推断 来缓解这个问题。

在函数式编程语言中,类型推断的经典方法是 Hindley Milner 算法,它最早是实现在 ML 中的。

Scala 类型推断系统的实现稍有不同,但本质类似:推断约束,并试图统一类型。

例如,在 Scala 中你无法这样做:

scala> { x => x }
<console>:7: error: missing parameter type
       { x => x }

而在 OCaml 中你可以:

# fun x -> x;;
- : 'a -> 'a = <fun>

在 Scala 中所有类型推断是 局部的 。Scala 一次分析一个表达式。例如:

scala> def id[T](x: T) = x
id: [T](x: T)T

scala> val x = id(322)
x: Int = 322

scala> val x = id("hey")
x: java.lang.String = hey

scala> val x = id(Array(1,2,3,4))
x: Array[Int] = Array(1, 2, 3, 4)

类型信息都保存完好,Scala 编译器为我们进行了类型推断。请注意我们并不需要明确指定返回类型。

变性 Variance

Scala 的类型系统必须同时解释类层次和多态性。类层次结构可以表达子类关系。在混合 OO 和多态性时,一个核心问题是:如果 T’ 是 T 一个子类,Container[T’]应该被看做是 Container[T] 的子类吗?变性(Variance)注解允许你表达类层次结构和多态类型之间的关系:

名称含义Scala 标记
协变covariant C[T’]是 C[T] 的子类 [+T]
逆变contravariant C[T] 是 C[T’]的子类 [-T]
不变invariant C[T] 和 C[T’]无关 [T]

子类型关系的真正含义:对一个给定的类型T,如果T’是其子类型,你能替换它吗?

scala> class Covariant[+A]
defined class Covariant

scala> val cv: Covariant[AnyRef] = new Covariant[String]
cv: Covariant[AnyRef] = Covariant@4035acf6

scala> val cv: Covariant[String] = new Covariant[AnyRef]
<console>:6: error: type mismatch;
 found   : Covariant[AnyRef]
 required: Covariant[String]
       val cv: Covariant[String] = new Covariant[AnyRef]
                                   ^
scala> class Contravariant[-A]
defined class Contravariant

scala> val cv: Contravariant[String] = new Contravariant[AnyRef]
cv: Contravariant[AnyRef] = Contravariant@49fa7ba

scala> val fail: Contravariant[AnyRef] = new Contravariant[String]
<console>:6: error: type mismatch;
 found   : Contravariant[String]
 required: Contravariant[AnyRef]
       val fail: Contravariant[AnyRef] = new Contravariant[String]
                                     ^

逆变似乎很奇怪。什么时候才会用到它呢?令人惊讶的是,函数特质的定义就使用了它!

trait Function1 [-T1, +R] extends AnyRef

如果你仔细从替换的角度思考一下,会发现它是非常合理的。让我们先定义一个简单的类层次结构:

scala> class Animal { val sound = "rustle" }
defined class Animal

scala> class Bird extends Animal { override val sound = "call" }
defined class Bird

scala> class Chicken extends Bird { override val sound = "cluck" }
defined class Chicken

假设你需要一个以 Bird 为参数的函数:

scala> val getTweet: (Bird => String) = // TODO

标准动物库有一个函数满足了你的需求,但它的参数是 Animal。在大多数情况下,如果你说“我需要一个___,我有一个___的子类”是可以的。但是,在函数参数这里是逆变的。如果你需要一个接受参数类型 Bird 的函数变量,但却将这个变量指向了接受参数类型为 Chicken 的函数,那么给它传入一个 Duck 时就会出错。然而,如果将该变量指向一个接受参数类型为 Animal 的函数就不会有这种问题:

scala> val getTweet: (Bird => String) = ((a: Animal) => a.sound )
getTweet: Bird => String = <function1>

函数的返回值类型是协变的。如果你需要一个返回 Bird 的函数,但指向的函数返回类型是 Chicken,这当然是可以的。

scala> val hatch: (() => Bird) = (() => new Chicken )
hatch: () => Bird = <function0>

边界

Scala 允许你通过边界来限制多态变量。这些边界表达了子类型关系。

scala> def cacophony[T](things: Seq[T]) = things map (_.sound)
<console>:7: error: value sound is not a member of type parameter T
       def cacophony[T](things: Seq[T]) = things map (_.sound)
                                                        ^

scala> def biophony[T <: Animal](things: Seq[T]) = things map (_.sound)
biophony: [T <: Animal](things: Seq[T])Seq[java.lang.String]

scala> biophony(Seq(new Chicken, new Bird))
res5: Seq[java.lang.String] = List(cluck, call)

类型下界也是支持的,这让逆变和巧妙协变的引入得心应手。List[+T] 是协变的;一个 Bird 的列表也是 Animal 的列表。List 定义一个操作::(elem T)返回一个加入了 elem 的新的 List。新的 List 和原来的列表具有相同的类型:

scala> val flock = List(new Bird, new Bird)
flock: List[Bird] = List(Bird@7e1ec70e, Bird@169ea8d2)

scala> new Chicken :: flock
res53: List[Bird] = List(Chicken@56fbda05, Bird@7e1ec70e, Bird@169ea8d2)

List 同样定义了::[B >: T](x: B) 来返回一个List[B]。请注意B >: T,这指明了类型B为类型T的超类。这个方法让我们能够做正确地处理在一个List[Bird]前面加一个 Animal 的操作:

scala> new Animal :: flock
res59: List[Animal] = List(Animal@11f8d3a8, Bird@7e1ec70e, Bird@169ea8d2)

注意返回类型是 Animal。

量化

有时候,你并不关心是否能够命名一个类型变量,例如:

scala> def count[A](l: List[A]) = l.size
count: [A](List[A])Int

这时你可以使用“通配符”取而代之:

scala> def count(l: List[_]) = l.size
count: (List[_])Int

这相当于是下面代码的简写:

scala> def count(l: List[T forSome { type T }]) = l.size
count: (List[T forSome { type T }])Int

注意量化会的结果会变得非常难以理解:

scala> def drop1(l: List[_]) = l.tail
drop1: (List[_])List[Any]

突然,我们失去了类型信息!让我们细化代码看看发生了什么:

scala> def drop1(l: List[T forSome { type T }]) = l.tail
drop1: (List[T forSome { type T }])List[T forSome { type T }]

我们不能使用 T 因为类型不允许这样做。

你也可以为通配符类型变量应用边界:

scala> def hashcodes(l: Seq[_ <: AnyRef]) = l map (_.hashCode)
hashcodes: (Seq[_ <: AnyRef])Seq[Int]

scala> hashcodes(Seq(1,2,3))
<console>:7: error: type mismatch;
 found   : Int(1)
 required: AnyRef
Note: primitive types are not implicitly converted to AnyRef.
You can safely force boxing by casting x.asInstanceOf[AnyRef].
       hashcodes(Seq(1,2,3))
                     ^

scala> hashcodes(Seq("one", "two", "three"))
res1: Seq[Int] = List(110182, 115276, 110339486)

 

posted @ 2022-03-10 22:12  hanease  阅读(48)  评论(0编辑  收藏  举报