基础知识

一、Scala 特点

  1. 可以从https://docs.scala-lang.org/api/all.html 查询scala doc 文档。

  2. Scala 允许添加新的数据类型,这些新加的类型用起来和内建的类型一样。

    Scala 也允许添加新的控制结构,其用法也和内建的控制结构一样。

1.1 面向对象&函数式

  1. Scala 同时支持面向对象编程和函数式编程。

  2. Scala 中,一切value 都是对象,每个操作都是方法调用。如:+ 都是方法调用,1+2 实际上是调用了Int 类定义的、一个叫做+ 的方法。

    • Scala 不同,Java/C++ 中的基本数据类型都不是对象,在这些语言中允许不是对象的value 存在。
    • Scala 不同,Java/C++ 中允许不以任何对象的成员形式存在的静态字段和静态方法。

    另外,在Scala 中可以定义名字像操作符的方法,这样就可以用操作符表示法来调用。如:obj ! msg! 就是自定义的一个方法的名字。

    而且,Scala 中函数就是对象,函数的类型是一个可以被继承的类,函数本身就是这个类的对象。

  3. 函数式编程有两大核心理念:

    • 函数是一等公民,其地位与整数、字符串等相同。

      • 可以将函数作为参数传递给其它函数,也可以返回函数作为某个函数的返回值,也可以将函数保存在变量里。
      • 可以在函数中定义另一个函数,就像在函数中定义整数一样。
      • 可以在定义函数时不指定名字,这称作函数字面量,就像整数字面量10 一样。

      Scala 不同,Java/C/C++ 中的函数是二等公民。

    • 程序中的操作应该将输入值映射成输出值,而不是原地修改数据。即:方法调用不应该产生副作用,方法只能通过接收入参、返回结果这两种方式与外部环境通信。

  4. 不可变数据结构是函数式编程的基础之一,Scala 类库在 Java API 的基础上定义了更多的不可变数据类型。如:不可变列表、不可变元组、不可变set、不可变map

    函数式编程鼓励采用不可变数据结构和referential transparent 的方法。referential transparent 方法指的是:对任何给定的输入,该方法调用都可以直接被其结果替换,同时不会影响程序的语义。如:c=f(a),假设f(a) 的结果是100,则该语句用 c=100 替换不会对程序有任何影响。

1.2 Scala 优势

  1. ScalaJava 高度兼容。Scala 运行在标准的 Java 平台上,可以和所有 Java 类库无缝协作:

    • Scala 程序会被编译成JVM 字节码,因此Scala 程序运行期性能通常与Java 程序相差无几。

    • Scala 代码可以调用Java 方法、访问Java 字段、从Java 类继承、实现Java 接口。这并不需要任何特殊的语法或者额外的代码。

    • Scala 重度复用了Java 的数据类型。如:

      • ScalaInt 是用Java 的基本类型int 实现的。
      • ScalaFloat 是用Java 的基本类型float 实现的。
      • ScalaBoolean 是用Java 的基本类型boolean 实现的。
      • Scala 的数组被映射成Java 的数组。
      • Scala 的字符串字面量是一个java.lang.String
      • Scala 抛出的异常必须是java.lang.Throwable 的子类。

      基本上所有的Java 基本数据类型在Scala 包中都有对应的类,当Scala 代码编译成Java 字节码时,Scala 编译器会尽量使用Java 基本数据类型从而达到更优的性能。

    • 也可以在Java 中调用Scala 的代码。但是由于ScalaJava 表达能力更为丰富,因此Scala 某些高级特性需要经过加工之后才能映射到Java

  2. Scala 程序通常都很短。与Java 相比,Scala 代码行数可以相差一个量级。更少的代码不仅意味着更少的打字,也意味着更少的bug 和更容易阅读与理解。

    Scala 更少的代码归因于以下几点:

    • Scala 语法避免了Java 程序中的一些样板代码boilerplate 。如:Scala 中分号是可选的,通常大家也不写分号。
    • Scala 代码支持类型推断,因此冗余的类型信息可以去掉,这使得代码更为紧凑。
    • Scala 提供了大量工具来定义功能强大的类库,使得代码更加精炼。
  3. Scala 是静态类型的,它拥有非常先进的静态类型系统。

    • Scala 通过类型推断避免了代码中到处是类型声明,从而导致代码过于罗嗦的问题。
    • Scala 通过模式匹配、创建新类型、类型组合的方式灵活的使用类型。

    静态类型系统的优点:

    • 可以证明某些运行期错误不可能发生。

      • 虽然这种保障比较简单,但这是真正的保障,不是单元测试所能提供的。
      • 静态类型系统不能代替单元测试,但是它可以减少单元测试的数量。
    • 可以安全的对代码进行重构。

    • 静态类型系统是程序化的文档,编译器会检查其正确性。

