《Scala 程序设计》学习笔记 Chapter 3:要点详解

操作符重载?

  • 举个例子[P60]
    • 1 + 2 中的 +操作符是一个方法
  • 首先,在 Scala 中,万物皆对象,包括基本数据类型。其次,使用中缀表达式法表示单参数方法时,其中的点号和括号可以省略,所以:1 + 2 等价于 1.+(2) 。[P60]
  • 调用无参方法也可以省略点号,这种写法也被称为后缀表示法。由于有的时候后缀表示法会触发歧义,所以 Scala 2.10+ 将其作为可选特性,需要 import scala.language.postfixOps ,或者使用 scala -language:postfixOps 开启 REPL 来开启这个特性。[P60 - 61]
  • 使用 scala -feature 开启 REPL 可以获取更多有意义的警告信息。[P61]
  • 允许出现在标示符中的字符:[P61 - 62]
    [略]
    • Scala 没有自增 / 自减运算符。
    • 在模式匹配表达式中,以小写字母开头的标记会被解析为变量标示符,而大写字母开头的标记则会被解析为常量标示符(如类名)。

无参数方法

  • Scala 允许用户决定是否为无参数方法使用括号:[P63]

    • 在定义无参数方法时如果省略括号,那么调用该方法的时候必须省略括号。但是如果定义时没有省略括号,用户在调用时可以选择省略或不省略。
  • Scala 社区的习惯:[P63]

    • 定义无副作用的无参方法时省略括号,有副作用的则添加括号。
  • scala 或者 scalac 中添加 -Xlint 参数时,定义有副作用的无参方法,省略括号时系统会发出警告。

  • 一个经典的省略括号的例子(方法链):[P63 - 64]

    def isEven(n: Int) = (n % 2) == 0
    List(1, 2, 3, 4) filter isEven foreach println
    // 展开(提示:关注一下每一个方法需要的参数,这里边并没有省略任何变量参数)
    
    List(1, 2, 3, 4).filter((i: Int) => isEven(i)).foreach((i: Int) => println(i))
    List(1, 2, 3, 4).filter(i => isEven(i)).foreach((i: Int) => println(i))
    List(1, 2, 3, 4).filter(isEven).foreach(println)
    List(1, 2, 3, 4) filter isEven foreach println
    

    简单梳理一下:filter 接收一个函数参数 isEven ,并返回一个新的 List ,然后调用 foreach ,它也接收一个函数参数,这个函数正好是 println 。其实参数一点都没省略,就是单纯的省略括号和省略 . 运算符。

    假如方法链中的某一个方法接收 0 个或大于 1 个的参数,需要为部分或全部方法补上点号。

优先级规则

  • 优先级表 [P64]

    1. 所有字母
    2. |
    3. ^
    4. &
    5. < >
    6. = !
    7. :
      • / %
    8. 其他特殊字符
  • 当 = 用于赋值时,优先级最低。[P64]

  • 在 Scala 中,任何名字以 : 结尾的方法都与右边的对象所绑定,其他的方法则是左绑定的。[P65]

  • 举个例子:cons 操作:使用 :: 方法将某一元素放置到列表前边。[P65]

    val list = List('b', 'c', 'd')
    val nList = 'a'::list
    

领域特定语言

[P65 - 66][略]

Scala 的 if 语句

  • Scala 的 if 语句是具有返回值的表达式。[P66]

  • if 语句的返回值类型也被称为所有条件分支的最小上界,也就是与每条 each 子句可能返回值类型最接近的父类型。以下例子中返回 String :[P66]

    val configFile = new java.io.File("somefile.txt")
    val configFilePath = if (configFile.exists()) {
        configFile.getAbsolutePath() // String
    } else {
        configFile.createNewFile()
        configFile.getAbsolutePath() // String
    }
    
  • Scala 没有三元表达式。[P66]

Scala 中的 for 推导式

生成器表达式

  • <- [�P67]

