在抉擇的哪一刻,成敗實已露出端倪。
Scala
擁有兩種參數傳遞的方式:Call-by-Value
(按值傳遞)與Call-by-Name
(按名傳遞)。Call-by-Value
避免了參數的重復求值,效率相對較高;而Call-by-Name
避免了在函數調用時刻的參數求值,而將求值推延至實際調用點,但有可能造成重復的表達式求值。
兩者存在微妙的差異,并應用于不同的場景。本文將闡述兩者之間的差異,并重點討論Call-by-Name
的實現模式和應用場景。
- 基本概念
- val與值
- def與方法
- val與var
- val與def
- 參數傳遞
- 按值傳遞
- 按名傳遞
- 借貸模式
基本概念
val與值
val
用于「變量聲明」與「值(Value)」定義。例如,pi
定義了一個常量,它直接持有Double
類型的字面值。
val pi = 3.1415926
val
也可以直接定義「函數值(Function Literals)」。例如,max
變量定義了一個類型為(Int, Int) => Int
的函數值。
val max = (x: Int, y: Int) => Int = if (x > y) x else y
當使用val
定義變量時,其引用的對象將被立即求值。max
在定義時,它立即對=
的右側表達式進行求值,它直接持有(Int, Int) => Int
類型的函數值。上例等價于:
val max = new Function2[Int, Int, Int] {
def apply(x: Int, y: Int): Int = if (x > y) x else y
}
但是,apply
方法并沒有立即被求值。直至發生函數調用時才會對apply
進行求值。
def與方法
def
用于定義「方法(Method)」。例如,max
定義了一個(Int, Int)Int
的方法,它表示max
是一個參數類型為(Int, Int)
,返回值類型為Int
的方法定義。
def max(x: Int, y: Int): Int = if (x > y) x else y
當使用def
定義方法時,其方法體并沒有立即被求值。但是,每當調用一次max
,方法體將被重復地被求值。
返回函數
可以將上例max
方法進行變換,使其返回(Int, Int) => Int
的函數值。
def max = (x: Int, y: Int) => if (x > y) x else y
此時,max
定義了一個方法,但省略了參數列表,其返回值類型為(Int, Int) => Int
。它等價于
def max() = (x: Int, y: Int) => if (x > y) x else y
因為max
是一個「無副作用」的方法,按照慣例,可以略去「空參數列表」,即省略max
后面的小括號()
。一則對外聲明無副作用的語義,二則使代碼更加簡明扼要。
方法與函數
def max(x: Int, y: Int): Int = if (x > y) x else y
def max = (x: Int, y: Int) => if (x > y) x else y
兩者都定義為「方法(Method)」,但后者返回了一個函數(Function)類型。因此,后者常常也被習慣地稱為「函數(Function)」。
首先,它們兩者可以具有相同的調用形式:max(1, 2)
。但對于后者,調用過程實際上包括了兩個子過程。
- 首先調用
max
返回(Int, Int) => Int
的實例; - 然后再在該函數的實例上調用
apply
方法,它等價于:
max.apply(1, 2)
其次,兩者獲取函數值的方式不同。后者可以直接獲取到函數值,而對于前者需要執行η
擴展才能取得等價的部分應用函數。
val f = max _
此時,f
也轉變為(Int, Int) => Int
的函數類型了。實施上,對于上例,η
擴展的過程類似于如下試下。
val f = new (Int, Int) => Int {
def apply(x: Int, y: Int): Int = max(x, y)
}
val與var
var
與val
都可以用于定義變量,但兩者表示不同的語義。val
一旦引用了對象,便不能再次引用其它對象了。
val s1 = "Alice"
s1 = "Bob" // Error
而var
引用變量可以隨時改變去引用其它的對象。
var s2 = "Alice"
s2 = "Bob" // OK
另外,var/val
都可以引用不可變(Immutable)類的實例,也可以引用可變(Mutable)類的實例。
val s1 = new StringBuilder // val可以引用可變類的實例
var s2 = "Alice" // var也可以引用不可變類的實例
var/val
的差異在于引用變量本身的可變性,前者表示引用隨時可修改,而后者表示引用不可修改,與它們所引用的對象是否可變無關。
val與def
def
用于定義方法,val
定義值。對于「返回函數值的方法」與「直接使用val
定義的函數值」之間存在微妙的差異,即使它們都定義了相同的邏輯。例如:
val max = (x: Int, y: Int) => if (x > y) x else y
def max = (x: Int, y: Int) => if (x > y) x else y
語義差異
雖然兩者之間僅存在一字之差,但卻存在本質的差異。
-
def
用于定義「方法」,而val
用于定義「值」。 -
def
定義的方法時,方法體并未被立即求值;而val
在定義時,其引用的對象就被立即求值了。 -
def
定義的方法,每次調用方法體就被求值一次;而val
僅在定義變量時僅求值一次。
例如,每次使用val
定義的max
,都是使用同一個函數值;也就是說,如下語句為真。
max eq max // true
而每次使用def
定義的max
,都將返回不同的函數值;也就是說,如下語句為假。
max eq max // false
其中,eq
通過比較對象id
實現比較對象間的同一性的。
類型參數
val
代表了一種餓漢求值的思維,而def
代表了一種惰性求值的思維。但是,def
具有更好可擴展性,因為它可以支持類型參數。
def max[T : Ordering](x: T, y: T): T = Ordering[T].max(x, y)
lazy惰性
def
在定義方法時并不會產生實例,但在每次方法調用時生成不同的實例;而val
在定義變量時便生成實例,以后每次使用val
定義的變量時,都將得到同一個實例。
lazy
的語義介于def
與val
之間。首先,lazy val
與val
語義類似,用于定義「值(value)」,包括函數值。
lazy val max = (x: Int, y: Int) => if (x > y) x else y
其次,它又具有def
的語義,它不會在定義max
時就完成求值。但是,它與def
不同,它會在第一次使用max
時完成值的定義,對于以后再次使用max
將返回相同的函數值。
參數傳遞
Scala
存在兩種參數傳遞的方式。
- Pass-by-Value:按值傳遞
- Pass-by-Name:按名傳遞
按值傳遞
默認情況下,Scala
的參數是按照值傳遞的。
def and(x: Boolean, y: Boolean) = x && y
對于如下調用語句:
and(false, s.contains("horance"))
表達式s.contains("horance")
首先會被立即求值,然后才會傳遞給參數y
;而在and
函數體內再次使用y
時,將不會再對s.contains("horance")
表達式求值,直接獲取最先開始被求值的結果。
傳遞函數
將上例and
實現修改一下,讓其具有函數類型的參數。
def and(x: () => Boolean, y: () => Boolean) = x() && y()
其中,() => Boolean
等價于Function0[Boolean]
,表示參數列表為空,返回值為Boolean
的函數類型。
調用方法時,傳遞參數必須顯式地加上() =>
的函數頭。
and(() => false, () => s.contains("horance"))
此時,它等價于如下實現:
and(new Function0[Boolean] {
def apply(): Boolean = false
}, new Function0[Boolean] {
def apply(): Boolean = s.contains("horance")
}
此時,and
方法將按照「按值傳遞」將Function0
的兩個對象引用分別傳遞給了x
與y
的引用變量。但時,此時它們函數體,例如s.contains("horance")
,在參數傳遞之前并沒有被求值;直至在and
的方法體內,x
與y
調用了apply
方法時才被求值。
也就是說,and
方法可以等價實現為:
def and(x: () => Boolean, y: () => Boolean) = x.apply() && y.apply()
按名傳遞
通過Function0[R]
的參數類型,在傳遞參數前實現了延遲初始化的技術。但實現中,參數傳遞時必須構造() => R
的函數值,并在調用點上顯式地加上()
完成apply
方法的調用,存在很多的語法噪聲。
因此,Scala
提供了另外一種參數傳遞的機制:按名傳遞。按名傳遞略去了所有()
語法噪聲。例如,函數實現中,x
與y
不用顯式地加上()
便可以完成調用。
def and(x: => Boolean, y: => Boolean) = x && y
其次,調用點用戶無需構造() => R
的函數值,但它卻擁有延遲初始化的功效。
and(false, s.contains("horance"))
借貸模式
資源回收是計算機工程實踐中一項重要的實現模式。對于具有GC
的程序設計語言,它僅僅實現了內存資源的自動回收,而對于諸如文件IO
,數據庫連接,Socket
連接等資源需要程序員自行實現資源的回收。
該問題可以形式化地描述為:給定一個資源R
,并將資源傳遞給用戶空間,并回調算法f: R => T
;當過程結束時資源自動釋放。
- Input: Given resource: R
- Output:T
- Algorithm:Call back to user namespace: f: R => T, and make sure resource be closed on done.
因此,該實現模式也常常被稱為「借貸模式」,是保證資源自動回收的重要機制。本文通過using
的抽象控制,透視Scala
在這個領域的設計技術,以便鞏固「按名傳遞」技術的應用。
控制抽象:using
import scala.language.reflectiveCalls
object using {
type Closeable = { def close(): Unit }
def apply[T <: Closeable, R](resource: => T)(f: T => R): R = {
var source = null.asInstanceOf[T]
try {
source = resource
f(source)
} finally {
if (source != null) source.close
}
}
}
客戶端
例如如下程序,它讀取用戶根目錄下的README.md
文件,并傳遞給using
,using
會將文件句柄回調給用戶空間,用戶實現文件的逐行讀取;當讀取完成后,using
自動關閉文件句柄,釋放資源,但用戶無需關心這個細節。
import scala.io.Source
import scala.util.Properties
def read: String = using(Source.fromFile(readme)) {
_.getLines.mkString(Properties.lineSeparator)
}
鴨子編程
type Closeable = { def close(): Unit }
定義了一個Closeable
的類型別名,使得T
必須是具有close
方法的子類型,這是Scala
支持「鴨子編程」的一種重要技術。例如,File
滿足T
類型的特征,它具有close
方法。
惰性求值
resource: => T
是按照by-name
傳遞,在實參傳遞形參過程中,并未對實參進行立即求值,而將求值推延至resource: => T
的調用點。
對于本例,using(Source.fromFile(source))
語句中,Source.fromFile(source)
并沒有馬上發生調用并傳遞給形參,而將求值推延至source = resource
語句。