此部分介紹在 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
表示形式
預設參數值
方法參數可以有預設值。在此範例中,timeout
和 protocol
參數都有提供預設值
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_000
和http
的預設參數值 - 在第二個範例中,
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-0 的arity 層級。類似地,當方法帶有一個參數時,它是一個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 中 true
和 false
的定義
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 參考。
現在,當您有一個名為 aCircle
的 Circle
執行個體時,您可以這樣呼叫這些方法
aCircle.circumference
aCircle.diameter
aCircle.area
更多
還有更多關於方法的知識,包括如何
- 呼叫超類別的方法
- 定義和使用依名稱參數
- 撰寫採用函數參數的方法
- 建立內嵌方法
- 處理例外
- 使用可變長度輸入參數
- 撰寫具有多個參數群組 (部分套用函數) 的方法
- 建立具有泛型類型參數的方法
請參閱本書中的其他章節,以取得這些功能的更多詳細資訊。