在抉擇的哪一刻,成敗實已露出端倪巾表。
Scala
擁有兩種參數(shù)傳遞的方式:Call-by-Value
(按值傳遞)與Call-by-Name
(按名傳遞)汁掠。Call-by-Value
避免了參數(shù)的重復求值,效率相對較高集币;而Call-by-Name
避免了在函數(shù)調(diào)用時刻的參數(shù)求值考阱,而將求值推延至實際調(diào)用點,但有可能造成重復的表達式求值鞠苟。
兩者存在微妙的差異乞榨,并應用于不同的場景。本文將闡述兩者之間的差異偶妖,并重點討論Call-by-Name
的實現(xiàn)模式和應用場景姜凄。
- 基本概念
- val與值
- def與方法
- val與var
- val與def
- 參數(shù)傳遞
- 按值傳遞
- 按名傳遞
- 借貸模式
基本概念
val與值
val
用于「變量聲明」與「值(Value)」定義政溃。例如趾访,pi
定義了一個常量,它直接持有Double
類型的字面值董虱。
val pi = 3.1415926
val
也可以直接定義「函數(shù)值(Function Literals)」扼鞋。例如,max
變量定義了一個類型為(Int, Int) => Int
的函數(shù)值愤诱。
val max = (x: Int, y: Int) => Int = if (x > y) x else y
當使用val
定義變量時云头,其引用的對象將被立即求值。max
在定義時淫半,它立即對=
的右側表達式進行求值溃槐,它直接持有(Int, Int) => Int
類型的函數(shù)值。上例等價于:
val max = new Function2[Int, Int, Int] {
def apply(x: Int, y: Int): Int = if (x > y) x else y
}
但是科吭,apply
方法并沒有立即被求值昏滴。直至發(fā)生函數(shù)調(diào)用時才會對apply
進行求值。
def與方法
def
用于定義「方法(Method)」对人。例如谣殊,max
定義了一個(Int, Int)Int
的方法,它表示max
是一個參數(shù)類型為(Int, Int)
牺弄,返回值類型為Int
的方法定義姻几。
def max(x: Int, y: Int): Int = if (x > y) x else y
當使用def
定義方法時,其方法體并沒有立即被求值势告。但是蛇捌,每當調(diào)用一次max
,方法體將被重復地被求值咱台。
返回函數(shù)
可以將上例max
方法進行變換络拌,使其返回(Int, Int) => Int
的函數(shù)值。
def max = (x: Int, y: Int) => if (x > y) x else y
此時吵护,max
定義了一個方法盒音,但省略了參數(shù)列表表鳍,其返回值類型為(Int, Int) => Int
。它等價于
def max() = (x: Int, y: Int) => if (x > y) x else y
因為max
是一個「無副作用」的方法祥诽,按照慣例譬圣,可以略去「空參數(shù)列表」,即省略max
后面的小括號()
雄坪。一則對外聲明無副作用的語義厘熟,二則使代碼更加簡明扼要。
方法與函數(shù)
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)」维哈,但后者返回了一個函數(shù)(Function)類型绳姨。因此,后者常常也被習慣地稱為「函數(shù)(Function)」阔挠。
首先飘庄,它們兩者可以具有相同的調(diào)用形式:max(1, 2)
。但對于后者购撼,調(diào)用過程實際上包括了兩個子過程跪削。
- 首先調(diào)用
max
返回(Int, Int) => Int
的實例; - 然后再在該函數(shù)的實例上調(diào)用
apply
方法迂求,它等價于:
max.apply(1, 2)
其次碾盐,兩者獲取函數(shù)值的方式不同。后者可以直接獲取到函數(shù)值揩局,而對于前者需要執(zhí)行η
擴展才能取得等價的部分應用函數(shù)毫玖。
val f = max _
此時,f
也轉(zhuǎn)變?yōu)?code>(Int, Int) => Int的函數(shù)類型了凌盯。實施上付枫,對于上例,η
擴展的過程類似于如下試下十气。
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
引用變量可以隨時改變?nèi)ヒ闷渌膶ο蟆?/p>
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
定義值绩郎。對于「返回函數(shù)值的方法」與「直接使用val
定義的函數(shù)值」之間存在微妙的差異潘鲫,即使它們都定義了相同的邏輯。例如:
val max = (x: Int, y: Int) => if (x > y) x else y
def max = (x: Int, y: Int) => if (x > y) x else y
語義差異
雖然兩者之間僅存在一字之差肋杖,但卻存在本質(zhì)的差異溉仑。
-
def
用于定義「方法」,而val
用于定義「值」状植。 -
def
定義的方法時浊竟,方法體并未被立即求值;而val
在定義時津畸,其引用的對象就被立即求值了振定。 -
def
定義的方法,每次調(diào)用方法體就被求值一次肉拓;而val
僅在定義變量時僅求值一次后频。
例如,每次使用val
定義的max
帝簇,都是使用同一個函數(shù)值徘郭;也就是說靠益,如下語句為真丧肴。
max eq max // true
而每次使用def
定義的max
,都將返回不同的函數(shù)值胧后;也就是說芋浮,如下語句為假。
max eq max // false
其中壳快,eq
通過比較對象id
實現(xiàn)比較對象間的同一性的纸巷。
類型參數(shù)
val
代表了一種餓漢求值的思維,而def
代表了一種惰性求值的思維眶痰。但是瘤旨,def
具有更好可擴展性,因為它可以支持類型參數(shù)竖伯。
def max[T : Ordering](x: T, y: T): T = Ordering[T].max(x, y)
lazy惰性
def
在定義方法時并不會產(chǎn)生實例存哲,但在每次方法調(diào)用時生成不同的實例;而val
在定義變量時便生成實例七婴,以后每次使用val
定義的變量時祟偷,都將得到同一個實例。
lazy
的語義介于def
與val
之間打厘。首先修肠,lazy val
與val
語義類似,用于定義「值(value)」户盯,包括函數(shù)值嵌施。
lazy val max = (x: Int, y: Int) => if (x > y) x else y
其次饲化,它又具有def
的語義,它不會在定義max
時就完成求值吗伤。但是滓侍,它與def
不同,它會在第一次使用max
時完成值的定義牲芋,對于以后再次使用max
將返回相同的函數(shù)值撩笆。
參數(shù)傳遞
Scala
存在兩種參數(shù)傳遞的方式。
- Pass-by-Value:按值傳遞
- Pass-by-Name:按名傳遞
按值傳遞
默認情況下缸浦,Scala
的參數(shù)是按照值傳遞的夕冲。
def and(x: Boolean, y: Boolean) = x && y
對于如下調(diào)用語句:
and(false, s.contains("horance"))
表達式s.contains("horance")
首先會被立即求值,然后才會傳遞給參數(shù)y
裂逐;而在and
函數(shù)體內(nèi)再次使用y
時歹鱼,將不會再對s.contains("horance")
表達式求值,直接獲取最先開始被求值的結果卜高。
傳遞函數(shù)
將上例and
實現(xiàn)修改一下弥姻,讓其具有函數(shù)類型的參數(shù)。
def and(x: () => Boolean, y: () => Boolean) = x() && y()
其中掺涛,() => Boolean
等價于Function0[Boolean]
庭敦,表示參數(shù)列表為空,返回值為Boolean
的函數(shù)類型薪缆。
調(diào)用方法時秧廉,傳遞參數(shù)必須顯式地加上() =>
的函數(shù)頭。
and(() => false, () => s.contains("horance"))
此時拣帽,它等價于如下實現(xiàn):
and(new Function0[Boolean] {
def apply(): Boolean = false
}, new Function0[Boolean] {
def apply(): Boolean = s.contains("horance")
}
此時疼电,and
方法將按照「按值傳遞」將Function0
的兩個對象引用分別傳遞給了x
與y
的引用變量。但時减拭,此時它們函數(shù)體蔽豺,例如s.contains("horance")
,在參數(shù)傳遞之前并沒有被求值拧粪;直至在and
的方法體內(nèi)修陡,x
與y
調(diào)用了apply
方法時才被求值。
也就是說既们,and
方法可以等價實現(xiàn)為:
def and(x: () => Boolean, y: () => Boolean) = x.apply() && y.apply()
按名傳遞
通過Function0[R]
的參數(shù)類型濒析,在傳遞參數(shù)前實現(xiàn)了延遲初始化的技術。但實現(xiàn)中啥纸,參數(shù)傳遞時必須構造() => R
的函數(shù)值号杏,并在調(diào)用點上顯式地加上()
完成apply
方法的調(diào)用,存在很多的語法噪聲。
因此盾致,Scala
提供了另外一種參數(shù)傳遞的機制:按名傳遞主经。按名傳遞略去了所有()
語法噪聲。例如庭惜,函數(shù)實現(xiàn)中罩驻,x
與y
不用顯式地加上()
便可以完成調(diào)用。
def and(x: => Boolean, y: => Boolean) = x && y
其次护赊,調(diào)用點用戶無需構造() => R
的函數(shù)值惠遏,但它卻擁有延遲初始化的功效。
and(false, s.contains("horance"))
借貸模式
資源回收是計算機工程實踐中一項重要的實現(xiàn)模式骏啰。對于具有GC
的程序設計語言节吮,它僅僅實現(xiàn)了內(nèi)存資源的自動回收,而對于諸如文件IO
判耕,數(shù)據(jù)庫連接透绩,Socket
連接等資源需要程序員自行實現(xiàn)資源的回收。
該問題可以形式化地描述為:給定一個資源R
壁熄,并將資源傳遞給用戶空間帚豪,并回調(diào)算法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.
因此狸臣,該實現(xiàn)模式也常常被稱為「借貸模式」,是保證資源自動回收的重要機制方仿。本文通過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
會將文件句柄回調(diào)給用戶空間,用戶實現(xiàn)文件的逐行讀热㈣搿贾节;當讀取完成后,using
自動關閉文件句柄衷畦,釋放資源栗涂,但用戶無需關心這個細節(jié)。
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
的調(diào)用點。
對于本例棍弄,using(Source.fromFile(source))
語句中望薄,Source.fromFile(source)
并沒有馬上發(fā)生調(diào)用并傳遞給形參,而將求值推延至source = resource
語句呼畸。