保护式:筛选元素

  • 通过加入 if 表达式来筛选元素,这些表达式也被称为保护式。[P68]

    val dogBreeds = List("Doberman", "yorkshire Terrier", "Dachshund", "Scottish Terrier")
    for (breed <- dogBreeds
        if breed.contains("Terrier")
    ) println(breed)
    
    val dogBreeds = List("Doberman", "yorkshire Terrier", "Dachshund", "Scottish Terrier")
    for (breed <- dogBreeds
        if breed.contains("Terrier")
        if !breed.startsWith("Yorkshire")
    ) println(breed)
    // 等价于
    for (breed <- dogBreeds
        if breed.contains("Terrier") && !breed.startsWith("Yorkshire")
    ) println(breed)
    

Yielding

  • 使用 yield 关键字在 for 表达式中生成新的集合,使用大括号替代圆括号,以相似的方法把参数列表封装在大括号中:[P68]

    val dogBreeds = List("Doberman", "yorkshire Terrier", "Dachshund", "Scottish Terrier")
    val filteredDogBreeds = for {
        breed <- dogBreeds
        if breed.contains("Terrier) && !breed.startsWith("Yorkshire")
    } yield breed
    
  • for 推导式仅包含单一表达式时使用圆括号,当其包含多个表达式时使用大括号。[P69]

扩展作用域与值定义

  • Scala 允许在 for 表达式中的最初部分定义值,并可以在后面的表达式中使用该值。[P69]

    val dogBreeds = List("Doberman", "yorkshire Terrier", "Dachshund", "Scottish Terrier")
    for {
        breed <- dogBreeds
        upcasedBreed = breed.toUpperCase() // 尽管 upcasedBreed 不可变,但并不需要使用 val 对其进行限定。
    } println(upcasedBreed)
    
  • for 表达式中使用 Option :[P69 - 70]

    val dogBreeds = List(Some("Doberman"), None, Some("Yorkshire Terrier"), Some("Dachshund"))
    for {
        breedOption <- dogBreeds
        // 系统隐式的加上了 if breedOption != None
        breed <- dogBreeds
        upcaseBreed = breed.toUpperCase()
    } println(uppercaseBreed)
    
    for {
        SOme(breed) <- dogBreeds
        upcasedBreed = breed.toUpperCase()
    } println(upcasedBreed)
    
  • 当遍历某一集合或其他像 Option 这样的容器并试图提取值时,应该使用箭头。当执行并不需要迭代的赋值操作时,应该使用等于号。[P70]

使用 trycatchfinal 语句

  • Scala 不支持已被视为失败设计的检查型异常( checked exception )。Scala 将 Java 中检查异型异常视为非检查型,而且方法声明中也不包含 throw 子句。[P72]

  • Scala 提供了有助于 Java 互操作的 @throws 注解。[P72]��

  • 在 Scala 中,异常处理用模式匹配来处理:[P72 - 73]

    var source: Option[Source] = None
    try {
        source = Some(Source.fromFile(fileName))
        val size = source.get.getLines.size
        println(s"file $fileName has $size lines")
    } catch {
        case NonFatal(ex) => println(s"Non fatal exception! $ex)" // 捕获 *非致命* 错误。
    } finally {
        for (s <- source) {
            println(s"Closing $fileName...")
            s.close
        }
    }
    
  • 在 SBT 中,使用 run-main 启动程序。启动程序前允许传递参数。[P74]

  • Scala 允许自定义异常:throw new MyBadException(...) 。如果自定义异常是一个 case 类,那么抛出异常时可以省略 new 关键字。[P74]

  • 自动资源管理:ScalaARM

名字调用和值调用 [P75 - 78]

  • manage.apply 方法

    def apply[
        R <: { def close():Unit }, // 1
        T]
        (resource: => R) // 2
        (f: R => T) = {...} // 3
    
    1. <: 表明 R 属于其他类的子类。 在本例中,R 的父类型是一个包含 close():Unit 方法的结构类型。
      • 结构化类型允许我们使用反射机制嵌入包含 close():Unit 方法的任意类型,但会造成许多系统开销,结构化类型也的代价也十分昂贵,所以 Scala 将反射列为可选特性。
    2. 传名参数:在进入 manage.apply 之前,该表达式都不会被执行,直到在 apply 中调用这个参数。(延迟计算)
    3. 匿名函数,输入为 resource 返回值类型为 T
  • 传名参数的行为与函数类似,每次使用该参数时便会执行表达式。[P77]

  • 运用传名参数,以递归取代循环结构:[P77]

    @annotation.tailrec
    def continue(conditional: => Boolean)(body: => Unit) {
        if (conditional) {
            body
            continue(conditional)(body)
        }
    }
    
    var count = 0
    continue(count < 5) {
        println(s"at $count")
        count += 1
    }
    
  • 传名参数会在每次被引用时估值。它的求值会被推迟,并可能一再地被重复调用,因此此类参数具有惰性。除此之外,Scala 还提供了惰性值。[P78]

惰性赋值

  • 以延迟的方式初始化某个值,并且表达式不会被重复计算。常见场景:[P78]

    • 表达式执行代价昂贵。
    • 缩短模块的启动时间,将不需要的工作推迟。
    • 确保对象中的其他字段的初始化过程优先执行。
  • 举个例子:[P78]

    object ExpensiveResource {
        lazy val resource: Int = init() // 推迟到需要时计算。
        def init(): Int = {
            // Some Expensive Operations.
            0
        }
    {
    
  • Scala 通过保护式( guard )来实现惰性值,由于无法解除保护式,所以会造成额外的开销。只有当保护式带来的额外开销小于初始化带来的开销,或者某些值惰性化能简化系统初始化过程,并确保执行顺序满足依赖条件时才应该使用惰性值。[P78]

枚举

  • Scala 定义了 Enumration 类来支持枚举,意味着 Scala 不为枚举提供特殊语法。[P79]

  • 一个例子:[P79 - 80]

    object Breed extends Enumeration {
        type Breed = Value
        val doberman = Value("Doberman Pinscher")
        val yorkie = Value("Yorkshire Terrier")
    }
    
    // 在其他类中
    for(breed <- Breed.values) println(s"${breed.id}, $breed")
    
    • 使用 .values 取得枚举列表。

    • Value 对象会自动从 0 开始逐一递增分配 ID 。

    • 在上述定义中,BreedValue 类型的一个别名,目的在于:

      def isTerrier(b: Breed) = b.toString.endsWith("Terrier")
      Breed.values filter isTerrier foreach println
      
    • 如果没有类型别名,上边的代码会出现语法错误。

  • 有时不希望给枚举值命名:[P80]

    object WeekDay extends Enumeration {
        type WeekDay = Value
        val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
    }
    
    def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)
    WeekDay.values filter isWorkingDay foreach println
    
  • case 类相比枚举值有两大有点:允许添加方法和字段;适用于包含未知枚举值的场景。[P81]

可插入字符串

  • 在使用 printf 格式字符串时(不是 printf 函数),Scala 会调用 Java 的 Formatter 类。printf 格式指令与 ${...} 之间不能有空格。[P81]
  • 在格式字符串中,将 DoubleInt 格式化输出会引发编译错误。[P82]
  • Scala 编译器会在某些语境中对 Java 字符串进行封装并提供一些额外的方法,这些定义在 scala.collection.immutable.StringLike 中。[P82]
  • “原生”( raw )插入器不会对控制字符串进行扩展。[P82]
  • Scala 允许自定义字符串插入器。[P83]

Trait :Scala 语言的接口和“混入”

  • Scala 使用 trait 来替代接口。(一种简单的理解是:允许将声明方法实现的接口)[P83]

  • trait 支持“混入”,即不只是定义方法和字段。可以参考书上关于日志系统的代码。[P84]

  • 使用 with 关键字混入 trait 。Scala 允许混入多个 trait ,混入的 trait 中的方法彼此独立。[P84 - 85]

  • 子类向父类传参的方式:

    class LoggedServiceImportante(name: String) extends ServiceImportante(name) with StdoutLogging {...} // 子类将 name 传递给父类
    
  • Scala 要求所有方法覆写都要使用 override 关键字。与 Java 一样,Scala 使用 super.methodName 访问父类方法。

你可能感兴趣的:(《Scala 程序设计》学习笔记 Chapter 3:要点详解)