《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 等。
    • 當類型成員與所封裝的類型同步變化時,類型成員更適用。有時這種特點被稱為家族多態,或者協特化。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容