《Scala 程序设计》学习笔记 Chapter 2:更简洁 更强大

分号

  • 在 scala REPL 中,使用 :paste 模式输入多行代码,然后用 Ctrl-D 结束。[P29]

变量声明

  • valvar 关键字只标识引用本身是否可以指向另一个不同的对象,它们并未表明其所引用的对象是否可变。[P30]
  • 为了减少可变性引起的 bug ,应当*尽可能地使用不可变变量。[p30]

Range [P31]

  1. 1 to 10 : 1, 2, .., 10
  2. 1 until 10 : 1, 2, 3, ..., 9
  3. 1 to 10 by 3 : 1, 4, 7, 10
  4. 1 to 10 by -3 : 10, 7, 4, 1
  5. 1L to 10L by 3L
  6. 1f to 10.3f by 0.3f
  7. 'a' to 'g' by 3

偏函数

  • ”偏“:偏函数不处理所有可能的输入,只处理能与至少一个 case 语句匹配的输入。[P32]

  • 在偏函数中只能用 case 语句,而整个函数必须用花括号包围。[P32]

  • 如果偏函数被调用,而函数的输入却与所有语句都不匹配,系统会抛出一个 MatchError 运行时错误。[P32]

  • 使用 isDefinedAt 方法测试特定输入是否与偏函数匹配:f.isDefinedAt(x) 返回 true / false 。[P32]

  • 可以使用 orElse 语法连接偏函数:[P32]

    val pf1 = PartialFunction[Any, String] = {case s:String => "YES"}
    val pf2 = PartialFunction[Any, String] = {case d:Double => "YES"}
    val pf = pf1 orElse pf2
    

方法声明

  • copy 方法也是 case 类自动创建的。它允许你在创建 case 类的新实例时,只给出与原始对象不同部分的参数。[P33]

方法具有多个参数列表

  • Scala 允许我们把参数列表两边的圆括号替换为花括号。[P34]

    def draw(offset: Point = Point(0.0, 0.0))(f: String => Unit): Unit = f(s"draw(offset = $offset), ${this.toString}")
    
    s.draw(Point(1.0, 2.0))(str => println(s"ShapesDrawingActor: $str"))
    // 等价于
    s.draw(Point(1.0, 2.0)){
        str => println(s"ShapesDrawingActor: $str"))
    }
    
  • 使用具有多个参数列表的方法有助于 Scala 进行参数推断。[�P35]

  • 使用具有多个参数列表的方法中,可以使用最后一个参数列表推断隐含参数。�[P35]

  • 隐含参数是用 implicit 关键字声明的参数,当相应方法被调用时,我们可以显式指定这个参数,或者不指定,让编译器在当前作用域找到一个合适的值作为参数。[P35]

  • 隐含参数可以代替参数默认值,而且更加灵活。[P35]

Future 简介

  • scala.concurrent.Future� 是 Scala 提供的一个并发工具,其 API 使用隐含参数减少代码冗余。[P35]

  • Future 与隐含参数[P36]

    apply[T](body: => T)(implicit executor: ExecutionContext): Future[T]
    
    import scala.concurrent.ExecutionContext.Implicits.global
        val future = {
            ...
        }
    
  • 只有用 implicit 关键字声明,在当前作用域可见的对象才能用作隐含值;只有被声明为 implicit 的函数参数才允许调用时不给出实参,而采用隐含的值。��[P37]

嵌套方法的定义与递归

  • 内部变量会屏蔽外部相同名称的变量。[P38]

  • Scala 采用局部作用域类型推断,所以必须为递归方法显式声明返回值类型。[P39]

  • 尾递归:调用递归函数是该函数的最后一个表达式,该表达式的返回值就是所调用的递归函数的返回值。[P39]

  • 尾递归检查 @tailrec [P39]

    import scala.annotation.tailrec
    
    def factorial(i: Int): Long = {
        @tailrec
        def fact(i: Int): Long = {
            if (i <= 1) accumulator
            else fact(i - 1, i * accumulator)
        }
        fact(i, 1)
    }
    
    • 如果函数不是尾递归,编译器会报错。
  • 外层方法中的一切在嵌套方法中都是可见的,包括传递给外层方法的参数。

