Scala 3 — 書籍

方法特性

語言

此部分介紹在 Scala 3 中定義和呼叫方法的各種面向。

定義方法

Scala 方法有許多特性,包括這些

  • 泛型(類型)參數
  • 預設參數值
  • 多個參數群組
  • 由內容提供的參數
  • 依名稱參數
  • 等等…

此部分示範了其中一些特性,但當您定義不使用這些特性的「簡單」方法時,語法如下

def methodName(param1: Type1, param2: Type2): ReturnType = {
  // the method body
  // goes here
}
def methodName(param1: Type1, param2: Type2): ReturnType =
  // the method body
  // goes here
end methodName   // this is optional

在該語法中

  • 關鍵字 def 用於定義方法
  • Scala 標準使用駝峰式命名法命名方法
  • 方法參數總是與其類型一起定義
  • 宣告方法回傳類型是可選的
  • 方法可以包含多行或只有一行
  • 在方法主體後提供 end methodName 部分也是可選的,並且僅建議用於長方法

以下是名為 add 的一行程式方法的兩個範例,它需要兩個 Int 輸入參數。第一個版本明確顯示方法的 Int 回傳類型,而第二個版本則沒有

def add(a: Int, b: Int): Int = a + b
def add(a: Int, b: Int) = a + b

建議使用回傳類型註解公開可見的方法。宣告回傳類型可以讓您在幾個月或幾年後查看它,或查看其他人的程式碼時更容易理解它。

呼叫方法

呼叫方法非常簡單

val x = add(1, 2)   // 3

Scala 集合類別有數十種內建方法。以下範例說明如何呼叫這些方法

val x = List(1, 2, 3)

x.size          // 3
x.contains(1)   // true
x.map(_ * 10)   // List(10, 20, 30)

注意事項

  • size 不帶任何引數,並傳回清單中的元素數目
  • contains 方法帶有一個引數,也就是要搜尋的值
  • map 帶有一個引數,也就是函式;此情況下,會傳入一個匿名函式

多行方法

當方法長度超過一行時,請在第二行開始方法主體,並向右縮排

def addThenDouble(a: Int, b: Int): Int = {
  // imagine that this body requires multiple lines
  val sum = a + b
  sum * 2
}
def addThenDouble(a: Int, b: Int): Int =
  // imagine that this body requires multiple lines
  val sum = a + b
  sum * 2

在該方法中

  • sum 是不可變的區域變數;無法在方法外存取
  • 最後一行將 sum 的值加倍;這個值會從方法傳回

當您將該程式碼貼到 REPL 時,您會看到它會如預期般運作

scala> addThenDouble(1, 1)
res0: Int = 4

請注意,方法結尾不需要 return 陳述式。因為 Scala 中幾乎所有內容都是運算式,表示程式碼的每一行都會傳回 (或評估為) 一個值,所以不需要使用 return

當您濃縮該方法並將它寫在一行時,這一點會變得更清楚

def addThenDouble(a: Int, b: Int): Int = (a + b) * 2

方法主體可以使用語言的所有不同功能

  • if/else 運算式
  • match 運算式
  • while 迴圈
  • for 迴圈和 for 運算式
  • 變數指派
  • 呼叫其他方法
  • 定義其他方法

這個 getStackTraceAsString 方法會將其 Throwable 輸入參數轉換為格式良好的 String,做為實際多行方法的範例

def getStackTraceAsString(t: Throwable): String = {
  val sw = new StringWriter()
  t.printStackTrace(new PrintWriter(sw))
  sw.toString
}
def getStackTraceAsString(t: Throwable): String =
  val sw = StringWriter()
  t.printStackTrace(PrintWriter(sw))
  sw.toString

在該方法中

  • 第一行將 StringWriter 的新執行個體指派給值繫結器 sw
  • 第二行將堆疊追蹤內容儲存在 StringWriter
  • 第三行產生堆疊追蹤的 String 表示形式

預設參數值

方法參數可以有預設值。在此範例中,timeoutprotocol 參數都有提供預設值

def makeConnection(timeout: Int = 5_000, protocol: String = "http") = {
  println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
  // more code here ...
}
def makeConnection(timeout: Int = 5_000, protocol: String = "http") =
  println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
  // more code here ...