1.3 语法风格

  1. Scala 的注释风格与Java 相同:行内注释 //,行间注释/*....*/

  2. Scala 推荐的代码缩进风格是:每个层级缩进2 个空格。

  3. Scala 的包和Java 的包很类似,它们都将全局命名空间划分为多个区域,提供了信息隐藏的机制。

  4. Scala 命令行参数可以通过args 数组获取,数组下标从0 开始。

    • 0 个参数是真正的命令行参数,而不是脚本文件名。
    • Scala 数组的索引是圆括号args(0),这与Java 不同(Javaargs[0] )。
  5. Scala 中,每条语句最后的分号通常是可选的:如果当前行只有一条语句,则分号不是必须的;如果当前行有多条语句,则分号是必须的。

    • 如果一条语句跨越多行,则大多数情况下直接换行即可,Scala 会自动断句:

      但是有时候会产生意外的效果:

      会被解析成两条语句x+y。如果希望编译器解析成单条语句x+y ,则有两种做法:

      • 使用圆括号:

      • + 放到行尾。当使用中缀操作符(如+)来串接表达式时,Scala 风格是将操作符放到行尾:

    • 分号推断规则:除非以下任意一条为true,否则代码行的末尾就被当作分号处理:

      • 当前行以一个不能作为语句结尾的词结尾,如:英文句点.,中缀操作符。
      • 下一行以一个不能作为语句开头的词开头。
      • 当前行的行尾出现在圆括号() 或者方括号[] 内,因为圆括号、方括号不能包含多条语句。

二、入门

2.1 基础数据类型

2.1.1 基础数据类型

  1. Java 的基础类型和操作符在Scala 中具有相同的含义。

  2. Scala 的基础数据类型包括:String、值类型(包括:Int/Long/Short/Byte/Float/Double/Char)、Boolean

    • Byte/Short/Int/Long/Char 类型统称为整数类型,整数类型加上FloatDouble 称作数值类型。

    • String 位于java.lang,其它几种基础数据类型位于scala 包。如Int 的完整名称是scala.Int

      由于scala 源文件中默认自动引入了java.lang 包的所有成员和scala 包的所有成员,因此可以在任何地方使用这些类型的简单名字(而不是完整名称)。

    • 取值区间:

      • Byte8位带符号整数。取值区间:,闭区间。
      • Short16位带符号整数。取值区间:,闭区间。
      • Int32位带符号整数。取值区间:,闭区间。
      • Long64位带符号整数。取值区间:,闭区间。
      • Char16位无符号Unicode字符。取值区间:,闭区间。
      • StringChar 的序列。
      • Float32IEEE 754 单精度浮点数。
      • Double64IEEE 754 双精度浮点数。
      • Booleantrue 或者false
  3. Scala 基础数据类型和Java 中对应的类型取值区间完全相同,这使得Scala 编译器可以在生成的字节码中,将Scala 的值类型,如:Int/Double 的实例转换成Java 的基本类型。

  4. Scala 中的每个基础数据类型都有一个富包装类,该富包装类提供额外的方法。当在基础数据类型调用更多的方法时,这些基础数据类型通过隐式转换得到对应的富包装类,并对富包装类调用这些方法。

    基础数据类型对应的富包装类为:Byte -> scala.runtime.RichByteShort -> scala.runtime.RichShortInt -> scala.runtime.RichIntLong -> scala.runtime.RichLongChar -> scala.runtime.RichCharFloat -> scala.runtime.RichFloatDouble -> scala.runtime.RichDoubleBoolean -> scala.runtime.RichBooleanString -> scala.collection.immutable.StringOps