推断类型信息

  • 什么时候需要显式类型注解:[P41]

    • 声明了可变的 var 变量或不可变的 val 变量,没有进行初始化。
    • 所有的方法参数(如 def deposit(amount: Money) = {...} )。
    • 方法的返回值类型,在以下情况中必须显式声明其类型。
      • 在方法中明显地使用了 return (即使在方法末尾也是如此)。
      • 递归方法。
      • 两个或多个方法重载(拥有相同的函数名),其中一个方法调用了另一个重载方法,调用者需要显式类型注解。
      • Scala 推断出的类型比你期望的类型更为宽泛,如 Any 。(少见)
  • Scala 支持可变方法,但是:[P42]

    • 方法可以用有其他参数,但必须位于可变参数之前;
    • 方法只能有一个可变参数。
  • 加入返回类型注解可以在编译阶段发现推断类型错误,也有助于提升代码可读性。[P43]

  • Scala 将方法体前的声明和等号当做函数定义,而在函数式编程中,函数总要有返回值。当 Scala 发现函数体之前没有等号时,就认定程序员希望该方法是一个 procedure ,意味着它只返回 Unit 。[P44]

  • Unit 类型拥有一个名为 () 的值。[P44]

保留字 [P44 - 45]

  • forSome :用在已经存在的类型声明中,限制其能够使用的具体类型。

  • implicit :使得方法或变量值可以被�������用于隐含转换;将方法参数标记为可选的,只要在调用该方法时,作用域内有类型匹配的候选对象,就会使用该对象作为参数。

  • lazy :推迟 val 变量的赋值。

  • requires :[已停用]

  • sealed:用于父类型,要求所有派生的子类必须在同一个源文件中声明。

  • trait:这是一个混入模块,对类的实例添加额外的状态和行为;也可以用于声明而不实现方法,类似 Java 的 interface

  • type:声明类型。

  • yield:在 for 循环中返回元素,这些元素会构成一个序列。

  • 还有一些字符,具体看文档。

  • Scala 没有 breakcontinue

  • 在 Scala 中引用保留的 Java 方法时,要为其加上 ``

    java.util.Scanner.`match`
    

字面量

整数字面量

  • 整数字面量如果超过规定的范围,会引发一个编译错误。[P46]

字符字面量

  • 不可打印的 Unicode 字符(如:\u0009 水平制表符)在 Scala 中是不允许的。[P48]

字符串字面量

  • 字符串字面量是被双引号或者三重引号包围的字符串序列,如 """...""" 。[P48]
  • 用三重双引号包含的字符串字面量被称为多行字符串字面量,这些字符串可以跨越多行, 换行符是字符串的一部分。可以包含任意字符,但不能出现三个连续的双引号。三重双引号包含的字符串不转义。[P49]
  • 在多行字符串中,可以使用 String.stripMargin 移除每行字符串开头的空格和第一个遇到的垂直分割符 | 。 [P49]
    • 如果希望用别的字符替代 | ,可以使用 stripMargin 的重载版本,该函数可以指定一个 Char 参数替代 | 。如果想要移除整个字符串(而不是字符串的各个行)的前缀和后缀,有相应的 stripPrefixstripSuffix 方法可以完成。