由於參數有預設值,因此可以透過以下方式呼叫方法

makeConnection()                 // timeout = 5000, protocol = http
makeConnection(2_000)            // timeout = 2000, protocol = http
makeConnection(3_000, "https")   // timeout = 3000, protocol = https

以下是關於這些範例的幾個重點

  • 在第一個範例中,沒有提供任何引數,因此方法使用 5_000http 的預設參數值
  • 在第二個範例中,2_000 提供給 timeout 值,因此使用此值,以及 protocol 的預設值
  • 在第三個範例中,兩個參數都有提供值,因此都會使用

請注意,透過使用預設參數值,使用者會認為他們可以使用三種不同的覆寫方法。

命名參數

如果您願意,您也可以在呼叫方法時使用方法參數的名稱。例如,makeConnection 也可以透過以下方式呼叫

makeConnection(timeout=10_000)
makeConnection(protocol="https")
makeConnection(timeout=10_000, protocol="https")
makeConnection(protocol="https", timeout=10_000)

在某些架構中,命名參數被大量使用。當多個方法參數具有相同的類型時,它們也非常有用

engage(true, true, true, false)

如果沒有 IDE 的協助,該程式碼可能會難以閱讀,但此程式碼更為清晰且明顯

engage(
  speedIsSet = true,
  directionIsSet = true,
  picardSaidMakeItSo = true,
  turnedOffParkingBrake = false
)

關於不帶參數的方法的建議

當方法不帶參數時,它被稱為具有arity-0arity 層級。類似地,當方法帶有一個參數時,它是一個arity-1 方法。當您建立 arity-0 方法時

  • 如果方法執行副作用,例如呼叫 println,請使用空括號宣告方法
  • 如果方法不執行副作用,例如取得集合的大小(類似於存取集合上的欄位),請省略括號

例如,此方法執行副作用,因此使用空括號宣告

def speak() = println("hi")

這樣做需要呼叫方法的人在呼叫方法時使用開啟括號

speak     // error: "method speak must be called with () argument"
speak()   // prints "hi"

雖然這只是一個慣例,但遵循它可以大幅提升程式碼可讀性:它讓你可以一目了然地了解到一個元數為 0 的方法會執行副作用。

使用 if 作為方法主體

由於 if/else 表達式會傳回一個值,因此它們可以用作方法的主體。以下是名為 isTruthy 的方法,它實作了 Perl 中 truefalse 的定義

def isTruthy(a: Any) = {
  if (a == 0 || a == "" || a == false)
    false
  else
    true
}
def isTruthy(a: Any) =
  if a == 0 || a == "" || a == false then
    false
  else
    true

這些範例說明了該方法如何運作

isTruthy(0)      // false
isTruthy("")     // false
isTruthy("hi")   // true
isTruthy(1.0)    // true

使用 match 作為方法主體

一個 match 表達式也可以用作整個方法主體,而且通常會這樣做。以下是 isTruthy 的另一個版本,使用 match 表達式撰寫

def isTruthy(a: Any) = a match {
  case 0 | "" | false => false
  case _ => true
}
def isTruthy(a: Matchable) = a match
  case 0 | "" | false => false
  case _ => true

這個方法的運作方式與先前使用 if/else 表達式的方法相同。我們使用 Matchable 而不是 Any 作為參數的類型,以接受任何支援模式比對的值。

有關 Matchable 特質的更多詳細資訊,請參閱 參考文件

控制類別中的可見性

在類別、物件、特質和列舉中,Scala 方法預設為公開,因此在此建立的 Dog 執行個體可以存取 speak 方法

class Dog {
  def speak() = println("Woof")
}

val d = new Dog
d.speak()   // prints "Woof"
class Dog:
  def speak() = println("Woof")

val d = new Dog
d.speak()   // prints "Woof"

方法也可以標記為 private。這會讓它們對目前的類別為私有的,因此它們無法在子類別中被呼叫或覆寫

class Animal {
  private def breathe() = println("I’m breathing")
}