2.1.2 字面量

  1. 所有基础数据类型都可以用字面量literal 来表示该类型的常量值。

  2. 整数字面量:Byte/Short/Int/Long 的字面量有两种形式:十进制表示和十六进制表示(以0x 或者0X 开头,包含0~9 以及大小的a~f 或者小写的A~F )。

    • 整数字面量不支持八进制的表示,也不支持以0 开头的表示(如012)。

    • Scalashell 总是以十进制打印整数值,无论它是用什么形式初始化的。

    • 如果整数字面量是以l 或者L 结尾,则它是Long 类型的;否则就是Int 类型的。

    • 当一个Int 类型的字面量赋值给一个Byte 或者Short 类型的变量时,该字面量会被当做Byte 或者Short 类型,只要这个字面量的值在变量类型的取值区间内即可。

  3. 浮点数字面量:由十进制数字、可选的小数点、可选的E 或者e 开头的指数组成(科学计数法)。

    如果浮点数字面量以f 或者F 结尾,则它是Float 型的;否则就是Double 型的。Double 型浮点数字面量也可以以d 或者D 结尾,但是这不是必须的。

  4. 字符字面量:由一对单引号、任意一个Unicode 字符组成。这里除了显式的给出原始字符,也可以用字符的Unicode 码来表示:\u 加上Unicode 码对应的四位十六进制数字。

    事实上,这种Unicode 字符的方式可以出现在Scala 程序的任何位置,包括变量名中:

    但这种方式并不友好,因为不容易阅读。

    还有一些字符字面量是特殊转义字符:

    • \n: 换行符\u000A
    • \b:退格符\u000B
    • \t:制表符\u0009
    • \f:换页符\u000C
    • \r:回车符\u000D
    • \":双引号\u0022
    • \':单引号\u0027
    • \\:反斜杠\u005C
  5. 字符串字面量:由双引号包起来的字符组成。其中每个字符也可以用Unicode 码表示,也支持转义字符。

    Scala 支持一种特殊语法来表示原生字符串raw string:用三个双引号开始,并以三个双引号结束。其内部可以包含任何字符,包括换行、单双引号、以及其它特殊字符(三个双引号除外)。

    原生字符串对于包含大量转义字符、或者跨越多行的字符串比较友好。

  6. Symbol字面量:格式为'ident ,其中ident 可以是任何字母、数字组成的标识符。

    一个Symbol 是一种特殊的字符串,相比较于String 类型,它更节省内存并且相等比较的速度很快。

    • 事实上String 类内部维护一个字符串池strings pool。当调用Stringintern() 方法时,如果字符串池中已经存在该字符串,则直接返回池中字符串对象的引用;如果不存在,则将该字符串添加到池中,并返回该字符串对象的引用。执行过intern() 方法的字符串被称作内部化了的,默认情况下代码中的字符串字面量都是内部化了的(在Java 中,字符串常量也是内部化了的)。同值字符串的intern() 方法返回的引用都相同。

      而在Scala 中,Symbol 类型的对象是被内部化了的,任意同名symbol 都指向同一个Symbol,因此节省了内存。

    • 由于不同名symbol 一定指向不同的Symbol 对象,因此symbol 对象之间可以使用操作符== 快速的进行相等性比较,常数时间内即可完成。而字符串的equals 方法需要逐个字符的比较两个字符串,取决于两个字符串的长度。

    • Symbol 类型一般用于快速比较,如Map<Symbol,Data> 查询Symbol 要比Map<String,Data> 查询String 快得多。

    • Symbol 字面量会被编译器展开成一个工厂方法的调用:Symbol("ident")

    • 对于Symbol 对象,你唯一能做的是获取它的名字:

  7. 布尔值字面量:只有truefalse

2.1.3 字符串插值

  1. Scala 支持字符串插值:允许在字符串字面量中嵌入表达式。

    其中表达式s"hello,$name!" 称作processed 字符串字面量。由于s 出现在字符串首个双引号之前,因此Scala 将使用s插值器 来处理该字面量。s 插值器将对内嵌的每个表达式求值,对求值结果调用toString来替代字面量中的那些表达式。

    在被处理的字符串字面量中,可以随时用美元符号$ 开启一个表达式。Scala 将从美元符号开始、直到首个非标识符的部分作为表达式。如果表达式包含非标识字符(如空格、操作符),则必须将其放入花括号{}中,左花括号需要紧跟$

  2. scala 还提供另外两种字符串插值器:

    • raw 字符串插值器:其行为跟s 字符串插值器类似,但是raw字符串插值器并不识别转义字符。

    • f 字符串插值器:其行为跟s字符串插值器类似,但是允许为内嵌的表达式添加printf 风格的格式化指令。格式化指令位于表达式之后,以百分号% 开始,使用java.util.Formatter 给出的语法。

      如果不给出格式化指令,则默认采用%s,其含义是用toString 的值来替换,就像s 字符串插值器一样。

  3. Scala 中,字符串插值器是通过编译期间重写代码来实现的。编译器会将任何由某个标记(如 r 或者 f )紧跟着字符串字面量的左双引号这样的表达式当作字符串插值器表达式求值。

    你也可以定义自己的字符串插值器来满足不同的需求。

2.2 变量定义

  1. Scala 的变量分为两种:valvar

    • val:和Javafinal 变量类似,一旦初始化就不能被重新赋值。

      注意:当采用val 定义一个变量时,变量本身不能被重新赋值,但是它指向的对象可能发生改变。如:

      不能将 string1 重新赋值为另一个数组,但是可以改变string1 指向的数组的元素。

    • var:类似于Java 的非final 变量,在整个生命周期内可以被重新赋值。

    varval 都有各自的用武之地,本质上并没有哪个更好或者更坏。

    如果代码中包含任何var 变量,则它通常是非函数式的。如果代码中完全没有var,则它可能是函数式的。因此函数式风格的编程尽量不使用varScala 鼓励使用函数式编程,尽量采用val。因为这样的代码更容易阅读、更少出错。

    采用val 的另一个好处是等式推理equational reasoing 的支持。引入的val 等于计算它的值的表达式(假设这个表达式没有副作用)。因此任何该val 变量名出现的地方,都可以直接用对应的表达式替代。

  2. Scala 的变量定义(以val 为例):

    这中定义方式显式给出了类型标注,方式为:在变量名之后添加冒号和类型 。

    实际上String 的完整形式为java.lang.String。因为Scala 程序默认引入了java.lang 包,因此可以直接写作String

    由于Scala 的类型推断可以推断出非显式指定的类型,因此上述定义可以修改为:

    这样的代码更紧凑、易读。