符号字面量

  • Scala 支持符号。符号是一些规定的字符串。两个同名符号会指向内存中的同一对象。[P50]
  • 符号是单引号( ' )后边跟上一个或多个数字、字幕或下划线,但第一个字符不能是数字。[P50]
  • 符号字面量 'id 是表达式 scala.Symbol("id") 的简写形式,如果要创建一个包含空格的符号,可以使用 Symbol.apply ,比如 Symbol(" programming Scala") 。[P50]

函数字面量

  • (i: Int, s: String) => s + i 是一个类型为 Function2[Int, String, String] 的函数字面量。以下声明等价:[P50]

    val f1: (Int, String) => String = (i, s) => s + i
    val f2: Function2[Int, String, String] = (i, s) => s + i
    

元组字面量

  • Scala 库中包含 TupleN 类,用于组建 N 元素组,它以小括号加上逗号分隔的元素序列的形式来创建元素组。TupleN 表示的多个类各自独立,N 的取值从 1 到 22 ,包括 22 。[P50]

  • 用字面量语法声明 Tuple 类型的变量:[P50]

    val t1: (Int, String) = (1, "two")
    val t2: Tuple2[Int, String] = (1, "two")
    
  • 使用元组:[P51]

    val t = ("Hello", 1, 2.3)
    println("Print the whole tuple: " + t)
    println("Print the first item: " + t._1)
    println("Print the second item: " + t._2)
    println("Print the third item: " + t._3)
    
  • 表达式 t._n 提取元组 t 中的第 n 个元素。元组从 1 开始计数。[P51]

  • 一个量元素的元组,有时被简称为 pair ,有很多定义 pair 的方法,除了在圆括号中列出元素值以外,还可以”箭头操作符“放在两个值之间,也可以用相应类的工厂方法:[P51]

    (1, "one")
    1 -> "one
    Tuple2(1, "one")
    

    箭头操作符只适用于两元素的元组。

OptionSomeNone :避免使用 null

  • 抽象类 Option 具有两个两个具体的子类 SomeNoneSome 用于表示有值,None 用于表示没有值。[P52]

  • 举个例子:[P52]

    val stateCapitals = Map(
        "Alabama" -> "Montgomery",
        "Alaska" -> "Juneau"
    )
    stateCapitals.get("Alabama") // Some(Montgomery)
    stateCapitals.get("Alabama").get // "Montgomery"
    stateCapitals.get("Unknown") // None
    stateCapitals.get("Unknown").getOrElse("Oops") // "Oops"
    

    Map.get 方法返回 Option[T],本例子中 TString

封闭类的继承

  • 关键字 sealed 用于实现封闭类的继承。[P53]

    sealed abstract class Option[+A] ... { ... }
    

    关键字 sealed 告诉编译器,所有的子类必须在同一个源文件中声明。

  • 如果为了防止用户派生任何子类,也可以用 final

用文件和命名空间组织代码

  • Scala 支持嵌套 package 语法:[P54 - 55]

    package org {
        ...
        package scala {
            ...
            package demo {
            ...
            }
        }
    }
    package com {
        package example {
            ...
        }
    }
    package com1.example2 {
        ...
    }
    
  • 使用连续包声明时,必须使用单独的 package 语句:[P55]

    package com.example // 导入 example 中所有包级别的声明
    package mypkg // 导入 mypkg 中所有包级别的声明
    class MyPkgClass {
        ...
    }
    
  • Scala 不允许在脚本中定义包,脚本被隐含包装在一个对象中。在对象中声明包是不允许的。[P55]

导入类型及其成员

  • 在 Scala 中,使用 _ 作为通配符。[P56]

  • Scala 的 import 语句几乎可以放在任何位置上,所以你可以将其可见性限制在需要的作用域中。[P56]

  • 可以在导入时对类型做重命名以解决冲突问题:[P56]

    import java.math.BigInteger.{
        ONE => _, // 重命名为下划线,使该常量不可见。
        TEN,
        ZERO => JAVAZERO }
    

导入是相对的

import collection.immutable._ // 由于 scala 已经默认导入,所以不需要给出全路径
import _root_.scala.collection.parallel._ // 从 ”根“ 开始的全路径

使用第二种导入方法时,要保证库的所在路径被包含在了 CLASSPATH 中。[P57]

包对象

  • Scala 支持包对象一种特殊类型的、作用域为包层次的对象。它像普通的对象一样声明,但与普通对象有着一些不同点:[P57]

    • 文件名必须是 package.scala 。 - 1
    • 标记上层包的作用域。 - 2
    • 使用 package 关键字给包名之后的对象命名。 - 3
    • 适合暴露给客户端的成员。 - 4
    // src/com/example/json/package.scala // 1
    package com.example // 2
    package object json { // 3
        class JSONObject { ... } // 4
        def fromString(string: String): JSONObject = { ... }
    }
    // 客户端可以使用 import com.example.json._ 导入所有定义,或用通常方法单独导入元素。
    

抽象类型与参数化类型

  1. Scala 的参数化类型与 Java 的泛型很像。举一个参数化类型的例子:[P58]

    • 在 Scaladoc 中,List 的声明为 sealed abstract class List[+A]
    • A 之前的 + 表示:如果 BA 的子类型,则 List[B] 也是 List[A] 的子类型,这被称为协类型。
    • 如果类型参数前有 - ,则表示另一种关系:如果 BA 的子类型,且 Foo[A] 被声明为 Foo[-A] ,则 Foo[B]Foo[A] 的父类型(称为逆类型)。
  2. Scala 还支持一种被称为”抽象类型“ 的抽象机制,可以用在许多参数化类型中。[P58]

  3. 参数化类型和抽象类型都被声明为其他类型的成员,就像是该类型的方法与属性一样。[P58 - 59]

    // 类型成员( type )
    abstract class BulkReader {
        type In
        val source: In
        def read: String // 不同的 In ,read 的方式不同。
    }
    class StringBulkReader extends BulkReader {
        type In = String
    }
    
    // 参数化类型
    abstract class BulkReader[In] {
        val source: In
        ...
    }
    class StringBulkReader extends BulkReader[String] = { ... }
    
  4. 类型成员比参数化类型的优势:[P59]

    • 当类型参数与参数化类型无关时,参数化类型更适用。
      • 举个例子:List[A]A 可能是 IntString 等。
    • 当类型成员与所封装的类型同步变化时,类型成员更适用。有时这种特点被称为家族多态,或者协特化。

你可能感兴趣的:(《Scala 程序设计》学习笔记 Chapter 2:更简洁 更强大)