新聞中心
Scala 是一種基于JVM,集合了面向?qū)ο缶幊毯秃瘮?shù)式編程優(yōu)點的高級程序設(shè)計語言。在《Scala編程指南 更少的字更多的事》中我們從幾個方面見識了Scala 簡潔,可伸縮,高效的語法。我們也描述了許多Scala 的特性。本文為《Programming Scala》第三章,我們會在深入Scala 對面向?qū)ο缶幊毯秃瘮?shù)式編程的支持前,完成對Scala 本質(zhì)的講解。

成都創(chuàng)新互聯(lián)公司制作網(wǎng)站網(wǎng)頁找三站合一網(wǎng)站制作公司,專注于網(wǎng)頁設(shè)計,成都網(wǎng)站建設(shè)、成都網(wǎng)站制作,網(wǎng)站設(shè)計,企業(yè)網(wǎng)站搭建,網(wǎng)站開發(fā),建網(wǎng)站業(yè)務(wù),680元做網(wǎng)站,已為成百上千服務(wù),成都創(chuàng)新互聯(lián)公司網(wǎng)站建設(shè)將一如既往的為我們的客戶提供最優(yōu)質(zhì)的網(wǎng)站建設(shè)、網(wǎng)絡(luò)營銷推廣服務(wù)!
推薦專題:Scala編程語言
Scala 本質(zhì)
在我們深入Scala 對面向?qū)ο缶幊桃约昂瘮?shù)式編程的支持之前,讓我們來先完成將來可能在程序中用到的一些Scala 本質(zhì)和特性的討論。
操作符?操作符?
Scala 一個重要的基礎(chǔ)概念就是所有的操作符實際上都是方法??紤]下面這個最基礎(chǔ)的例子。
- // code-examples/Rounding/one-plus-two-script.scala
- 1 + 2
兩個數(shù)字之間的加號是個什么呢?是一個方法。第一,Scala 允許非字符型方法名稱。你可以把你的方法命名為+,-,$ 或者其它任何你想要的名字(譯著:后面會提到例外)。第二,這個表達式等同于1 .+(2)。(我們在1 的后面加了一個空格因為1. 會被解釋為Double 類型。)當一個方法只有一個參數(shù)的時候,Scala 允許你不寫點號和括號,所以方法調(diào)用看起來就像是操作符調(diào)用。這被稱為“中綴表示法”,也就是操作符是在實例和參數(shù)之間的。我們會很快見到很多這樣的例子。
類似的,一個沒有參數(shù)的方法也可以不用點號,直接調(diào)用。這被稱為“后綴表示法”。
Ruby 和SmallTalk 程序員現(xiàn)在應(yīng)該感覺和在家一樣親切了。因為那些語言的使用者知道,這些簡單的規(guī)則有著廣大的好處,它可以讓你用自然的,優(yōu)雅的方式來創(chuàng)建應(yīng)用程序。
那么,哪些字符可以被用在標識符里呢?這里有一個標識符規(guī)則的概括,它應(yīng)用于方法,類型和變量等的名稱。要獲取更精確的細節(jié)描述,參見[ScalaSpec2009]。Scala 允許所有可打印的ASCII 字符,比如字母,數(shù)字,下劃線,和美元符號$,除了括號類的字符,比如‘(’, ‘)’, ‘[’, ‘]’, ‘{’, ‘}’,和分隔類字符比如‘`’, ‘’’, ‘'’, ‘"’, ‘.’, ‘;’, 和 ‘,’。除了上面的列表,Scala 還允許其他在u0020 到u007F 之間的字符,比如數(shù)學(xué)符號和“其它” 符號。這些余下的字符被稱為操作符字符,包括了‘/’, ‘<’ 等。
1 不能使用保留字
正如大多數(shù)語言一樣,你不能是用保留字作為標識符。我們在《第2章 - 打更少的字,做更多的事》的“保留字” 章節(jié)列出了所有的保留字?;貞浺幌?,其中有些保留字是操作符和標點的組合。比如說,簡單的一個下劃線(‘_’) 是一個保留字!
2 普通標識符 - 字母,數(shù)字以及 ‘$’, ‘_’ , 操作符的組合
和Java 以及很多其它語言一樣,一個普通標志符可以以一個字母或者下劃線開頭,緊跟著更多的字母,數(shù)字,下劃線和美元符號。和Unicode 等同的字符也是被允許的。然而,和Java 一樣,Scala 保留了美元符號作為內(nèi)部使用,所以你不應(yīng)該在你自己的標識符里使用它。在一個下劃線之后,你可以接上字母,數(shù)字,或者一個序列的操作符字符。下劃線很重要,它告訴編譯器把后面直到空格之前所有的字符都處理為標識符。比如,val xyz__++ = 1 把值1 賦值給變量xyz__++,而表達式val xyz++= = 1卻不能通過編譯,因為這個標識符同樣可以被解釋為xyz ++=,看起來像是要把某些東西加到xyz 后面去。類似的,如果你在下劃線后接有操作符字符,你不能把它們和字母數(shù)字混合在一起。這個約束避免了像這樣的表達式的二義性:abc_=123。這是一個標識符abc_=123 還是給abc_ 賦值123 呢?
3 普通標識符 - 操作符
如果一個標識符以操作符為開頭,那么余下的所有字符都必須是操作符字符。
4 反引用字面值
一個標識符可以是兩個反單引號內(nèi)一個任意的字符串(受制于平臺的限制)。比如val `this is a valid identifier` = "Hello World!"?;貞浺幌挛覀兛梢园l(fā)現(xiàn),這個語法也是引用Java 或者.NET 的類庫中和Scala 保留字的名稱一樣的方法時候所用的方式,比如java.net.Proxy.`type`()。
5 模式匹配標識符
在模式匹配表達式中,以小寫字母開頭的標識都會被解析為變量標識符,而以大寫字母開頭的標識會被解析為常量標識符。這個限定避免了一些由于非常簡潔的變量語法而帶來的二義性,例如:不用寫val 關(guān)鍵字。
語法糖蜜
一旦你知道所有的操作符都是方法,那么理解一些不熟悉的Scala 代碼就會變的相對容易些了。你不用擔(dān)心那些充滿了新奇操作符的特殊案例。在《第1章 - 從0 分到60 分:Scala 介紹》中的“初嘗并發(fā)” 章節(jié)中,我們使用了Actor 類,你會注意到我們使用了一個驚嘆號(!)來發(fā)送消息給一個Actor?,F(xiàn)在你知道!只是另外一個方法而已,就像其它你可以用來和Actor 交互的快捷操作符一樣。類似的,Scala 的XML 庫提供了 操作符來滲入到文檔結(jié)構(gòu)中去。這些只是scala.xml.NodeSeq 類的方法而已。
靈活的方法命名規(guī)則能讓你寫出就像Scala 原生擴展一樣的庫。你可以寫一個數(shù)學(xué)類庫,處理數(shù)字類型,加減乘除以及其它常見的數(shù)學(xué)操作。你也可以寫一個新的行為類似Actors 的并發(fā)消息層。各種的可能性僅受到Scala 方法命名限制的約束。
警告
別因為你可以就覺得你應(yīng)該這么作。當用Scala 來設(shè)計你自己的庫和API 的時候,記住,晦澀的標點和操作符會難以被程序員所記住。過量使用這些操作符會導(dǎo)致你的代碼充滿難懂的噪聲。堅持已有的約定,當一個快捷符號沒有在你腦海中成型的時候,清晰地把它拼出來吧。
不用點號和括號的方法
為了促進閱讀性更加的編程風(fēng)格,Scala 在方法的括號使用上可謂是靈活至極。如果一個方法不用接受參數(shù),你可以無需括號就定義它。調(diào)用者也必須不加括號地調(diào)用它。如果你加上了空括號,那么調(diào)用者可以有選擇地加或者不加括號。例如,List 的size 方法沒有括號,所以你必須寫List(1,2,3).size。如果你嘗試寫List(1,2,3).size() 就會得到一個錯誤。然而,String 類的length 方法在定義時帶有括號,所以,"hello".length() 和"hello".length 都可以通過編譯。
Scala 社區(qū)的約定是,在沒有副作用的前提下,省略調(diào)用方法時候的空括號。所以,查詢一個序列的大?。╯ize)的時候可以不用括號,但是定義一個方法來轉(zhuǎn)換序列的元素則應(yīng)該寫上括號。這個約定給你的代碼使用者發(fā)出了一個有潛在的巧妙方法的信號。
當調(diào)用一個沒有參數(shù)的方法,或者只有一個參數(shù)的方法的時候,還可以省略點號。知道了這一點,我們的List(1,2,3).size 例子就可以寫成這樣:
- // code-examples/Rounding/no-dot-script.scala
- List(1, 2, 3) size
很整潔,但是又令人疑惑。在什么時候這樣的語法靈活性會變得有用呢?是當我們把方法調(diào)用鏈接成自表達性的,自我解釋的語“句” 的時候:
- // code-examples/Rounding/no-dot-better-script.scala
- def isEven(n: Int) = (n % 2) == 0
- List(1, 2, 3, 4) filter isEven foreach println
就像你所猜想的,運行上面的代碼會產(chǎn)生如下輸出:
- 24
Scala 這種對于方法的括號和點號不拘泥的方式為書寫域特定語言(Domain-Specific Language)定了基石。我們會在簡短地討論一下操作符優(yōu)先級之后再來學(xué)習(xí)它。
優(yōu)先級規(guī)則
那么,如果這樣一個表達式:2.0 * 4.0 / 3.0 * 5.0 實際上是Double 上的一系列方法調(diào)用,那么這些操作符的調(diào)用優(yōu)先級規(guī)則是什么呢?這里從低到高表述了它們的優(yōu)先級[ScalaSpec2009]。
◆所有字母
◆|
◆^
◆&
◆< >
◆= !
◆:
◆+ -
◆* / %
◆所有其它特殊字符
在同一行的字符擁有同樣的優(yōu)先級。一個例外是當= 作為賦值存在時,它擁有最低的優(yōu)先級。
因為* 和/ 有一樣的優(yōu)先級,下面兩行scala 對話的行為是一樣的。
- scala> 2.0 * 4.0 / 3.0 * 5.0res2: Double = 13.333333333333332
- scala> (((2.0 * 4.0) / 3.0) * 5.0)res3: Double = 13.333333333333332
在一個左結(jié)合的方法調(diào)用序列中,它們簡單地進行從左到右的綁定。你說“左綁定”?在Scala 中,任何以冒號: 結(jié)尾的方法實際上是綁定在右邊的,而其它方法則是綁定在左邊。舉例來說,你可以使用:: 方法(稱為“cons”,“constructor” 構(gòu)造器的縮寫)在一個List 前插入一個元素。
- scala> val list = List('b', 'c', 'd')
- list: List[Char] = List(b, c, d)
- scala> 'a' :: list
- res4: List[Char] = List(a, b, c, d)
第二個表達式等效于list.::(a)。在一個右結(jié)合的方法調(diào)用序列中,它們從右向左綁定。那左綁定和有綁定混合的表達式呢?
- scala> 'a' :: list ++ List('e', 'f')
- res5: List[Char] = List(a, b, c, d, e, f)
(++ 方法鏈接了兩個list。)在這個例子里,list 被加入到List(e,f) 中,然后a 被插入到前面來創(chuàng)建最后的list。通常我們最好加上括號來消除可能的不確定因素。
提示
任何名字以: 結(jié)尾的方法都向右邊綁定,而不是左邊。
最后,注意當你使用scala 命令的時候,無論是交互式還是使用腳本,看上去都好像可以在類型之外定義“全局”變量和方法。這其實是一個假象;解釋器實際上把所有定義都包含在一個匿名的類型中,然后才去生成JVM 或者.NET CLR 字節(jié)碼。
#p#
領(lǐng)域特定語言
領(lǐng)域特定語言,也稱為DSL,為特定的問題領(lǐng)域提供了一種方便的語意來表達自己的目標。比如,SQL 為處理與數(shù)據(jù)庫打交道的問題,提供了剛剛好的編程語言功能,使之成為一個領(lǐng)域特定語言。
有些DSL 像SQL 一樣是自我包含的,而今使用成熟語言來實現(xiàn)DSL 使之成為母語言的一個子集變得流行起來。這允許程序員充分利用宿主語言來涵蓋DSL 不能覆蓋到的邊緣情況,而且節(jié)省了寫詞法分析器,解析器和其它語言基礎(chǔ)的時間。
Scala 的豐富,靈活的語法使得寫DSL 輕而易舉。你可以把下面的例子看作使用Specs 庫(參見“Specs” 章節(jié))來編寫行為驅(qū)動開發(fā)[BDD] 程序的風(fēng)格。
- // code-examples/Rounding/specs-script.scala
- // Example fragment of a Specs script. Doesn't run standalone
- "nerd finder" should {
- "identify nerds from a List" in {
- val actors = List("Rick Moranis", "James Dean", "Woody Allen")
- val finder = new NerdFinder(actors)
- finder.findNerds mustEqual List("Rick Moranis", "Woody Allen")
- }
- }
注意這段代碼和英語語法的相似性:“this should test that in the following scenario(這應(yīng)該在以下場景中測試它)”,“this value must equal that value (這個值必須等于那個值)”,等等。這個例子使用了華麗的Specs 庫,它提供了一套高效的DSL 來用于行為驅(qū)動開發(fā)測試和工程方法學(xué)。通過最大化利用Scala 的自有語法和諸多方法,Specs 測試組即使對于非開發(fā)人員來說也是可讀的。
這只是對Scala 強大的DSL 的一個簡單嘗試。我們會在后面看到更多其它例子,以及在討論更高級議題的時候?qū)W習(xí)如何編寫你自己的DSL(參見《第11章 - Scala 的領(lǐng)域特定語言》)。
Scala if 指令
即使是最常見的語言特性在Scala 里也被增強了。讓我們來看看簡單的if 指令。和大多數(shù)語言一樣,Scala 的if 測試一個條件表達式,然后根據(jù)結(jié)果為真或假來跳轉(zhuǎn)到響應(yīng)語句塊中。一個簡單的例子:
- // code-examples/Rounding/if-script.scala
- if (2 + 2 == 5) {
- println("Hello from 1984.")
- } else if (2 + 2 == 3) {
- println("Hello from Remedial Math class?")
- } else {
- println("Hello from a non-Orwellian future.")
- }
在Scala 中與眾不同的是,if 和其它幾乎所有指令實際上都是表達式。所以,我們可以把一個if 表達式的結(jié)果賦值給其它(變量),像下面這個例子所展示的:
- // code-examples/Rounding/assigned-if-script.scala
- val configFile = new java.io.File(".myapprc")
- val configFilePath = if (configFile.exists()) {
- configFile.getAbsolutePath()
- } else {
- configFile.createNewFile()
- configFile.getAbsolutePath()
- }
注意, if 語句是表達式,意味著它們有值。在這個例子里,configFilePath 的值就是if 表達式的值,它處理了配置文件不存在的情況,并且返回了文件的絕對路徑。這個值現(xiàn)在可以在程序中被重用了,if 表達式的值只有在被使用到的時候才會被計算。
因為在Scala 里if 語句是一個表達式,所以就不需要C 類型子語言的三重條件表達式了。你不會在Scala 里看到x ? doThis() : doThat() 這樣的代碼。因為Scala 提供了一個即強大又更具有可讀性的機制。
如果我們在上面的例子里省略else 字句會發(fā)生什么?在scala 解釋器里輸入下面的代碼會告訴我們發(fā)生什么。
- scala> val configFile = new java.io.File("~/.myapprc")
- configFile: java.io.File = ~/.myapprc
- scala> val configFilePath = if (configFile.exists()) {
- | configFile.getAbsolutePath()
- | }
- configFilePath: Unit = ()
- scala>
注意現(xiàn)在configFilePath 是Unit 類型了。(之前是String。)類型推斷選擇了一個滿足if 表達式所有結(jié)果的類型。Unit 是唯一的可能,因為沒有值也是一個可能的結(jié)果。
Scala for 推導(dǎo)語句
Scala 另外一個擁有豐富特性的類似控制結(jié)構(gòu)是for 循環(huán),在Scala 社區(qū)中也被稱為for 推導(dǎo)語句或者for 表達式。語言的這個功能絕對對得起一個花哨的名字,因為它可以做一些很酷的戲法。
實際上,術(shù)語推導(dǎo)(comprehension)來自于函數(shù)式編程。它表達了這樣個一個觀點:我們正在遍歷某種集合,“推導(dǎo)”我們所發(fā)現(xiàn)的,然后從中計算出一些新的東西出來。
一個簡單的小狗例子
讓我們從一個基本的for 表達式開始:
- // code-examples/Rounding/basic-for-script.scala
- val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
- "Scottish Terrier", "Great Dane", "Portuguese Water Dog")
- for (breed <- dogBreeds)
- println(breed)
你可能已經(jīng)猜到了,這段代碼的意思是“對于列表dogBreeds 里面的每一個元素,創(chuàng)建一個臨時變量叫breed,并賦予這個元素的值,然后打印出來?!卑?- 操作符看作一個箭頭,引導(dǎo)集合中一個一個的元素到那個我們會在for 表達式內(nèi)部引用的局部變量中去。這個左箭頭操作符被稱為生成器,之所以這么叫是因為它從一個集合里產(chǎn)生獨立的值來給一個表達式用。
過濾器
那如果我們需要更細的粒度呢? Scala 的for 表達式通過過濾器來我們指定集合中的哪些元素是我們希望使用的。所以,要在我們的狗品種列表里找到所有的梗類犬,我們可以把上面的例子改成下面這樣:
- // code-examples/Rounding/filtered-for-script.scala
- val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
- "Scottish Terrier", "Great Dane", "Portuguese Water Dog")
- for (breed <- dogBreeds
- if breed.contains("Terrier")
- ) println(breed)
如果需要給一個for 表達式添加多于一個的過濾器,用分號隔開它們:
- // code-examples/Rounding/double-filtered-for-script.scala
- val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
- "Scottish Terrier", "Great Dane", "Portuguese Water Dog")
- for (breed <- dogBreeds
- if breed.contains("Terrier");
- if !breed.startsWith("Yorkshire")
- ) println(breed)
現(xiàn)在你已經(jīng)找到了所有不出生于約克郡的梗類犬,但愿也知道了過濾器在過程中是多么的有用。
產(chǎn)生器
如果說,你不想把過濾過的集合打印出來,而是希望把它放到程序的另外一部分去處理呢?yeild 關(guān)鍵字就是用for 表達式來生成新集合的關(guān)鍵。在下面的例子中,注意我們把for 表達式包裹在了一對大括號中,就像我們定義任何一個語句塊一樣。
提示
for 表達式可以用括號或者大括號來定義,但是使用大括號意味著你不必用分號來分割你的過濾器。大部分時間里,你會在有一個以上過濾器,賦值的時候傾向使用大括號。
- // code-examples/Rounding/yielding-for-script.scala
- val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
- "Scottish Terrier", "Great Dane", "Portuguese Water Dog")
- val filteredBreeds = for {
- breed <- dogBreeds
- if breed.contains("Terrier")
- if !breed.startsWith("Yorkshire")
- } yield breed
在for 表達式的每一次循環(huán)中,被過濾的結(jié)果都會產(chǎn)生一個名為breed 的值。這些結(jié)果會隨著每運行而累積,最后的結(jié)果集合被賦給值filteredBreeds(正如我們上面用if 指令做的那樣)。由for-yield 表達式產(chǎn)生的集合類型會從被遍歷的集合類型中推斷。在這個例子里,filteredBreeds 的類型是List[String],因為它是類型為List[String] 的dogBreeds 列表的一個子集。
擴展的作用域
Scala 的for 推導(dǎo)語句最后一個有用的特性是它有能力把在定義在for 表達式第一部分里的變量用在后面的部分里。這個例子是一個最好的說明:
- // code-examples/Rounding/scoped-for-script.scala
- val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
- "Scottish Terrier", "Great Dane", "Portuguese Water Dog")
- for {
- breed <- dogBreeds
- upcasedBreed = breed.toUpperCase()
- } println(upcasedBreed)
注意,即使沒有聲明upcaseBreed 為一個val,你也可以在你的for 表達式主體內(nèi)部使用它。這個方法對于想在遍歷集合的時候轉(zhuǎn)換元素的時候來說是很理想的。
最后,在《第13章 - 應(yīng)用程序設(shè)計》的“Options 和For 推導(dǎo)語句”章節(jié),我們會看到使用Options 和for 推導(dǎo)語句可以大大地減少不必要的“null” 和空判斷,從而減少代碼數(shù)量。
#p#
其它循環(huán)結(jié)構(gòu)
Scala 有幾種其它的循環(huán)結(jié)構(gòu)
Scala while 循環(huán)
和許多語言類似,while 循環(huán)在條件為真的時候會持續(xù)執(zhí)行一段代碼塊。例如,下面的代碼在下一個星期五,同時又是13號之前,每天打印一句抱怨的話:
- // code-examples/Rounding/while-script.scala
- // WARNING: This script runs for a LOOOONG time!
- import java.util.Calendar
- def isFridayThirteen(cal: Calendar): Boolean = {
- val dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)
- val dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)
- // Scala returns the result of the last expression in a method
- (dayOfWeek == Calendar.FRIDAY) && (dayOfMonth == 13)
- }
- while (!isFridayThirteen(Calendar.getInstance())) {
- println("Today isn't Friday the 13th. Lame.")
- // sleep for a day
- Thread.sleep(86400000)
- }
你可以在下面找到一張表,它列舉了所有在while 循環(huán)中工作的條件操作符。
Scala do-while 循環(huán)
和上面的while 循環(huán)類似,一個do-while 循環(huán)當條件表達式為真時持續(xù)執(zhí)行一些代碼。唯一的區(qū)別是do-while 循環(huán)在運行代碼塊之后才進行條件檢查。要從1 數(shù)到10,我們可以這樣寫:
- // code-examples/Rounding/do-while-script.scala
- var count = 0
- do {
- count += 1
- println(count)
- } while (count < 10)
這也展示了在Scala 中,遍歷一個集合還有一種更優(yōu)雅的方式,我們會在下一節(jié)看到。
生成器表達式
還記得我們在討論for 循環(huán)的時候箭頭操作符嗎(<-)?我們也可以讓它在這里工作。讓我們來整理一下上面的do-while 循環(huán):
- // code-examples/Rounding/generator-script.scala
- for (i <- 1 to 10)
- println(i)
這就是所有需要的了。是Scala 的RichInt(富整型)使得這個簡潔的單行代碼成為可能。編譯器執(zhí)行了一個隱式轉(zhuǎn)換,把1,一個Int (整型),轉(zhuǎn)換成了RichInt 類型。(我們會在《第7章 - Scala 對象系統(tǒng)》的“Scala 類型結(jié)構(gòu)” 章節(jié)以及《第8章 - Scala 函數(shù)式編程》的“隱式轉(zhuǎn)換” 章節(jié)中討論這些轉(zhuǎn)換。)RichInt 定義了以訛to 方法,它接受另外一個整數(shù),然后返回一個Range.Inclusive 的實例。也就是說,Inclusive 是Rang 伴生對象(Companion Object,我們在《第1章 - 從0 分到60 分:Scala 介紹》中間要介紹過,參考《第6章 - Scala 高級面向?qū)ο缶幊獭帆@取更多信息。)的一個嵌套類。類Range 的這個嵌套類繼承了一系列方法來和序列以及可迭代的數(shù)據(jù)結(jié)構(gòu)交互,包括那些在for 循環(huán)中必然會使用到的。
順便說一句,如果你想從1 數(shù)到10 但是不包括10, 你可以使用until 來代替to,比如for (i <- 0 until 10)。
這樣就一幅清晰的圖畫展示了Scala 的內(nèi)部類庫是如何結(jié)合起來形成簡單易用的語言結(jié)構(gòu)的。
注意
當和大多數(shù)語言的循環(huán)一起工作時,你可以使用break 來跳出循環(huán),或者continue 來繼續(xù)下一個迭代。Scala 沒有這兩個指令,但是當編寫地道的Scala 代碼時,它們是不必要的。你應(yīng)該使用條件表達式來測試一個循環(huán)是否應(yīng)該繼續(xù),或者利用遞歸。更好的方法是,在這之前就用過濾器來出去循環(huán)中復(fù)雜的條件狀態(tài)。然而,因為大眾需求,2.8 版本的Scala 加入了對break 的支持,不過是以庫的一個方法實現(xiàn),而不是內(nèi)建的break 關(guān)鍵字。
條件操作符
Scala 從Java 和它的前輩身上借用了絕大多數(shù)的條件操作符。你可以在下面的if 指令,while 循環(huán),以及其它任何可以運用條件判斷的地方發(fā)現(xiàn)它們。
表格 3.1. 條件操作符
| 操作符 | 操作 | 描述 |
| && | 與 | 在操作符左右的值都為真的時候結(jié)果為真。右邊的值只有在左邊的值為真的時候才會被評估(短路表達式)。 |
| || | 或 | 操作符兩邊只要有一個為真結(jié)果就為真。右邊的值只有在左邊的值為假的時候才被評估(短路表達式)。 |
| > | 大于 | 左側(cè)的值大于右側(cè)的值時結(jié)果為真。 |
| >= | 大于等于 | 左側(cè)的值大于等于右側(cè)的值時結(jié)果為真。 |
| < | 小于 | 左側(cè)的值小于右側(cè)的值時結(jié)果為真。 |
| <= | 小于等于 | 左側(cè)的值小于右側(cè)的值時結(jié)果為真。 |
| == | 等于 | 左側(cè)的值等于右側(cè)的值時結(jié)果為真。 |
| != | 不等于 | 左側(cè)的值不等于右側(cè)的值時結(jié)果為真。 |
注意&& 和|| 是“短路” 操作符。它們會在結(jié)果必然已知的情況下停止對表達式的評估。
我們會在《第6章 - Scala 高級面向?qū)ο缶幊獭返摹皩ο蟮南嗟取?章節(jié)中更深入討論對象相等性。例如,我們會看到== 在Scala 和Java 中有著不同的含義。除此以外,這些操作符大家應(yīng)該都很熟悉,所以讓我們繼續(xù)前進到一些新的,激動人心的特性上去。
#p#
模式匹配
模式匹配是從函數(shù)式語言中引入的強大而簡潔的多條件選擇跳轉(zhuǎn)方式。你也可以把模式匹配想象成你最喜歡的C 類語言的case 指令,當然是打了激素的。在典型的case 指令中,通常只允許對序數(shù)類型進行匹配,產(chǎn)生一些這樣的表達式:“在i 為5 的case 里,打印一個消息;在i 為6 的case里,離開程序?!倍辛薙cala 的模式匹配,你的case 可以包含類型,通配符,序列,甚至是對象變量的深度檢查。
一個簡單的匹配
讓我們從模擬拋硬幣匹配一個布爾值開始:
- // code-examples/Rounding/match-boolean-script.scala
- val bools = List(true, false)
- for (bool <- bools) {
- bool match {
- case true => println("heads")
- case false => println("tails")
- case _ => println("something other than heads or tails (yikes!)")
- }
- }
看起來很像C 風(fēng)格的case 語句,對吧?唯一的區(qū)別是最后一個case 使用了下劃線'_' 通配符。它匹配了所有上面的case 中沒有定義的情況,所以它和Java、C# 中的switch 指令的default 關(guān)鍵字作用相同。
模式匹配是貪婪的;只有第一個匹配的情況會贏。所以,如果你在所有case 前方一個case _ 語句,那么編譯器會在下一個條件拋出一個“無法執(zhí)行到的代碼”的錯誤,因為沒人能跨過那個default 條件。
提示
使用case _ 來作為默認的,“滿足所有”的匹配。
那如果我們希望獲得匹配的變量呢?
匹配中的變量
- // code-examples/Rounding/match-variable-script.scala
- import scala.util.Random
- val randomInt = new Random().nextInt(10)
- randomInt match {
- case 7 => println("lucky seven!")
- case otherNumber => println("boo, got boring ol' " + otherNumber)
- }
在這個例子里,我們把通配符匹配的值賦給了一個變量叫otherNumber,然后在下面的表達式中打印出來。如果我們生成了一個7,我們會對它稱頌道德。反之,我們則詛咒它讓我們經(jīng)歷了一個不幸運的數(shù)字。
類型匹配
這些例子甚至還沒有開始接觸到Scala 的模式匹配特性的最表面。讓我們來嘗試一下類型匹配:
- // code-examples/Rounding/match-type-script.scala
- val sundries = List(23, "Hello", 8.5, 'q')
- for (sundry <- sundries) {
- sundry match {
- case i: Int => println("got an Integer: " + i)
- case s: String => println("got a String: " + s)
- case f: Double => println("got a Double: " + f)
- case other => println("got something else: " + other)
- }
- }
這次,我們從一個元素為Any 類型的List 中拉出所有元素,包括了String,Double,Int,和Char。對于前三種類型,我們讓用戶知道我們拿到了那種類型以及它們的值。當我們拿到其它的類型(Char),我們簡單地讓用戶知道值。我們可以添加更多的類型到那個列表,它們會被最后默認的通配符case 捕捉。
序列匹配
鑒于用Scala 工作通常意味著和序列打交道,要是能和列表、數(shù)組的長度和內(nèi)容來匹配豈不美哉?下面的例子就做到了,它測試了兩個列表來檢查它們是否包含4個元素,并且第二個元素是3。
- // code-examples/Rounding/match-seq-script.scala
- val willWork = List(1, 3, 23, 90)
- val willNotWork = List(4, 18, 52)
- val empty = List()
- for (l <- List(willWork, willNotWork, empty)) {
- l match {
- case List(_, 3, _, _) => println("Four elements, with the 2nd being '3'.")
- case List(_*) => println("Any other list with 0 or more elements.")
- }
- }
在第二個case 里我們使用了一個特殊的通配符來匹配一個任意大小的List,甚至0個元素,任何元素的值都行。你可以在任何序列匹配的最后使用這個模式來解除長度制約。
回憶一下我們提過的List 的“cons” 方法,::。表達式a :: list 在一個列表前加入一個元素。你也可以使用這個操作符來從一個列表中解出頭和尾。
- // code-examples/Rounding/match-list-script.scala
- val willWork = List(1, 3, 23, 90)
- val willNotWork = List(4, 18, 52)
- val empty = List()
- def processList(l: List[Any]): Unit = l match {
- case head :: tail =>
- format("%s ", head)
- processList(tail)
- case Nil => println("")
- }
- for (l <- List(willWork, willNotWork, empty)) {
- print("List: ")
- processList(l)
- }
processList 方法對List 參數(shù)l 進行匹配。像下面這樣開始一個方法定義可能看起來比較奇怪。
- def processList(l: List[Any]): Unit = l match {
- ...
- }
用省略號來隱藏細節(jié)以后應(yīng)該會更加清楚一些。processList 方法實際上是一個跨越了好幾行的單指令。
它先匹配head :: tail,這時head 會被賦予這個列表的第一個元素,tail 會被賦予列表剩余的部分。也就是說,我們使用:: 來從列表中解出頭和尾。當這個case 匹配的時候,它打印出頭,然后遞歸調(diào)用processList 來處理列表尾。
第二個case 匹配空列表,Nil。它打印出一行的最后一個字符,然后終止遞歸。
元組匹配(以及守衛(wèi))
另外,如果我們只是想測試我們是否有一個有2 個元素的元組,我們可以進行元組匹配:
- // code-examples/Rounding/match-tuple-script.scala
- val tupA = ("Good", "Morning!")
- val tupB = ("Guten", "Tag!")
- for (tup <- List(tupA, tupB)) {
- tup match {
- case (thingOne, thingTwo) if thingOne == "Good" =>
- println("A two-tuple starting with 'Good'.")
- case (thingOne, thingTwo) =>
- println("This has two things: " + thingOne + " and " + thingTwo)
- }
- }
例子里的第二個case,我們已經(jīng)解出了元組里的值并且附給了局部變量,然后在結(jié)果表達式中使用了這些變量。
在第一個case 里,我們加入了一個新的概念:守衛(wèi)(Guard)。這個元組后面的if 條件是一個守衛(wèi)。這個守衛(wèi)會在匹配的時候進行評估,但是只會解出本case 的變量。守衛(wèi)在構(gòu)造cases 的時候提供了額外的尺度。在這個例子里,兩個模式的唯一區(qū)別就是這個守衛(wèi)表達式,但是這樣足夠編譯器來區(qū)分它們了。
提示
回憶一下,模式匹配的cases 會被按順序依次被評估。例如,如果你的第一個case 比第二個case 更廣,那么第二個case 就不會被執(zhí)行到。(不可執(zhí)行到的代碼會導(dǎo)致一個編譯錯誤。)你可以在模式匹配的最后包含一個“default” 默認case,可以使用下劃線通配符,或者有含義的變量名。當使用變量時,它不應(yīng)該顯式聲明為任何類型,除非是Any,這樣它才能匹配所有情況。另外一方面,嘗試通過設(shè)計讓你的代碼規(guī)避這樣全盤通吃的條件,保證它只接受指定的意料之中的條目。
Case 類匹配
讓我們來嘗試一次深度匹配,在我們的模式匹配中檢查對象的內(nèi)容。
- // code-examples/Rounding/match-deep-script.scala
- case class Person(name: String, age: Int)
- val alice = new Person("Alice", 25)
- val bob = new Person("Bob", 32)
- val charlie = new Person("Charlie", 32)
- for (person <- List(alice, bob, charlie)) {
- person match {
- case Person("Alice", 25) => println("Hi Alice!")
- case Person("Bob", 32) => println("Hi Bob!")
- case Person(name, age) =>
- println("Who are you, " + age + " year-old person named " + name + "?")
- }
- }
從上面例子的輸出中我們可以看出,可憐的Charlie 被無視了:
- Hi Alice!
- Hi Bob!
- Who are you, 32 year-old person named Charlie?
我們收線定義了一個case 類,一個特殊類型的類,我們會在《第6章 - Scala 高級面向?qū)ο缶幊獭返摹癈ase 類”章節(jié)中學(xué)到更多細節(jié)。現(xiàn)在,我們只需要知道,一個case 類允許精煉的簡單對象的構(gòu)造,以及一些預(yù)定義的方法。然后,我們的模式匹配通過檢查傳入的Person case 類的值來查找Alice 和Bob。Charlie 則直到最后那個饑不擇食的case 才被捕獲;盡管他和Bob 有一樣的年齡,但是我們同時也檢查了名字屬性。
我們后面會看到,這種類型的模式匹配和Actor 配合工作時會非常有用。Case 類經(jīng)常會被作為消息發(fā)送到Actor,對一個對象的內(nèi)容進行深度模式匹配是“分析”這些消息的方便方式。
正則表達式匹配
正則表達式用來從有著非正式結(jié)構(gòu)的字符串中提取數(shù)據(jù)是很方便的,但是對“結(jié)構(gòu)性數(shù)據(jù)”(就是類似XML,或者JSON 那樣的格式)則不是。正則表達式是幾乎所有現(xiàn)代編程語言的共有特性之一,通常被簡稱為regexes(regex 的復(fù)數(shù),Regular Expression 的簡稱)。它們提供了一套簡明的語法來說明復(fù)雜的匹配,其中一種通常被翻譯成后臺狀態(tài)機來獲得優(yōu)化的性能。
如果已經(jīng)在其它編程語言中使用正則表達式,那么Scala 的應(yīng)該不會讓你感覺到驚訝。讓我們來看一個例子。
- // code-examples/Rounding/match-regex-script.scala
- val BookExtractorRE = """Book: title=([^,]+),s+authors=(.+)""".r
- val MagazineExtractorRE = """Magazine: title=([^,]+),s+issue=(.+)""".r
- val catalog = List(
- "Book: title=Programming Scala, authors=Dean Wampler, Alex Payne",
- "Magazine: title=The New Yorker, issue=January 2009",
- "Book: title=War and Peace, authors=Leo Tolstoy",
- "Magazine: title=The Atlantic, issue=February 2009",
- "BadData: text=Who put this here??"
- )
- for (item <- catalog) {
- item match {
- case BookExtractorRE(title, authors) =>
- println("Book "" + title + "", written by " + authors)
- case MagazineExtractorRE(title, issue) =>
- println("Magazine "" + title + "", issue " + issue)
- case entry => println("Unrecognized entry: " + entry)
- }
- }
我們從兩個正則表達式開始,其中一個記錄書的信息,另外一個記錄雜志。在一個字符串上調(diào)用.r 會把它變成一個正則表達式;我們是用了原始(三重引號)字符串來避免諸多雙重轉(zhuǎn)義的反斜杠。如果你覺得字符串的.r 轉(zhuǎn)換方法不是很清晰,你也可以通過創(chuàng)建Regex 類的實例來定義正則表達式,比如new Regex("""W""")。
注意每一個正則表達式都定義了兩個捕捉組,由括號表示。每一個組捕獲記錄上的一個單獨字段,自如書的標題或者作者。Scala 的正則表達式會把這些捕捉組翻譯成抽取器。每個匹配都會把捕獲結(jié)果設(shè)置到到對應(yīng)的字段去;要是沒有捕捉到就設(shè)為null。
這在實際中有什么意義呢?如果提供給正則表達式的文本匹配了,case BookExtractorRE(title,authors) 會把第一個捕捉組賦給title,第二個賦給authors。我們可以在case 語句的右邊使用這些值,正如我們在上面的例子里看到的。抽取器中的變量名title 和author 是隨意的;從捕捉組來的匹配結(jié)果會簡單地從左往右被賦值,你可以叫它們?nèi)魏蚊帧?/p>
這就是Scala 正則表達式的簡要介紹。scala.util.matching.Regex 類提供了幾個方便的方法來查找和替代字符串中的匹配,不管是所有的匹配還是第一個。好好利用它們。
我們不會在這里涵蓋書寫正則表達式的細節(jié)。Scala 的Regex 類使用了對應(yīng)平臺的正則表達式API(就是Java,或者.NET 的)。參考這些API 的文檔來獲取詳細信息,不同語言之間可能會存在微妙的差別。
#p#
在Case 字句中綁定嵌套變量
有時候你希望能夠綁定一個變量到匹配中的一個對象,同時又能在嵌套的對象中指定匹配的標準。我們修改一下前面一個例子,來匹配map 的鍵值對。我們把同樣的Person 對象作為值,員工ID 作為鍵。我們會給Person 加一個屬性- 角色,用來指定對應(yīng)的實例是類型層次結(jié)構(gòu)中的哪一種。
- // code-examples/Rounding/match-deep-pair-script.scala
- class Role
- case object Manager extends Role
- case object Developer extends Role
- case class Person(name: String, age: Int, role: Role)
- val alice = new Person("Alice", 25, Developer)
- val bob = new Person("Bob", 32, Manager)
- val charlie = new Person("Charlie", 32, Developer)
- for (item <- Map(1 -> alice, 2 -> bob, 3 ->
分享標題:Scala編程指南揭示Scala的本質(zhì)
文章鏈接:http://m.fisionsoft.com.cn/article/cdopiod.html


咨詢
建站咨詢