2.3 标识符

  1. 构成Scala 标识符的两种最重要的形式:字母数字组合、操作符。

  2. 字母数字组合标识符:以字母或者下划线开始,可以包含更多的字母、数字或下划线。

    • 字符 $ 也算字母,但是它预留给那些由Scala 编译器生成的标识符。

    • Scala 遵循了Java 的驼峰命名法camel-case的传统。如:toString、HashSet

      • 字段、方法参数、局部变量、函数的命名应该以小写字母开始。
      • 类、特质的命名应该以大写字母开始。
    • 虽然下划线是合法的标识符,但是它们在Scala 中并不常用,原因有两个:一个原因是和Java 保持一致。另一个原因是,下划线在Scala 中还有很多其它非标识符的用法。

      在标识符结尾尽量不要使用下划线。如:

      第一行将被Scala 识别为变量名name_:,这会引起编译错误。

    • Scala 中,常量命名只要求首字母大写,而Java 中要求全大写而且下划线分隔不同的单词。

      Scala 中的常量并不是val。如:方法的参数是val,但是每次被调用时,这些val 都得到不同的值。

  3. 操作符标识符:由一个或者多个操作符组成。操作符指的是那些可以被打印的ASCII 字符,如+,:,?,&

    Scala 编译器会在内部将操作符标识符用内嵌的 $ 方式转换为合法的Java 标志符。如::-> 被转换成$colon$minus$greater 。如果你希望从Java 代码中访问这些标识符,则需要使用这种内部形式。

  4. 混合标识符:由一个字母数字组合操作符、一个下划线、一个符号操作符组成。如:unary_+ 用于表示类的+ 操作符的方法名。

  5. 字面标识符:用反引号括起来的任意字符串。

    可以将任何能被runtime接收的字符串放在反引号当中,甚至当该字符串是Scala 保留字时也生效。如:

2.4 操作符

  1. Scala 为基础数据类型提供了一组丰富的操作符,但这些操作符其实只是普通方法调用的语法糖 。如:1+2 实际上是1.+(2),它调用的是Int 类的一个名为+ 的方法,该方法接收一个Int 参数并返回一个Int 结果。

    实际上Int 包含多个重载的+ 方法,这些方法分别接收不同的参数类型。

  2. Scala 中,操作符表示法不仅仅局限于那些其它语言(如Java/Python)中看起来像是操作符的那些方法,也可以包括任何方法。即:任何方法都可以是操作符。如:

    如果方法的参数有多个,则在操作符表示法中需要将这些参数都放在圆括号里。

  3. 操作符方法虽然方便使用,但是不能滥用。过度使用操作符方法会使得代码难于阅读和理解。

  4. Scala 将从数组到表达式的一切都视为带方法的对象来处理,从而实现了概念上的简化。这种统一描述并不会带来显著的性能开销,因为Scala 在编译代码时,会尽可能使用Java 数组、基本类型和原生的算术指令。

    如,Scala 的数组Array的访问方式是:将下标放置在圆括号里,如:string1(0) 。这一点与Java/C++/Python 都不同。

    • Scala 中,当用一个圆括号包围一组值应用到某个对象上时,将调用该对象的.apply() 方法。因此string1(0) 等价于string1.apply(0)

      因此在Scala 中访问数组的一个元素就是一个简单的方法调用,并没有任何特殊的地方。

    • Scala 中,当用一个圆括号包围一组值应用到某个对象上并位于赋值= 的左侧时,将调用该对象的.update() 方法。因此string1(0)="hello" 等价于string1.update(0,"hello")

2.4.1 前/中/后缀操作符

  1. + 这类操作符是中缀操作符,这意味着被调用的方法名位于调用对象和参数之间。如:1+2

    Scala 还提供了另外两类操作符:

    • 前缀操作符:方法名位于调用对象的前面。如:-1
    • 后缀操作符:方法名位于调用对象的后面。如:1 toLong
  2. 跟中缀操作符表示法不同,前缀操作符和后缀操作符是一元的:它们只接受一个操作元。前缀操作符中,操作元位于操作符右侧;后缀操作符中,操作元位于操作符左侧。

    • 唯一能被用作前缀操作符的是+,-,!,~

    • 前缀操作符对应的完整方法名是unary_ 加上操作符(注意:不包含圆括号)。如:

  3. 后缀操作符是那些不接收参数,并且在调用时没有用.() 的方法。在Scala 中,可以在方法调用时省去空的圆括号。

    但是通常来讲,如果方法有副作用,则需要保留空的圆括号。如:println() 。如果方法没有副作用,则可以省略空的圆括号。Scala 支持进一步去掉句点.,从而演化为后缀操作符表示法。

