從Java走進(jìn)Scala:Scala控制結(jié)構(gòu)內(nèi)部揭密
迄今為止,在此系列中,我們已經(jīng)討論了 Scala 對(duì)生態(tài)環(huán)境的保真度,展示了 Scala 如何將眾多的 Java 核心對(duì)象功能合并在一起。如果 Scala 只是編寫對(duì)象的另一種方式,那么它不會(huì)有任何引人注意的地方,或者說(shuō)不再那么功能強(qiáng)大。Scala 的函數(shù)概念和對(duì)象概念的合并,以及它對(duì)編程人員效率的重視,這些使得學(xué)習(xí) Scala 語(yǔ)言比 Java-cum-Scala 編程人員所想象的體驗(yàn)更加復(fù)雜、更加微妙。
例如,對(duì)控制結(jié)構(gòu)(比如 if、while 和 for)使用 Scala 的方法。盡管這些控制結(jié)構(gòu)看起來(lái)類似一些老的、還比較不錯(cuò)的 Java 結(jié)構(gòu),但實(shí)際上 Scala 為它們?cè)黾恿艘恍┩耆煌奶匦?。本月的文章是關(guān)于使用 Scala 控制結(jié)構(gòu)時(shí)能夠期望獲得哪些東西的入門級(jí)讀物,而不是在制造許多錯(cuò)誤(并編寫一堆錯(cuò)誤代碼)之后,讓您冒著遭受挫折的風(fēng)險(xiǎn)去尋找差異。
修訂后的 Person.scala
在 本系列的上一篇文章 中,可以了解到 Scala 能夠通過(guò)定義一些方法來(lái)定義 POJO,這些方法模仿基于 POJO 的環(huán)境所需的傳統(tǒng) “getter 和 setter”。在這篇文章發(fā)表之后,我收到了 Bill Venners 發(fā)來(lái)的電子郵件,Bill Venners 是即將發(fā)表的正式的 Scala 參考資料使用 Scala 編程(請(qǐng)參閱 參考資料)的合著者之一。Bill 指出了實(shí)現(xiàn)上述操作的一個(gè)更簡(jiǎn)單的方法,即使用 scala.reflect.BeanProperty 標(biāo)注,如下所示:
清單 1. 修改后的 Person.scala
- class Person(fn:String, ln:String, a:Int)
- {
- @scala.reflect.BeanProperty
- var firstName = fn
- @scala.reflect.BeanProperty
- var lastName = ln
- @scala.reflect.BeanProperty
- var age = a
- override def toString =
- "[Person firstName:" + firstName + " lastName:" + lastName +
- " age:" + age + " ]"
- }
清單 1 中的方法(上一篇文章 中的清單 13 的修訂版)為指定的 var 生成了 get/set 方法對(duì)。惟一的缺陷是這些方法并不實(shí)際存在于 Scala 代碼中,因此其他 Scala 代碼無(wú)法調(diào)用它們。這通常不是什么大問(wèn)題,因?yàn)?Scala 將對(duì)為自己生成的字段使用已生成的方法;如果事先不知道,那么這些對(duì)您而言可能是一個(gè)驚喜。
在查看了清單 1 中的代碼之后,最讓我感到震動(dòng)的是,Scala 并沒(méi)有只演示組合函數(shù)概念和對(duì)象概念的強(qiáng)大威力,它還演示了自 Java ***發(fā)布之后的 30 年里對(duì)象語(yǔ)言帶來(lái)的一些益處。
控制是一種幻想
您將看到的許多奇怪的、不可思議的東西都可以歸功于 Scala 的函數(shù)特性,因此,簡(jiǎn)單介紹一下函數(shù)語(yǔ)言開(kāi)發(fā)和演變的背景可能非常有用。
在函數(shù)語(yǔ)言中,將越來(lái)越高級(jí)的結(jié)構(gòu)直接構(gòu)建到語(yǔ)言中是不常見(jiàn)的。此外,語(yǔ)言是通過(guò)一組核心原語(yǔ)結(jié)構(gòu)定義的。在與將函數(shù)作為對(duì)象傳遞的功能結(jié)合之后,可用來(lái)定義功能的高階函數(shù) 看起來(lái) 像是超出了核心語(yǔ)言的范圍,但實(shí)際上它只是一個(gè)庫(kù)。類似于任何庫(kù),此功能可以替換、擴(kuò)充或擴(kuò)展。
根據(jù)一組核心原語(yǔ)構(gòu)建語(yǔ)言的合成 特性由來(lái)已久,可以追溯到 20 世紀(jì) 60 年代和 70 年代使用 Smalltalk、Lisp 和 Scheme 的時(shí)候。諸如 Lisp 和 Scheme 之類的語(yǔ)言因?yàn)樗鼈冊(cè)诟图?jí)別的抽象上定義更高級(jí)別抽象的能力而受到人們的狂熱追捧。編程人員可以使用高級(jí)抽象,用它們構(gòu)建更高級(jí)的抽象。如今聽(tīng)到討論這個(gè)過(guò)程時(shí),它通常是關(guān)于特定于域的語(yǔ)言(或 DSL)的(請(qǐng)參閱 參考資料)。實(shí)際上,它只是關(guān)于如何在抽象之上構(gòu)建抽象的過(guò)程。
在 Java 語(yǔ)言中,惟一選擇就是利用 API 調(diào)用完成此操作;在 Scala 中,可以通過(guò)擴(kuò)展語(yǔ)言本身實(shí)現(xiàn)它。試圖擴(kuò)展 Java 語(yǔ)言會(huì)帶來(lái)創(chuàng)建極端場(chǎng)景(corner case)的風(fēng)險(xiǎn),這些場(chǎng)景將威脅全局的穩(wěn)定性。而試圖擴(kuò)展 Scala 則只意味著創(chuàng)建一個(gè)新庫(kù)。
#p#
If 結(jié)構(gòu)
我們將從傳統(tǒng)的 if 結(jié)構(gòu)開(kāi)始 —— 當(dāng)然,此結(jié)構(gòu)必須是最容易處理的結(jié)構(gòu)之一,不是嗎?畢竟,從理論上說(shuō),if 只檢查一個(gè)條件。如果條件為真,則執(zhí)行后面跟著的代碼。
但是,這種簡(jiǎn)單性可能帶有欺騙性。傳統(tǒng)上,Java 語(yǔ)言對(duì) if 的 else 子句的使用是隨意的,并且假定如果條件出錯(cuò),可以只跳過(guò)代碼塊。但在函數(shù)語(yǔ)句中,情況不是這樣。為了保持函數(shù)語(yǔ)句的算術(shù)特性,所有一切都必須以表達(dá)式計(jì)算的方式出現(xiàn),包括 if 子句本身(對(duì)于 Java 開(kāi)發(fā)人員,這正是三元操作符 —— ?: 表達(dá)式 —— 的工作方式)。
在 Scala 中,非真代碼塊(代碼塊的 else 部分)必須以與 if 代碼塊中值種類相同的形式呈現(xiàn),并且必須產(chǎn)生同一種類的值。這意味著不論以何種方式執(zhí)行代碼,總會(huì)產(chǎn)生一個(gè)值。例如,請(qǐng)參見(jiàn)以下 Java 代碼:
清單 2. 哪個(gè)配置文件?(Java 版)
- // This is Java
- String filename = "default.properties";
- if (options.contains("configFile"))
- filename = (String)options.get("configFile");
因?yàn)?Scala 中的 if 結(jié)構(gòu)自身就是一個(gè)表達(dá)式,所以重寫上述代碼會(huì)使它們成為清單 3 中所示的更正確的代碼片段:
清單 3. 哪個(gè)配置文件?(Scala 版)
- // This is Scala
- val filename =
- if (options.contains("configFile"))
- options.get("configFile")
- else
- "default.properties"
也就是說(shuō),Scala 編程人員通常應(yīng)該*** val 結(jié)構(gòu),并在明確需要可變性的時(shí)候選擇 var。原因很簡(jiǎn)單:除了使編程更容易之外,val 還能確保程序的線程安全性,Scala 中的一個(gè)內(nèi)在主題是:幾乎每次認(rèn)為需要可變狀態(tài)時(shí),其實(shí)都不需要可變狀態(tài)。讓我們從不可變字段和本地變量(val)開(kāi)始,這是展示上述情況的一種方法,甚至對(duì)最堅(jiān)定的 Java 懷疑論者也是如此。從 Java 中的 final 開(kāi)始介紹可能不是很合理,或許是因?yàn)?Java 的非函數(shù)特性,盡管此原因不可取。一些好奇的 Java 開(kāi)發(fā)人員可能想嘗試一下。
盡管真正的贏家是 Scala,但可以通過(guò)編寫代碼將結(jié)果分配給 val,而不是 var。在設(shè)置之后,就無(wú)法對(duì) val 進(jìn)行更改,這與 Java 語(yǔ)言中 final 變量的操作方式是相同的。不可變本地變量最顯著的副作用是很容易實(shí)現(xiàn)并發(fā)性。試圖用 Java 代碼實(shí)現(xiàn)同樣的操作時(shí),會(huì)帶來(lái)許多不錯(cuò)的、易讀的好代碼,如清單 4 中所示:
清單 4. 哪個(gè)配置文件?(Java 版,三元式)
- //This is Java
- final String filename =
- options.contains("configFile") ?
- options.get("configFile") : "default.properties";
用代碼評(píng)審解釋這一點(diǎn)可能需要點(diǎn)技巧。也許這樣做是正確的,但許多 Java 編程人員會(huì)不以為然并且詢問(wèn) “您做那個(gè)干什么”?
val 與 var
您可能想更多地了解 val 與 var 之間的不同,實(shí)際上,它們的不同之處在于 —— 一個(gè)是只讀的值,另一個(gè)是可變的變量。通常,函數(shù)語(yǔ)言,特別是被認(rèn)為是 “純” 函數(shù)語(yǔ)言(不允許帶有副作用,比如可變狀態(tài))的那些函數(shù)語(yǔ)言,只支持 val 概念;但是,因?yàn)?Scala 要同時(shí)吸引函數(shù)編程人員和命令/對(duì)象編程人員,所以這二種結(jié)構(gòu)它都提供。
已公開(kāi)的 while 結(jié)構(gòu)
接下來(lái),讓我們來(lái)看一下 while 及其同胞 do-while。它們做的基本上是同一件事:測(cè)試一個(gè)條件,如果該條件為真,則繼續(xù)執(zhí)行提供的代碼塊。
通常,函數(shù)語(yǔ)言會(huì)避開(kāi) while 循環(huán),因?yàn)?while 實(shí)現(xiàn)的大多數(shù)操作都可以使用遞歸來(lái)完成。函數(shù)語(yǔ)言真地非常類似于 遞歸。例如,可以考慮一下 “Scala by Example”(請(qǐng)參閱 參考資料)中展示的 quicksort 實(shí)現(xiàn),該實(shí)現(xiàn)可以與 Scala 實(shí)現(xiàn)一起使用:
清單 5. Quicksort(Java 版)
- //This is Java
- void sort(int[] xs) {
- sort(xs, 0, xs.length -1 );
- }
- void sort(int[] xs, int l, int r) {
- int pivot = xs[(l+r)/2];
- int a = l; int b = r;
- while (a <= b)
- while (xs[a] < pivot) { a = a + 1; }
- while (xs[b] > pivot) { b = b – 1; }
- if (a <= b) {
- swap(xs, a, b);
- a = a + 1;
- b = b – 1;
- }
- }
- if (l < b) sort(xs, l, b);
- if (b < r) sort(xs, a, r);
- }
- void swap(int[] arr, int i, int j) {
- int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
- }
不必深入太多的細(xì)節(jié),就可以了解 while 循環(huán)的用法,它是通過(guò)數(shù)組中的各種元素進(jìn)行迭代的,先找到一個(gè)支點(diǎn),然后依次對(duì)每個(gè)子元素進(jìn)行排序。毫不令人奇怪的是,while 循環(huán)也需要一組可變本地變量,在這里,這些變量被命名為 a 和 b,其中存儲(chǔ)的是當(dāng)前支點(diǎn)。注意,此版本甚至可以在循環(huán)自身中使用遞歸,兩次調(diào)用循環(huán)本身,一次用于對(duì)列表左手邊的內(nèi)容進(jìn)行排序,另一次對(duì)列表右手邊的內(nèi)容進(jìn)行排序。
這足以說(shuō)明清單 5 中的 quicksort 真的不太容易讀取,更不用說(shuō)理解它。現(xiàn)在來(lái)考慮一下 Scala 中的直接 等同物(這意味著該版本與上述版本盡量接近):
清單 6. Quicksort(Scala 版)
- //This is Scala
- def sort(xs: Array[Int]) {
- def swap(i: Int, j: Int) {
- val t = xs(i); xs(i) = xs(j); xs(j) = t
- }
- def sort1(l: Int, r: Int) {
- val pivot = xs((l + r) / 2)
- var i = l; var j = r
- while (i <= j) {
- while (xs(i) < pivot) i += 1
- while (xs(j) > pivot) j -= 1
- if (i <= j) {
- swap(i, j)
- i += 1
- j -= 1
- }
- }
- if (l < j) sort1(l, j)
- if (j < r) sort1(i, r)
- }
- sort1(0, xs.length 1)
- }
清單 6 中的代碼看起來(lái)非常接近于 Java 版。也就是說(shuō),該代碼很長(zhǎng),很難看,并且難以理解(特別是并發(fā)性那一部分),明顯不具備 Java 版的一些優(yōu)點(diǎn)。
所以,我將其改進(jìn)……
清單 7. Quicksort(更好的 Scala 版)
- //This is Scala
- def sort(xs: Array[Int]): Array[Int] =
- if (xs.length <= 1) xs
- else {
- val pivot = xs(xs.length / 2)
- Array.concat(
- sort(xs filter (pivot >)),
- xs filter (pivot ==),
- sort(xs filter (pivot <)))
- }
顯然,清單 7 中的 Scala 代碼更簡(jiǎn)單一些。注意遞歸的使用,避免完全 while 循環(huán)。可以對(duì) Array 類型使用 filter 函數(shù),從而對(duì)其中的每個(gè)元素應(yīng)用 “greater-than”、“equals” 和 “l(fā)ess-than” 函數(shù)。事實(shí)上,在引導(dǎo)裝入程序之后,因?yàn)?if 表達(dá)式是返回某個(gè)值的表達(dá)式,所以從 sort() 返回的是 sort() 的定義中的(單個(gè))表達(dá)式。
簡(jiǎn)言之,我已經(jīng)將 while 循環(huán)的可變狀態(tài)完全再次分解為傳遞給各種 sort() 調(diào)用的參數(shù) —— 許多 Scala 狂熱愛(ài)好者認(rèn)為這是編寫 Scala 代碼的正確方式。
可能值得一提的是,Scala 本身并不介意您是否使用 while 代替迭代 —— 您會(huì)看到來(lái)自編譯器的 “您在干什么,在做蠢事嗎?” 的警告。Scala 也不會(huì)阻止您在可變狀態(tài)下編寫代碼。但是,使用 while 或可變狀態(tài)意味著犧牲 Scala 語(yǔ)言的另一個(gè)關(guān)鍵方面,即鼓勵(lì)編寫具有良好并行性的代碼。只要有可能并且可行,“Scala 式作風(fēng)” 會(huì)建議您優(yōu)先在命令塊上執(zhí)行遞歸。
#p#
編寫自己的語(yǔ)言結(jié)構(gòu)
我想走捷徑來(lái)討論一下 Scala 的控制結(jié)構(gòu),做一些大多數(shù) Java 開(kāi)發(fā)人員根本無(wú)法相信的事 —— 創(chuàng)建自己的語(yǔ)言結(jié)構(gòu)。
那些通過(guò)死讀書學(xué)習(xí)語(yǔ)言的書呆子會(huì)發(fā)現(xiàn)一件有趣的事:while 循環(huán)(Scala 中的一個(gè)原語(yǔ)結(jié)構(gòu))可能只是一個(gè)預(yù)定義函數(shù)。Scala 文檔以及假設(shè)的 “While” 定義中對(duì)此進(jìn)行了解釋說(shuō)明:
- // This is Scala
- def While (p: => Boolean) (s: => Unit) {
- if (p) { s ; While(p)(s) }
- }
上述語(yǔ)句指定了一個(gè)表達(dá)式,該表達(dá)式產(chǎn)生了一個(gè)布爾值和一個(gè)不返回任何結(jié)果的代碼塊(Unit),這正是 while 所期望的。
擴(kuò)展這些代碼行很容易,并且可以根據(jù)需要使用它們,只需導(dǎo)入正確的庫(kù)即可。正如前面提到的,這是構(gòu)建語(yǔ)言的綜合方法。在下一節(jié)介紹 try 結(jié)構(gòu)的時(shí)候,請(qǐng)將這一點(diǎn)牢記于心。
再三嘗試
try 結(jié)構(gòu)允許編寫如下所示代碼:
清單 8. 如果最初沒(méi)有獲得成功……
- // This is Scala
- val url =
- try {
- new URL(possibleURL)
- }
- catch {
- case ex: MalformedURLException =>
- new URL("www.tedneward.com")
- }
清單 8 中的代碼與 清單 2 或 清單 3 中 if 示例中的代碼相差甚遠(yuǎn)。實(shí)際上,它比使用傳統(tǒng) Java 代碼編寫更具技巧,特別是在您想捕獲不可變位置上存儲(chǔ)的值的時(shí)候(正如我在 清單 4 中最后一個(gè)示例中所做的那樣)。這是 Scala 的函數(shù)特性的又一個(gè)優(yōu)點(diǎn)!
清單 8 中所示的 case ex: 語(yǔ)法是另一個(gè) Scala 結(jié)構(gòu)(匹配表達(dá)式)的一部分,該表達(dá)式用于 Scala 中的模式匹配。我們將研究模式匹配,這是函數(shù)語(yǔ)言的一個(gè)常見(jiàn)特性,稍后將介紹它;現(xiàn)在,只把它看作一個(gè)將用于 switch/case 的概念,那么哪種 C 風(fēng)格的 struct 將用于類呢?
現(xiàn)在,再來(lái)考慮一下異常處理。眾所周知,Scala 支持異常處理是因?yàn)樗且粋€(gè)表達(dá)式,但開(kāi)發(fā)人員想要的是處理異常的標(biāo)準(zhǔn)方法,并不僅僅是捕獲異常的能力。在 AspectJ 中,是通過(guò)創(chuàng)建方面(aspect)來(lái)實(shí)現(xiàn)這一點(diǎn)的,這些方面圍繞代碼部分進(jìn)行聯(lián)系,它們是通過(guò)切入點(diǎn)定義的,如果想讓數(shù)據(jù)庫(kù)的不同部分針對(duì)不同種類異常采取不同行為,那么必須小心編寫這些切入點(diǎn) —— SQLExceptions 的處理應(yīng)該不同于 IOExceptions 的處理,依此類推。
在 Scala 中,這只是微不足道的細(xì)節(jié)。請(qǐng)留神觀察!
清單 9. 一個(gè)自定義異常表達(dá)式
- // This is Scala
- object Application
- {
- def generateException()
- {
- System.out.println("Generating exception...");
- throw new Exception("Generated exception");
- }
- def main(args : Array[String])
- {
- tryWithLogging // This is not part of the language
- {
- generateException
- }
- System.out.println("Exiting main()");
- }
- def tryWithLogging (s: => _) {
- try {
- s
- }
- catch {
- case ex: Exception =>
- // where would you like to log this?
- // I choose the console window, for now
- ex.printStackTrace()
- }
- }
- }
與前面討論過(guò)的 While 結(jié)構(gòu)類似,tryWithLogging 代碼只是來(lái)自某個(gè)庫(kù)的函數(shù)調(diào)用(在這里,是來(lái)自同一個(gè)類)??梢栽谶m當(dāng)?shù)牡胤绞褂貌煌闹黝}變量,不必編寫復(fù)雜的切入點(diǎn)代碼。
此方法的優(yōu)點(diǎn)在于它利用了 Scala 的捕獲一級(jí)結(jié)構(gòu)中橫切邏輯的功能 —— 以前只有面向方面的人才能對(duì)此進(jìn)行聲明。清單 9 中的一級(jí)結(jié)構(gòu)捕獲了一些異常(經(jīng)過(guò)檢查的和未經(jīng)檢查的都包括)并以特定方式進(jìn)行處理。上述想法的副作用非常多,惟一的限制也許就是想象力了。您只需記得 Scala 像許多函數(shù)語(yǔ)言一樣允許使用代碼塊(aka 函數(shù))作為參數(shù)并根據(jù)需要使用它們即可。
"for" 生成語(yǔ)言
所有這些都引導(dǎo)我們來(lái)到了 Scala 控制結(jié)構(gòu)套件的實(shí)際動(dòng)力源泉:for 結(jié)構(gòu)。該結(jié)構(gòu)看起來(lái)像是 Java 的增強(qiáng) for 循環(huán)的簡(jiǎn)單早期版,但它遠(yuǎn)比一般的 Java 編程人員開(kāi)始設(shè)想的更強(qiáng)大。
讓我們來(lái)看一下 Scala 如何處理集合上的簡(jiǎn)單順序迭代,根據(jù)您的 Java 編程經(jīng)驗(yàn),我想您應(yīng)該非常清楚該怎么做:
清單 10. 對(duì)一個(gè)對(duì)象使用 for 循環(huán)和對(duì)所有對(duì)象使用 for 循環(huán)
- // This is Scala
- object Application
- {
- def main(args : Array[String])
- {
- for (i <- 1 to 10) // the left-arrow means "assignment" in Scala
- System.out.println("Counting " + i)
- }
- }
此代碼所做的正如您期望的那樣,循環(huán) 10 次,并且每次都輸出一些值。需要小心的是:表達(dá)式 “1 to 10” 并不意味著 Scala 內(nèi)置了整數(shù)感知(awareness of integer)以及從 1 到 10 的計(jì)數(shù)方式。從技術(shù)上說(shuō),這里存在一些更微妙的地方:編譯器使用 Int 類型上定義的方法 to 生成一個(gè) Range 對(duì)象(Scala 中的任何東西都是對(duì)象,還記得嗎?),該對(duì)象包含要迭代的元素。如果用 Scala 編譯器可以看見(jiàn)的方式重新編寫上述代碼,那么該代碼看起來(lái)很可能如下所示:
清單 11. 編譯器看見(jiàn)的內(nèi)容
- // This is Scala
- object Application
- {
- def main(args : Array[String])
- {
- for (i <- 1.to(10)) // the left-arrow means "assignment" in Scala
- System.out.println("Counting " + i)
- }
- }
實(shí)際上,Scala 的 for 并不了解那些成員,并且并不比其他任何對(duì)象類型做得更好。它所了解的是 scala.Iterable,scala.Iterable 定義了在集合上進(jìn)行迭代的基本行為。提供 Iterable 功能(從技術(shù)上說(shuō),它是 Scala 中的一個(gè)特征,但現(xiàn)在將它視為一個(gè)接口)的任何東西都可以用作 for 表達(dá)式的核心。List、Array,甚至是您自己的自定義類型,都可以在 for 中使用。
#p#
讓 Scala 與英語(yǔ)更接近
您可能已經(jīng)注意到,理解清單 11 中的 Scala 的 for 循環(huán)版本更容易一些。這要感謝 Range 對(duì)象暗中將兩端都包含在內(nèi),以下英語(yǔ)語(yǔ)言語(yǔ)法比 Java 語(yǔ)言更接近些。假如有一條 Range 語(yǔ)句說(shuō) “from 1 to 10, do this”,那么這意味著不再產(chǎn)生意外的 off-by-one 錯(cuò)誤。
特殊性
正如上面已經(jīng)證明的那樣,for 循環(huán)可以做許多事情,并不只是遍歷可迭代的項(xiàng)列表。事實(shí)上,可以使用一個(gè) for 循環(huán)在操作過(guò)程中過(guò)濾許多項(xiàng),并在每個(gè)階段都產(chǎn)生一個(gè)新列表:
清單 12. 看一看還有哪些優(yōu)點(diǎn)
- // This is Scala
- object Application
- {
- def main(args : Array[String])
- {
- for (i <- 1 to 10; i % 2 == 0)
- System.out.println("Counting " + i)
- }
- }
注意到清單 12 中 for 表達(dá)式的第二個(gè)子句了嗎?它是一個(gè)過(guò)濾器,實(shí)際上,只有那些傳遞給過(guò)濾器(即計(jì)算 true)的元素 “向前傳給” 了循環(huán)主體。在這里,只輸出了 1 到 10 的偶數(shù)數(shù)字。
并不要求 for 表達(dá)式的各個(gè)階段都成為過(guò)濾器。您甚至可以將一些完全平淡無(wú)奇的東西(從循環(huán)本身的觀點(diǎn)來(lái)看)放入管道中。例如以下代碼顯示了在下一個(gè)階段進(jìn)行計(jì)算之前的 i 的當(dāng)前值:
清單 13. 讓我如何愛(ài)上您呢?別那么冗長(zhǎng)
- // This is Scala
- object App
- {
- def log(item : _) : Boolean =
- {
- System.out.println("Evaluating " + item)
- true
- }
- def main(args : Array[String]) =
- {
- for (val i <- 1 to 10; log(i); (i % 2) == 0)
- System.out.println("Counting " + i)
- }
- }
在運(yùn)行的時(shí)候,范圍 1 到 10 中的每個(gè)項(xiàng)都將發(fā)送給 log,它將通過(guò)顯式計(jì)算每個(gè)項(xiàng)是否為 true 來(lái) “批準(zhǔn)” 每個(gè)項(xiàng)。然后,for 的第三個(gè)子句將對(duì)這些項(xiàng)進(jìn)行篩選,過(guò)濾出那些滿足是偶數(shù)的條件的元素。因此,只將偶數(shù)傳遞給了循環(huán)主體本身。
簡(jiǎn)單性
在 Scala 中,可以將 Java 代碼中復(fù)雜的一長(zhǎng)串語(yǔ)句縮短為一個(gè)簡(jiǎn)單的表達(dá)式。例如,以下是遍歷目錄查找所有 .scala 文件并顯示每個(gè)文件名稱的方法:
清單 14. Finding .scala
- // This is Scala
- object App
- {
- def main(args : Array[String]) =
- {
- val filesHere = (new java.io.File(".")).listFiles
- for (
- file <- filesHere;
- if file.isFile;
- if file.getName.endsWith(".scala")
- ) System.out.println("Found " + file)
- }
- }
這種 for 過(guò)濾很常見(jiàn)(并且在此上下文中,分號(hào)很讓人討厭),使用這種過(guò)濾是為了幫助您做出忽略分號(hào)的決定。此外,Scala 允許將上述示例中的圓括號(hào)之間的語(yǔ)句直接作為代碼塊對(duì)待:
清單 15. Finding .scala(版本 2)
- // This is Scala
- object App
- {
- def main(args : Array[String]) =
- {
- val filesHere = (new java.io.File(".")).listFiles
- for {
- file <- filesHere
- if file.isFile
- if file.getName.endsWith(".scala")
- } System.out.println("Found " + file)
- }
- }
作為 Java 開(kāi)發(fā)人員,您可能發(fā)現(xiàn)最初的圓括號(hào)加分號(hào)的語(yǔ)法更直觀一些,沒(méi)有分號(hào)的曲線括號(hào)語(yǔ)法很難讀懂。幸運(yùn)的是,這兩種句法產(chǎn)生的代碼是等效的。
一些有趣的事
在 for 表達(dá)式的子句中可以分配一個(gè)以上的項(xiàng),如清單 16 中所示。
清單 16. 名稱中有什么?
- // This is Scala
- object App
- {
- def main(args : Array[String]) =
- {
- // Note the array-initialization syntax; the type (Array[String])
- // is inferred from the initialized elements
- val names = Array("Ted Neward", "Neal Ford", "Scott Davis",
- "Venkat Subramaniam", "David Geary")
- for {
- name <- names
- firstName = name.substring(0, name.indexOf(' '))
- } System.out.println("Found " + firstName)
- }
- }
這被稱為 “中途賦值(midstream assignment)”,其工作原理如下:定義了一個(gè)新值 firstName,該值用于保存每次執(zhí)行循環(huán)后的 substring 調(diào)用的值,以后可以在循環(huán)主體中使用此值。
這還引出了嵌套 迭代的概念,所有迭代都位于同一表達(dá)式中:
清單 17. Scala grep
- // This is Scala
- object App
- {
- def grep(pattern : String, dir : java.io.File) =
- {
- val filesHere = dir.listFiles
- for (
- file <- filesHere;
- if (file.getName.endsWith(".scala") || file.getName.endsWith(".java"));
- line <- scala.io.Source.fromFile(file).getLines;
- if line.trim.matches(pattern)
- ) println(line)
- }
- def main(args : Array[String]) =
- {
- val pattern = ".*object.*"
- grep pattern new java.io.File(".")
- }
- }
在此示例中,grep 內(nèi)部的 for 使用了兩個(gè)嵌套迭代,一個(gè)在指定目錄(其中每個(gè)文件都與 file 連接在一起)中找到的所有文件上進(jìn)行迭代,另一個(gè)迭代在目前正被迭代的文件(與 line 本地變量連接在一起)中發(fā)現(xiàn)的所有行上進(jìn)行迭代。
使用 Scala 的 for 結(jié)構(gòu)可以做更多的事,但目前為止提供的示例已足以表達(dá)我的觀點(diǎn):Scala 的 for 實(shí)際上是一條管道,它在將元素傳遞給循環(huán)主體之前處理元素組成的集合,每次一個(gè)。此管道其中的一部分負(fù)責(zé)將更多的元素添加到管道中(生成器),一部分負(fù)責(zé)編輯管道中的元素(過(guò)濾器),還有一些負(fù)責(zé)處理中間的操作(比如記錄)。無(wú)論如何,Scala 會(huì)帶給您與 Java 5 中引入的 “增強(qiáng)的 for 循環(huán)” 不同的體驗(yàn)。
匹配
今天要了解的最后一個(gè) Scala 控制結(jié)構(gòu)是 match,它提供了許多 Scala 模式匹配功能。幸運(yùn)的是,模式匹配會(huì)聲明對(duì)某個(gè)值進(jìn)行計(jì)算的代碼塊。首先,將執(zhí)行代碼塊中最接近的匹配結(jié)果。因此,在 Scala 中可以包含以下代碼:
清單 18. 一個(gè)簡(jiǎn)單的匹配
- // This is Scala
- object App
- {
- def main(args : Array[String]) =
- {
- for (arg <- args)
- arg match {
- case "Java" => println("Java is nice...")
- case "Scala" => println("Scala is cool...")
- case "Ruby" => println("Ruby is for wimps...")
- case _ => println("What are you, a VB programmer?")
- }
- }
- }
剛開(kāi)始您可能將 Scala 模式匹配設(shè)想為支持 String 的 “開(kāi)關(guān)’,帶有通常用作通配符的下劃線字符,而這正是典型開(kāi)關(guān)中的默認(rèn)情況。但是,這樣想會(huì)極大地低估該語(yǔ)言。模式匹配是許多(但不是大多數(shù))函數(shù)語(yǔ)言中可以找到的另一個(gè)特性,它提供了一些有用的功能。
對(duì)于初學(xué)者(盡管這沒(méi)什么好奇怪的),可能認(rèn)為 match 表達(dá)式自身會(huì)產(chǎn)生一個(gè)值,該值可能出現(xiàn)在賦值語(yǔ)句的右邊,正如 if 和 try 語(yǔ)句所做的那樣。這一點(diǎn)本身也很有用,但匹配的真正威力體現(xiàn)在基于各種類型進(jìn)行匹配時(shí),而不是如上所述匹配單個(gè)類型的值,或者更多的時(shí)候,它是兩種匹配的組合。
因此,假設(shè)您有一個(gè)聲明返回 Object 的函數(shù)或方法 —— 在這里,Java 的 java.lang.reflect.Method.invoke() 方法的結(jié)果可能是一個(gè)好例子。通常,在使用 Java 語(yǔ)言計(jì)算結(jié)果時(shí),首先應(yīng)該確定其類型;但在 Scala 中,可以使用模式匹配簡(jiǎn)化該操作:
清單 19. 您是什么?
- //This is Scala
- object App
- {
- def main(args : Array[String]) =
- {
- // The Any type is exactly what it sounds like: a kind of wildcard that
- // accepts any type
- def describe(x: Any) = x match {
- case 5 => "five"
- case true => "truth"
- case "hello" => "hi!"
- case Nil => "the empty list"
- case _ => "something else"
- }
- println describe(5)
- println describe("hello")
- }
- }
因?yàn)?match 的很容易簡(jiǎn)單明了地描述如何針對(duì)各種值和類型進(jìn)行匹配的能力,模式匹配常用于解析器和解釋器中,在那里,解析流中的當(dāng)前標(biāo)記是與一系列可能的匹配子句匹配的。然后,將針對(duì)另一系列子句應(yīng)用下一個(gè)標(biāo)記,依此類推(注意,這也是使用函數(shù)語(yǔ)言編寫許多語(yǔ)言解析器、編譯器和其他與代碼有關(guān)的工具的部分原因,這些函數(shù)語(yǔ)言中包括 Haskell 或 ML)。
關(guān)于模式匹配,還有許多可說(shuō)的東西,但這些會(huì)將我們直接引導(dǎo)至 Scala 的另一個(gè)特性 case 類,我想將它留到下次再介紹。
結(jié)束語(yǔ)
Scala 在許多方面看起來(lái)都非常類似于 Java,但實(shí)際上只有 for 結(jié)構(gòu)存在一些相似性。核心語(yǔ)法元素的函數(shù)特性不僅提供了一些有用的特性(比如已經(jīng)提到的賦值功能),還提供了使用新穎有趣的方式擴(kuò)展語(yǔ)言的能力,不必修改核心 javac 編譯器本身。這使該語(yǔ)言更加符合 DSL 的定義(這些 DSL 是在現(xiàn)有語(yǔ)言的語(yǔ)法中定義的),并且更加符合編程人員根據(jù)一組核心原語(yǔ)(a la Lisp 或 Scheme)構(gòu)建抽象的愿望。
關(guān)于 Scala,有如此多的內(nèi)容可以談?wù)?,但我們這個(gè)月的時(shí)間已經(jīng)用完了。記得試用最新的 Scala bits(在撰寫本文時(shí)是 2.7.0-final)并嘗試提供的示例,感受一下該語(yǔ)言的操作(請(qǐng)參閱 參考資料)。請(qǐng)記住,到下一次的時(shí)候,Scala 會(huì)將一些有趣的(函數(shù))特性放入編程中!
【相關(guān)閱讀】