1.3 准备工作

本书假设读者对串行编程有基本的了解。虽然建议读者至少熟悉一些 Scala 语言的知识,但对于本书来说,理解类似的语言(比如 Java 或 C#)也足够了。如果对面向对象编程中的概念有基本的了解,比如类、对象、接口(interface)等,则阅读本书也会更容易一些。同样,对函数式编程原则有基本理解,比如头等函数、纯洁性和类型多态性等,对阅读本书也有帮助,但这不是必需的。

1.3.1 执行一个Scala程序

为了更好地理解 Scala 程序的执行模型,先考虑使用一个简单的程序。在此程序中用square方法来计算数字5的平方,然后将结果输出到标准输出上。

    object SquareOf5 extends App {
      def square(x: Int): Int = x * x
      val s = square(5)
      println(s"Result: $s")
    }

使用简单构建工具(Simple Build Tool,SBT)运行这个程序时,JVM运行时会分配程序所需的内存。这里考虑两种重要的内存区域:调用栈和堆对象。调用栈存储了程序的局部变量信息和当前方法的参数信息。堆对象中保存了程序分配的对象。为理解这两个区域的区别,考虑上述程序的执行过程,如图1.1所示。

图1.1

如图1.1中第1步所示,程序在调用栈中为局部变量s分配一个条目,在第2步中调用square方法计算局部变量s的值。程序将值5放在调用栈上,作为x参数的值。程序还保留了调用栈中的一个条目,用于存放方法的返回值。到这里为止,程序可以开始执行square方法了,让x参数与它自己相乘,并将返回值25放在调用栈中,如图1.1第3步所示。

square方法返回之后,结果25被复制到局部变量s所在调用栈的位置上,如图1.1第4步所示。现在,程序必须为println语句创建一个字符串。在Scala中,字符串为String类的对象实例,程序会在堆对象上分配一个新的String对象,如图1.1第5步所示。如图1.1第6步所示,程序将对分配对象的引用保存在调用栈中的x位置上,然后调用println方法。

虽然这个过程被严重简化了,但它展示了Scala程序的基本执行模型。在第2章中,我们将了解到,每个执行的线程都会维护自己独立的调用栈,并且线程之间主要通过修改堆对象进行通信,而堆对象和局部调用栈之间的不一致造成了并发程序中的大部分错误。

了解了Scala程序的典型执行过程,现在就可以看一看Scala有哪些语言特性了。本章只介绍理解本书所必需的那些内容。

1.3.2 初识Scala

本节简单描述和本书示例相关的 Scala 语言特性,以快速、粗略的介绍为主,并不是Scala的完整指南。

通过本节,读者可以回想起一些 Scala 语言特性,并能将其和自己熟悉的语言进行比较。如果想更深入地了解Scala,请参考本章小结中提到的参考书目。

下面的示例代码定义了 Printer 类,它有一个 greeting 参数和两个方法:printMessage和printNumber。

    class Printer(val greeting: String) {
      def printMessage(): Unit = println(greeting + "!")
      def printNumber(x: Int): Unit = {
        println("Number: " + x)
      }
    }

上述代码中,printMessage 方法没有参数,只有一个 println 语句。printNumber有一个Int类型的参数x。两个方法都没有返回值,因此标识为Unit类型。下面的代码将此类实例化,并调用其方法。

    val printy = new Printer("Hi")
    printy.printMessage()
    printy.printNumber(5)

Scala 支持单例对象的声明。这就像声明一个类,然后将其实例化。之前介绍过的SquareOf5就是一个单例对象,它适用于声明一个简单的Scala程序。下面的单例对象Test声明了字段Pi,并将其初始化为3.14。

    object Test {
      val Pi = 3.14
    }

在其他类似语言中,供类扩展的实体称为接口,Scala 中相似的概念则称为特质(trait)。Scala 的类可以扩展特质,而且 Scala 特质还支持具体的字段和方法实现。在下面的示例中,定义了Logging特质,它通过抽象的log方法输出自定义错误和警告信息,然后将此特质加入PrintLogging类中。

    trait Logging {
      def log(s: String): Unit
      def warn(s: String) = log("WARN: " + s)
      def error(s: String) = log("ERROR: " + s)
    }
    class PrintLogging extends Logging {
      def log(s: String) = println(s)
    }

类定义中可以有类型参数(type parameter)。下面的泛型Pair类有两个类型参数P和Q,其决定了两个参数的类型。

    class Pair[P, Q](val first: P, val second: Q)

Scala支持头等函数对象,其也称为匿名函数。在下列代码中,声明了一个匿名函数twice,它用于将参数乘以2。

    val twice: Int => Int = (x: Int) => x * 2

在上述代码中,(x: Int)为匿名函数的参数部分,而x * 2则是函数体。=>符号必须位于匿名函数的参数和函数体之间。=>符号还用于表示匿名函数的类型,这里是Int => Int,可念成“从Int到Int”。在前面的示例中,函数类型标记Int => Int是可以省略的,因为编译器可以自动推理twice函数的类型,如下所示。

    val twice = (x: Int) => x * 2

在一种更简洁的语法中,可以忽略匿名函数声明中的参数类型标记,如下所示。

    val twice: Int => Int = x => x * 2

如果匿名函数的参数只在函数体中出现一次,甚至还可以表示得更简单一些,如下所示。

    val twice: Int => Int = _ * 2

Scala对头等函数的支持表现在可以将代码块作为参数传给函数,从而得到一种更轻量级的简洁语法。在下面的示例中,使用传名参数(byname parameter)声明了runTwice方法,此方法将代码块执行两次。

    def runTwice(body: =>Unit) = {
      body
      body
    }

在传名参数的声明中,=>符号被置于类型之前。RunTwice方法每引用一次body参数,这个代码块中的语句就会被重新执行,如下所示。

    runTwice { // 将Hello输出两次
      println("Hello")
    }

Scala的for表达式可以对容器进行遍历和变换。下面的for循环输出0~10的数字(不包含10)。

    for (i <- 0 until 10) println(i)

上述代码中,区间由表达式0 until 10创建,它等价于0.until(10),即调用值0的until方法。在Scala中,当调用对象的方法时,句点符号可以忽略。每个for循环都等价于一个foreach语句。上述for循环会被Scala编译器编译成下链表达式。

    (0 until 10).foreach(i => println(i))

Scala的for推导式(comprehension)语句可实现数据的变换。下面的for推导式将0~10的数字都乘以-1。

    val negatives = for (i <- 0 until 10) yield -i

negatives中的值为−10~0的负数。这个for推导式等价于下列map调用。

    val negatives = (0 until 10).map(i => -1 * i)

for推导式还支持多个输入数据的变换。下面的for推导式语句创建0~4的整数的所有二元组。

    val pairs = for (x <- 0 until 4; y <- 0 until 4) yield (x, y)

上述for推导式等价于下列表达式。

    val pairs = (0 until 4).flatMap(x => (0 until 4).map(y => (x, y)))

for 推导式中支持嵌入任意多个生成器表达式。Scala编译器会将它们翻译成多个嵌套flatMap,然后在最里层调用map。

常用的Scala容器包括序列(sequence),记为Seq[T]类型;映射(map),记为Map[K, V]类型;集合(set),记为Set[T]类型。在下面的示例中,创建了字符串的一个序列。

    val messages: Seq[String] = Seq("Hello", "World.", "!")

本书使用了大量的字符串模板(string interpolation)功能。一般来说,Scala字符串使用双引号。而字符串模板前面则多了一个 s 字符,字符串中间可以用$符号引用任何当前作用域中的标识符,如下所示。

    val magic = 7
    val myMagicNumber = s"My magic number is $magic"

模式匹配是另一个重要的Scala 语言特性。对Java、C#或C 用户而言,理解 Scala的match语句的一种办法是将其类比于switch语句。match语句可以分解为任意多个子句,并支持用户在程序中简洁地表达不同的匹配情况。

在下面的示例中,声明了一个 Map 容器,名为 successors,它将整数映射到自己的直接后继。然后调用get方法来获得数字5的后继。get方法返回了一个对象,类型为Option[Int],表示结果要么属于Some类(表示5在此映射中存在),要么属于None类(表示5不是此映射的一个键)。Option对象上的模式匹配支持逐个情况的比对,如下所示。

    val successors = Map(1 -> 2, 2 -> 3, 3 -> 4)
    successors.get(5) match {
      case Some(n) => println(s"Successor is: $n")
      case None => println("Could not find successor.")
    }

在 Scala 中,大部分操作符可重载。操作符重载不同于重新声明一个方法。在下面的示例中,定义了一个Position类,它有一个+操作符。

    class Position(val x: Int, val y: Int) {
      def +(that: Position) = new Position(x + that.x, y + that.y)
    }

Scala还支持定义包对象(package object),用于存储一个包的最外层方法和值定义。在下面的示例中,声明了org.learningconcurrency中的一个包对象。其中实现了最外层的log方法,用于输出指定的字符串和当前线程名称。

    package org
    package object learningconcurrency {
      def log(msg: String): Unit =
        println(s"${Thread.currentThread.getName}: $msg")
    }

本书后面会一直使用这个log方法,它用于追踪并发程序的执行过程。

本节的 Scala 语言特性就介绍到这里了。如果想更深入地理解这门语言,建议参考Scala的串行编程入门图书。