2.4.2 各类操作符

  1. 算术操作符:+,-,*,/,%:加、减、乘、除、取余。它们都是中缀操作符,对任何数值类型调用对应的算术方法。

    • 当左右两个操作元都是整数类型时,/ 操作符会计算出商的整数部分,不包含余数。% 操作符得到整数除法后的余数。

    • % 用于浮点数除法时,其余数与IEEE 754 标准不同。IEEE 754 的余数在计算时用四舍五入,而% 是截断。

      如果需要IEEE 754 的余数,则采用scala.math.IEEEremainder() 方法。

  2. Scala 对数值类型还提供了+,- 两个一元前缀操作符(unary_+ 方法和unary_- 方法),用于表示数值型字面量是正数还是负数。

    • 如果不给出+,-,则数值字面量默认为正数。
    • 一元+仅仅是为了和一元- 对称,它没有任何作用。
    • 一元- 不仅可以作用于数值字面量,还可以作用于变量,用来给变量取负值。
  3. Java++i,i++,--i,i--Scala 中并不工作。在Scala 中你可以使用:i = i+1 或者 i+=1 表示自增,i = i-1 或者 i-=1 表示自减。

  4. 关系操作符:>,<,>=,<=:大于、小于、大于等于、小于等于。它们都是中缀操作符,用于比较数值类型的大小,返回Boolean 结果。

    一元前缀操作符! (unary_! 方法) ,用于对Boolean 值取反。

  5. 逻辑操作符:&&,&,||,|:逻辑与,逻辑与,逻辑或,逻辑或。它们都是中缀操作符,用于对Boolean 操作元执行逻辑与/或,返回Boolean 结果。

    • &&|| 是短路求值的:只会对结果有决定性作用的部分求值。当操作符左侧的操作元能够决定表达式的结果时,右侧的操作元不会被求值。
    • &| 是非短路求值的:它们会对所有的操作元进行求值。
  6. Scala 中,所有方法都有一个机制来延迟对入参的求值,或者干脆不对其求值。这个机制叫做传名参数by-name parameter

  7. 位运算符:&,|,^:按位与、按位或、按位异或。它们都是中缀操作符,用于对整数类型执行位运算。

    一元前缀操作符~(unary_~ 方法) ,用于对操作元的每一位取反。

  8. 位运算符:<<,>>,>>>:左移,右移,无符号右移。它们都是中缀操作符,用于将整数左移或者右移。

    左移和无符号右移会将空出的位自动填0,右移会将空出的位自动填上符号位(最高位)。

  9. 相等运算符:==,!= :相等比较、不等比较。它们都是中缀操作符,用于比较两个对象是否相等。

    • 这两个操作符实际上可以应用于所有对象,而不仅仅是数值类型。

    • 可以比较不同类型的两个对象,甚至可以和null 比较。背后的规则很简单:

      • 首先检查左侧是否为null,如果不是null,则调用左侧对象的equals 方法。
      • 如果左侧是null,则检查右侧是null

      由于Scala 有自动的null 检查,你不必亲自做这个检查。

  10. Java 中,可以用== 来比较基本类型和引用类型。对于基本类型,== 比较的是值的相等性;对于引用类型,== 比较的是引用的相等性。

    Scala 中,== 对基本类型和引用类型都比较的是值的相等性。Scala 提供了eqne 来用于比较引用相等性。

2.4.3 优先级和结合性

  1. 操作符优先级:决定了表达式中哪些部分优先求值。当然你也可以通过圆括号来指定求值顺序。

    • Scala 中的操作符仅是用操作符表示法来使用对象的方法而已,它根据方法名的首个字母来判定优先级。

    • Scala 的操作符优先级(依次递减):(所有其它特殊字符)*,/,%+,-:=,!<,>&^|(所有字母)(所有赋值操作符)

      • 位于同一级的操作符具有相同的优先级。
      • 操作符优先级查看的是该操作符打头的字符,如&& 的优先级查看的是& 字符,<< 的优先级查看的是< 字符。
      • 一个例外是赋值操作符,它们以=结尾,且不是比较操作符(不是<=,>=,!=),它们的优先级和简单的赋值操作符= 相同。即:*= 优先级不是由* 决定,而是由= 决定。
  2. 操作符结合性:当多个同等优先级的操作符并排时,操作符的结合性决定了操作符的分组。

    • Scala 中,操作符的结合性由操作符的最后一个字符决定。任何以: 字符结尾的方法都是在它右侧的操作元上调用的,传入左侧的操作元;任何以其它字符结尾的方法都是在它左侧的操作元上调用的,传入右侧的操作元。

      a*b 等价于 a.*(b),而a ::: b 等价于 b.:::(a)

    • 不论操作符的结合性是哪一种,其操作元的求值顺序都是从左到右。

      如:a ::: b 等价于:

  3. 一个良好的编码风格是清晰的表达什么操作符被用在什么表达式上。你唯一可以放心的让其它程序员不查文档就能知道的优先级顺序是:乘除比加减优先级更高。因此通常添加圆括号() 来显式的呈现表达式的优先级。

2.5 内建控制结构

  1. Scala 只有很少的内建控制结构:if,while,for,try,match 和函数调用。
  2. Scala 所有的控制结构都有返回值,如:if、for 等结构都有返回值。这是函数式语言采取的策略:程序被认为是用于计算出某个值,因此程序的各组成部分也应该计算出某个值。

2.5.1 if

  1. if 控制结构:首先测试某个条件,然后根据条件是否满足来执行两个不同分支中的一个。if 表达式的返回值就是被选中分支的值。