class Cat extends Animal {
  // this method won’t compile
  override def breathe() = println("Yo, I’m totally breathing")
}
class Animal:
  private def breathe() = println("I’m breathing")

class Cat extends Animal:
  // this method won’t compile
  override def breathe() = println("Yo, I’m totally breathing")

如果你想要讓一個方法對目前的類別為私有的,同時也允許子類別呼叫或覆寫它,請將該方法標記為 protected,如本範例中 speak 方法所示

class Animal {
  private def breathe() = println("I’m breathing")
  def walk() = {
    breathe()
    println("I’m walking")
  }
  protected def speak() = println("Hello?")
}

class Cat extends Animal {
  override def speak() = println("Meow")
}

val cat = new Cat
cat.walk()
cat.speak()
cat.breathe()   // won’t compile because it’s private
class Animal:
  private def breathe() = println("I’m breathing")
  def walk() =
    breathe()
    println("I’m walking")
  protected def speak() = println("Hello?")

class Cat extends Animal:
  override def speak() = println("Meow")

val cat = new Cat
cat.walk()
cat.speak()
cat.breathe()   // won’t compile because it’s private

protected 設定表示

  • 同一個類別的其他執行個體可以存取方法 (或欄位)
  • 目前套件中的其他程式碼無法看到
  • 子類別可以使用

物件可以包含方法

您之前看到特質和類別可以有方法。Scala object 關鍵字用於建立單例類別,而物件也可以包含方法。這是將一組「工具程式」方法分組的好方法。例如,這個物件包含一組用於字串的方法

object StringUtils {

  /**
   * Returns a string that is the same as the input string, but
   * truncated to the specified length.
   */
  def truncate(s: String, length: Int): String = s.take(length)

  /**
    * Returns true if the string contains only letters and numbers.
    */
  def lettersAndNumbersOnly_?(s: String): Boolean =
    s.matches("[a-zA-Z0-9]+")

  /**
   * Returns true if the given string contains any whitespace
   * at all. Assumes that `s` is not null.
   */
  def containsWhitespace(s: String): Boolean =
    s.matches(".*\\s.*")

}
object StringUtils:

  /**
   * Returns a string that is the same as the input string, but
   * truncated to the specified length.
   */
  def truncate(s: String, length: Int): String = s.take(length)

  /**
    * Returns true if the string contains only letters and numbers.
    */
  def lettersAndNumbersOnly_?(s: String): Boolean =
    s.matches("[a-zA-Z0-9]+")

  /**
   * Returns true if the given string contains any whitespace
   * at all. Assumes that `s` is not null.
   */
  def containsWhitespace(s: String): Boolean =
    s.matches(".*\\s.*")

end StringUtils

擴充方法

在許多情況下,您會想要為封閉類別新增功能。例如,假設您有一個 Circle 類別,但您無法變更其原始碼。它可以在第三方程式庫中這樣定義

case class Circle(x: Double, y: Double, radius: Double)

當您想要為這個類別新增方法時,您可以將它們定義為擴充方法,如下所示

implicit class CircleOps(c: Circle) {
  def circumference: Double = c.radius * math.Pi * 2
  def diameter: Double = c.radius * 2
  def area: Double = math.Pi * c.radius * c.radius
}

在 Scala 2 中,使用 implicit class,在此處了解更多詳細資訊。

extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2
  def diameter: Double = c.radius * 2
  def area: Double = math.Pi * c.radius * c.radius

在 Scala 3 中,使用新的 extension 建構。有關更多詳細資訊,請參閱這本書中的章節,或Scala 3 參考

現在,當您有一個名為 aCircleCircle 執行個體時,您可以這樣呼叫這些方法

aCircle.circumference
aCircle.diameter
aCircle.area

更多

還有更多關於方法的知識,包括如何

  • 呼叫超類別的方法
  • 定義和使用依名稱參數
  • 撰寫採用函數參數的方法
  • 建立內嵌方法
  • 處理例外
  • 使用可變長度輸入參數
  • 撰寫具有多個參數群組 (部分套用函數) 的方法
  • 建立具有泛型類型參數的方法

請參閱本書中的其他章節,以取得這些功能的更多詳細資訊。

此頁面的貢獻者