Scala參數傳遞

在抉擇的哪一刻,成敗實已露出端倪。

Scala擁有兩種參數傳遞的方式:Call-by-Value(按值傳遞)與Call-by-Name(按名傳遞)。Call-by-Value避免了參數的重復求值,效率相對較高;而Call-by-Name避免了在函數調用時刻的參數求值,而將求值推延至實際調用點,但有可能造成重復的表達式求值。

兩者存在微妙的差異,并應用于不同的場景。本文將闡述兩者之間的差異,并重點討論Call-by-Name的實現模式和應用場景。

  1. 基本概念
  • val與值
  • def與方法
  • val與var
  • val與def
  1. 參數傳遞
  • 按值傳遞
  • 按名傳遞
  1. 借貸模式

基本概念

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)。但對于后者,調用過程實際上包括了兩個子過程。

  1. 首先調用max返回(Int, Int) => Int的實例;
  2. 然后再在該函數的實例上調用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

varval都可以用于定義變量,但兩者表示不同的語義。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 

語義差異

雖然兩者之間僅存在一字之差,但卻存在本質的差異。

  1. def用于定義「方法」,而val用于定義「值」。
  2. def定義的方法時,方法體并未被立即求值;而val在定義時,其引用的對象就被立即求值了。
  3. 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的語義介于defval之間。首先,lazy valval語義類似,用于定義「值(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的兩個對象引用分別傳遞給了xy的引用變量。但時,此時它們函數體,例如s.contains("horance"),在參數傳遞之前并沒有被求值;直至在and的方法體內,xy調用了apply方法時才被求值。

也就是說,and方法可以等價實現為:

def and(x: () => Boolean, y: () => Boolean) = x.apply() && y.apply()

按名傳遞

通過Function0[R]的參數類型,在傳遞參數前實現了延遲初始化的技術。但實現中,參數傳遞時必須構造() => R的函數值,并在調用點上顯式地加上()完成apply方法的調用,存在很多的語法噪聲。

因此,Scala提供了另外一種參數傳遞的機制:按名傳遞。按名傳遞略去了所有()語法噪聲。例如,函數實現中,xy不用顯式地加上()便可以完成調用。

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文件,并傳遞給usingusing會將文件句柄回調給用戶空間,用戶實現文件的逐行讀取;當讀取完成后,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語句。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容