2.5.2 while

  1. while 控制结构:包含了一个条件检查和一个循环体,只要条件检查为真则循环体继续执行。

    • Scala 也有 do while 循环,它跟while 循环类似,只是它会首先执行循环体然后再执行条件检查。

    • whiledo-while 并不会返回一个有意义的值,即返回类型为Unit

    • Scala 中赋值表达式的结果是Unit,而Java 中赋值表达式的结果是被赋予的值。因此下面的做法在Scala 中是不可行的:

      由于 line=readLine() 返回Unit,因此 Unit!="" 永远成立,则循环体永远执行。

  2. 通常 while 循环是和 var 成对出现的。由于 while 循环没有返回值(或者说返回Unit),它要想对程序产生任何效果则要么更新一个 var 要么执行 I/O

    因此,对于代码中的 while 循环尽量采用其它方案来替代,除非确实难以替代它。

  3. 对于 while 循环通常可以用递归函数的方式来替代。

    这个递归函数比较特殊:所有的递归调用都发生在函数尾部,因此称作尾递归。编译器会将尾递归展开成和 while 循环类似的代码。

2.5.3 for

  1. for 表达式:Scala 中的 for 表达式有很多功能。

  2. 最简单的功能是遍历集合的所有元素。

    通过 file <- files 这样的生成器语法,我们将遍历files 的每个元素。每次迭代时,一个新的、名为 fileval 都被初始化成files 中一个元素的值。

  3. 你也可以遍历一个索引。

    1 to 4 将生成一个区间Range,范围是[1,4](闭区间)。如果希望得到一个左闭右开区间,则使用 1 until 4

  4. 有时需要遍历集合中的部分元素,而不是全部。此时可以在 for 中添加过滤器。形式为:for 表达式的圆括号中添加 if 子句。

    可以包含任意多的过滤器,直接添加 if 子句即可。

  5. 也可以添加多个 <- 子句,此时得到嵌套的“循环”。

    如果愿意,你也可以使用花括号{} 而不是圆括号() 来包括生成器和过滤器,好处是可以在需要时省略某些分号。因为Scala 编译器在圆括号中并不会自动推断分号。

  6. for 的生成器和过滤器中,支持中途变量绑定:将表达式的结果绑定到新的变量上。被绑定的变量引入和使用就跟val 一样。

    上述例子中,line.trim 被重复调用两次。通过中途变量绑定可以只需要调用一次。

  7. for 表达式可以返回有效的值。这是通过yield 关键字实现的。

    • for 表达式的代码体每次都被执行,都会产出一个值。当for 表达式执行完毕后,其结果将包含所有产出的值,包含在一个集合中。结果集合的类型基于迭代子句中处理的集合种类。这个例子中,每次产出的值就是 file,返回的集合类型为 Array[File]

    • for表达式的代码体如果有多行表达式,则最后一个表达式的返回值就是该表达式代码体的结果。

    • 注意:yield 关键字的位置是:for 子句 yield 代码体 。如果代码体由花括号{} 包围的,则yield 必须在花括号之前:

2.5.4 match

  1. match 表达式:让你从若干个可选项中选择,就像其它语言中的 switch 语句一样。但是match 表达式允许你使用任意的pattern 来选择。

    缺省的case 以下划线_ 来表示,这个通配符在Scala 中经常用于表示某个完全不知道的值。

  2. ScalamatchJavaswitch 相比有一些重要区别:

    • 任何常量、字符串等等都可以用作case ,而不仅局限于Javacase 支持的整数、枚举和字符串常量。
    • 在每个case 结尾并没有 break。在Scala 中,break 是隐含的,并不会出现某个case 执行结束之后继续执行下一个case 的情况。
    • Scalamatch 表达式会返回值。匹配到的case 的子句的结果就是match 表达式的返回值。

2.5.5 break

  1. Scala 中并没有breakcontinue 关键字,如果想实现对应的功能,最简单的方式是采用if-else 结构。

    如果仍然需要break 功能,则scala.util.control.Breaks 类给出了break 方法,它可以被用于退出包含它的、用breakable 标记的代码块。

    其实现方式为:由Breaks.break 抛出一个异常,然后由外围的 breakable 方法的应用所捕获。因此,对 break 的调用并不需要一定和 breakable 的调用放在同一个方法内。

2.6 异常

  1. Scala 的异常处理也和其它语言类似,方法除了正常返回某个值意外,还可以通过抛出异常来终止执行。方法的调用方要么捕获并处理这个异常,要么自我终止并让该异常传播到更上层调用方。

    异常通过这种方式传播,逐个展开调用栈,直到某个方法处理该异常或者再没有更多方法了为止。

  2. Scala 中抛出异常与Java 看起来一样,你首先创建一个异常对象,然后通过throw 关键字将其抛出。

    Java 不同,Scala 中的throw 是一个有返回类型的表达式。技术上来讲,throw 表达式的类型是Nothing。因此可以将throw 表达式的值当作任何类型的值来看待,因为任何想使用这个返回值的地方都没有机会真正使用它。

    如果throw 表达式没有返回值,则上述的if/else 结构无法通过编译。

  3. 可以通过catch 子句来捕获异常,其语法与Scala 的模式匹配相一致。

    try-catch 表达式和其它语言一样:首先代码体被执行。如果抛出异常,则依次尝试每个catch 子句,执行第一个匹配的catch 子句。如果所有的子句都不匹配,则异常继续向上传播。

  4. scala 中,并不会要求你捕获checked exception 或者在throws 子句里声明,这和Java 不同。

    当然你也可以通过 @ throws 注解来声明一个 throws 子句,但这不是必须的。

  5. 可以将那些无论是否抛出异常都想执行的代码以表达式的形式包括在 finally 子句里。

    这是确保那些非内存资源(如:文件、套接字、数据库连接)被正确关闭的惯用做法:首先获取资源,然后在try 块中使用资源,最后在finally 块中释放资源。这和Java 是一致的。

  6. Scala 中的大多数控制结构一样,try-catch-finally 最终返回一个值:

    • 如果没有异常抛出,则try 子句的结果就是整个表达式的结果。

      如果子句是多行表达式,则最后一个表达式的结果就是整个表达式的结果。

    • 如果有异常抛出,且被 catch 子句捕获,则该catch 子句的结果就是整个表达式的结果。

    • 如果有异常抛出,且没有被 catch 子句捕获,则整个表达式就没有结果。

    • 如果有 finally 子句,则该子句计算出来的值会被丢弃。因此该子句一般执行清理工作,且不应该改变主体代码或者catch 子句中计算出来的值。

      finally 子句包含一个显式return 语句或者抛出某个异常,则该返回值或者异常会“改写”任何在之前的 try 代码块或者 catch 子句中产生的值。

    Scala 不同,Javatry-finally 并不返回值。

2.7 表达式&语句

  1. Java 一样,在Scala 中的while/if 语句中的 boolean 表达式必须放在圆括号里,不能像python 一样写 if x<0

  2. Java 一样,在Scala 中如果 if 代码块只有一条语句,则可以选择不写花括号{}

  3. Scala 支持使用分号; 来分隔语句,但是Scala 通常都不写分号。

  4. Scala 推荐使用foreach 来代替while 循环,因为foreach 是函数式风格,而while 是指令式风格。如:

    采用foreach 替代为:

    其中(arg:String) => println(arg) 是一个函数字面量,可以进一步简化为:args.foreach(println)

  5. Scala 不支持for 循环语句,但是支持for 表达式。其用法为:

    其中arg 是一个val 变量,这确保它无法在循环体被被重新赋值。

2.8 输入输出

  1. Console.in,Console.out,Console.err 分别为标准输入流、标准输出流、标准异常流对象。

    • 输出:Console.out/errprint/printf/println 等方法。
    • 输入:Console.inread/readLine 等方法。
  2. scala.io.Source 类提供了文件IO 的方法。

    • Source.fromeFile(filename):打开指定的文件并返回一个Source 对象。
    • source.getLines() :读取source 对象指向的文件,并返回一个迭代器Iterator[String]

2.9 作用域

  1. Scala 的变量作用域和Java 几乎完全一样,一个区别是:Scala 允许你在嵌套的作用域内定义同名变量。

    • 花括号{}一般都会引入一个新的作用域,因此任何在花括号中定义的元素都会在右花括号} 之后离开作用域。

    • 函数中定义的所有变量都是局部变量。这些变量在定义它们的函数内部有效。函数每次被调用时,都会使用全新的局部变量。

    • 变量一旦定义好,就不能在相同作用域内定义相同名字的变量。但是可以在嵌套的作用域内定义一个跟外部作用域中同名的变量。内嵌作用域中的变量会屏蔽外部作用域中的同名变量。

      一个良好的编程习惯是:在内嵌作用域内选择一个新的、有意义的变量名,而不是和外部作用域中的变量同名。

    • 在解释器中,可以随心所欲的使用变量名,理论上解释器会对你录入的每一条语句创建一个新的作用域。

三、For 表达式

  1. 所有给出 yield 结果的 for 表达式都会被编译器翻译成对高阶函数 map,flatMapwithFilter 的调用;所有不带yield 结果的 for 表达式都被翻译成更小范围(只有 withFilterforeach )的高阶函数。

  2. 一般而言一个 for 表达式的格式为:

    seq 是一个序列的生成器generator、定义 definition 和过滤器 filter ,它们用分号隔开。

    如:

    其中 p <- persons 表示生成器,n = p.name 表示定义,if(n.startsWith("To")) 表示过滤器。

    也可以将 seq 放到花括号而不是圆括号中,此时分号就变成可选的:

    • 生成器 generator 的格式为:

      这里的表达式 expr 通常返回一个列表,然后模式 pat 会跟这个列表里的每个元素依次匹配。如果匹配成功,则模式中的变量就会被绑定上该元素对应的部分;如果匹配失败,则列表的当前元素就被丢弃,并不会抛出 MatchError 异常。

      最常见的情况下,模式 pat 只是一个变量 x,如 x <- expr 。此时变量 x 仅仅是简单的遍历 expr 返回的所有元素。

    • 定义 definition 的格式为:

      这个定义将模式 pat 绑定到 expr 的值,因此跟如下的 val 定义的作用是一样的:

      最简单的情况是简单的变量 x

    • 过滤器filter 的格式为:

      这里 expr 是个类型为 Boolean 的表达式。过滤器会将迭代中所有让 expr 返回 false 的元素丢弃。

  3. 每个for 表达式都以生成器开始。如果一个 for 表达式中存在多个生成器,则出现在后面的生成器比出现在前面的生成器调用得更频繁。

  4. 组合数学问题是 for 表达式特别适合的应用领域。

3.1 翻译

  1. 每个 for 表达式都可以用三个高阶函数map、flatMap、withFilter 来表示。

    • 单个生成器的 for 表达式:

      等价于:

    • 带一个生成器和一个过滤器的 for 表达式:

      等价于:

      这进一步等价于:

    • 相同的翻译机制对于过滤器后面更多元素也同样适用。如果 seq 是一组任意的生成器、定义和过滤器序列,则有:

      等价于:

      然后翻译过程继续处理第二个表达式 ,这个表达式已经比原始版本少了一个元素。

    • 如果有两个生成器开始的for 表达式,则等价于对 flatMap 的应用。如:

      等价于:

      这里传递给 flatMap 的函数值当中,会有另一个 for 表达式。这个 for 表达式比原始版本少一个元素,因此更简单。它也会按照相同的规则翻译。

  2. 如果生成器左边的部分是模式 pat 而不是简单变量时,翻译机制变得复杂。

    • 如果 for 表达式绑定一个元组的情况相对比较容易处理,此时跟单变量的规则几乎相同。

      如:

      被翻译为:

    • 如果生成器左边的部分是一个任意的模式 pat 而不是单个变量或者元组时,情况更复杂。

      如:

      被翻译为:

      也就是说,生成的项首先会被过滤,只有那些跟 pat 匹配的项才能进入下一步处理。因此,采用模式匹配的生成器不会抛出 MatchError

    • 这里的机制仅处理包含单个模式匹配的生成器的 for 表达式的 case。如果 for 表达式包含了其它生成器、过滤器或定义,编译器也有类似的规则来处理。

  3. for 表达式中包含内嵌定义时,如:

    我们假设 seq 是一个(或者为空的)生成器、定义和过滤器的序列。则上述表达式被翻译为:

    可以看到:每当新的 x 值生成出来时,expr2 就会被重新求值。这个重新求值操作是必要的,因为 expr2 可能会用到 x ,因此需要针对 x 值的变化重新求值。

    因此可以看到:如果 y 的值在for 循环内是不变的,则没必要在for 表达式内部内嵌该定义。如:

    其中 funcx 无关,且非常耗时。由于在 for 循环过程中 func 结果不变,且又非常耗时,因此可以修改为:

  4. 如果 for 循环只是简单的执行副作用,但是并不返回任何值(没有 yield 表达式),则其翻译也是类似的。

    从原理上讲,之前的翻译机制中用到 mapflatMap 的地方,这里都用 foreach

    如:

    等价于:

    如:

    等价于:

  5. 事实上,也可以把高阶函数翻译成 for 表达式:每个 mapflatMapfilter 的应用也可以由 for 表达式来表示。

    因此不难看出:for 表达式就是对 map,flatMap,withFilter 这三个函数的等效表达。

3.2 泛化 for

  1. 由于编译器对 for 表达式的翻译仅依赖于相应的 map,flatMap,withFilter 方法,因此我们可以对很多数据类型应用 for 表达式。

    • 除了列表、数组之外,Scala 标准类库中还有很多类型支持 map,flatMap,withFilter,foreach 方法,因此允许对它们使用 for 表达式。

    • 对于自定义数据类型,你也可以通过支持这四个方法来支持 for 表达式。你也可以仅支持其中的一部分方法,从而部分的支持 for 表达式。

      • 如果你的类型仅定义了 map 方法,则你可以对该类型的对象应用包含单个生成器的 for 表达式。
      • 如果你的类型同时定义了 map, flatMap 方法,则你可以对该类型的对象应用包含单个或者多个生成器的 for 表达式。
      • 如果你的类型仅定义了 foreach 方法,则你可以对该类型的对象应用for 循环(而不是表达式),此时可以支持单个生成器或者多个生成器。
      • 如果你的类型定义了 withFilter 方法,则你可以对该类型的对象应用的for 循环或者 for 表达式中存在 if 过滤器。
  2. for 表达式的翻译发生在类型检查之前。Scala 对于 for 表达式本身没有规定任何类型规则,也不要求 map,flatMap,withFilter,foreach 有任何特定的类型签名。

    如:

    在这里 withFilter 产出相同类的新集合,这意味着每次 withFilter 调用都会创建新的 C 对象,就跟 filter 做的事情一样。

    如果 withFilter 创建的对象会被接下来某个方法再次解开,且 C 对象很大(比如很长的一个字符串),则你可能希望避免创建这个中间对象。一个标准的做法是:让 withFilter 不要返回 C 的对象,而是返回一个“记住”这个元素需要被过滤的包装器,然后继续处理。