Scala 是一種基於JVM集合了面向對象編程和函數式編程優點的高級程序設計語言在《Scala編程指南 更少的字更多的事》中我們從幾個方面見識了Scala 簡潔可伸縮高效的語法我們也描述了許多Scala 的特性本文為《Programming Scala》第三章我們會在深入Scala 對面向對象編程和函數式編程的支持前完成對Scala 本質的講解
CTO推薦專題Scala編程語言
Scala 本質
在我們深入Scala 對面向對象編程以及函數式編程的支持之前讓我們來先完成將來可能在程序中用到的一些Scala 本質和特性的討論
操作符?操作符?
Scala 一個重要的基礎概念就是所有的操作符實際上都是方法考慮下面這個最基礎的例子
// codeexamples/Rounding/oneplustwoscriptscala + 兩個數字之間的加號是個什麼呢?是一個方法第一Scala 允許非字符型方法名稱你可以把你的方法命名為+-$ 或者其它任何你想要的名字(譯著後面會提到例外)第二這個表達式等同於 +()(我們在 的後面加了一個空格因為 會被解釋為Double 類型)當一個方法只有一個參數的時候Scala 允許你不寫點號和括號所以方法調用看起來就像是操作符調用這被稱為中綴表示法也就是操作符是在實例和參數之間的我們會很快見到很多這樣的例子
類似的一個沒有參數的方法也可以不用點號直接調用這被稱為後綴表示法
Ruby 和SmallTalk 程序員現在應該感覺和在家一樣親切了因為那些語言的使用者知道這些簡單的規則有著廣大的好處它可以讓你用自然的優雅的方式來創建應用程序
那麼哪些字符可以被用在標識符裡呢?這裡有一個標識符規則的概括它應用於方法類型和變量等的名稱要獲取更精確的細節描述參見[ScalaSpec]Scala 允許所有可打印的ASCII 字符比如字母數字下劃線和美元符號$除了括號類的字符比如( ) [ ] { }和分隔類字符比如` ; 和 除了上面的列表Scala 還允許其他在u 到uF 之間的字符比如數學符號和其它 符號這些余下的字符被稱為操作符字符包括了/ < 等
不能使用保留字
正如大多數語言一樣你不能是用保留字作為標識符我們在《第章 打更少的字做更多的事》的保留字 章節列出了所有的保留字回憶一下其中有些保留字是操作符和標點的組合比如說簡單的一個下劃線(_) 是一個保留字!
普通標識符 字母數字以及 $ _ 操作符的組合
和Java 以及很多其它語言一樣一個普通標志符可以以一個字母或者下劃線開頭緊跟著更多的字母數字下劃線和美元符號和Unicode 等同的字符也是被允許的然而和Java 一樣Scala 保留了美元符號作為內部使用所以你不應該在你自己的標識符裡使用它在一個下劃線之後你可以接上字母數字或者一個序列的操作符字符下劃線很重要它告訴編譯器把後面直到空格之前所有的字符都處理為標識符比如val xyz__++ = 把值 賦值給變量xyz__++而表達式val xyz++= = 卻不能通過編譯因為這個標識符同樣可以被解釋為xyz ++=看起來像是要把某些東西加到xyz 後面去類似的如果你在下劃線後接有操作符字符你不能把它們和字母數字混合在一起這個約束避免了像這樣的表達式的二義性abc_=這是一個標識符abc_= 還是給abc_ 賦值 呢?
普通標識符 操作符
如果一個標識符以操作符為開頭那麼余下的所有字符都必須是操作符字符
反引用字面值
一個標識符可以是兩個反單引號內一個任意的字符串(受制於平台的限制)比如val `this is a valid identifier` = Hello World!回憶一下我們可以發現這個語法也是引用Java 或者NET 的類庫中和Scala 保留字的名稱一樣的方法時候所用的方式比如Proxy`type`()
模式匹配標識符
在模式匹配表達式中以小寫字母開頭的標識都會被解析為變量標識符而以大寫字母開頭的標識會被解析為常量標識符這個限定避免了一些由於非常簡潔的變量語法而帶來的二義性例如不用寫val 關鍵字
語法糖蜜
一旦你知道所有的操作符都是方法那麼理解一些不熟悉的Scala 代碼就會變的相對容易些了你不用擔心那些充滿了新奇操作符的特殊案例在《第章 從 分到 分Scala 介紹》中的初嘗並發 章節中我們使用了Actor 類你會注意到我們使用了一個驚歎號(!)來發送消息給一個Actor現在你知道!只是另外一個方法而已就像其它你可以用來和Actor 交互的快捷操作符一樣類似的Scala 的XML 庫提供了 操作符來滲入到文檔結構中去這些只是scalaxmlNodeSeq 類的方法而已
靈活的方法命名規則能讓你寫出就像Scala 原生擴展一樣的庫你可以寫一個數學類庫處理數字類型加減乘除以及其它常見的數學操作你也可以寫一個新的行為類似Actors 的並發消息層各種的可能性僅受到Scala 方法命名限制的約束
警告
別因為你可以就覺得你應該這麼作當用Scala 來設計你自己的庫和API 的時候記住晦澀的標點和操作符會難以被程序員所記住過量使用這些操作符會導致你的代碼充滿難懂的噪聲堅持已有的約定當一個快捷符號沒有在你腦海中成型的時候清晰地把它拼出來吧
不用點號和括號的方法
為了促進閱讀性更加的編程風格Scala 在方法的括號使用上可謂是靈活至極如果一個方法不用接受參數你可以無需括號就定義它調用者也必須不加括號地調用它如果你加上了空括號那麼調用者可以有選擇地加或者不加括號例如List 的size 方法沒有括號所以你必須寫List()size如果你嘗試寫List()size() 就會得到一個錯誤然而String 類的length 方法在定義時帶有括號所以hellolength() 和hellolength 都可以通過編譯
Scala 社區的約定是在沒有副作用的前提下省略調用方法時候的空括號所以查詢一個序列的大小(size)的時候可以不用括號但是定義一個方法來轉換序列的元素則應該寫上括號這個約定給你的代碼使用者發出了一個有潛在的巧妙方法的信號
當調用一個沒有參數的方法或者只有一個參數的方法的時候還可以省略點號知道了這一點我們的List()size 例子就可以寫成這樣
// codeexamples/Rounding/nodotscriptscala List( ) size 很整潔但是又令人疑惑在什麼時候這樣的語法靈活性會變得有用呢?是當我們把方法調用鏈接成自表達性的自我解釋的語句 的時候
// codeexamples/Rounding/nodotbetterscriptscala def isEven(n: Int) = (n % ) == List( ) filter isEven foreach println 就像你所猜想的運行上面的代碼會產生如下輸出
Scala 這種對於方法的括號和點號不拘泥的方式為書寫域特定語言(DomainSpecific Language)定了基石我們會在簡短地討論一下操作符優先級之後再來學習它
優先級規則
那麼如果這樣一個表達式 * / * 實際上是Double 上的一系列方法調用那麼這些操作符的調用優先級規則是什麼呢?這裡從低到高表述了它們的優先級[ScalaSpec]
◆所有字母
◆|
◆^
◆&
◆< >
◆= !
◆:
◆+
◆* / %
◆所有其它特殊字符
在同一行的字符擁有同樣的優先級一個例外是當= 作為賦值存在時它擁有最低的優先級
因為* 和/ 有一樣的優先級下面兩行scala 對話的行為是一樣的
scala> * / * res: Double = scala> ((( * ) / ) * )res: Double = 在一個左結合的方法調用序列中它們簡單地進行從左到右的綁定你說左綁定?在Scala 中任何以冒號: 結尾的方法實際上是綁定在右邊的而其它方法則是綁定在左邊舉例來說你可以使用:: 方法(稱為consconstructor 構造器的縮寫)在一個List 前插入一個元素
scala> val list = List(b c d) list: List[Char] = List(b c d) scala> a :: list res: List[Char] = List(a b c d) 第二個表達式等效於list::(a)在一個右結合的方法調用序列中它們從右向左綁定那左綁定和有綁定混合的表達式呢?
scala> a :: list ++ List(e f) res: List[Char] = List(a b c d e f) (++ 方法鏈接了兩個list)在這個例子裡list 被加入到List(ef) 中然後a 被插入到前面來創建最後的list通常我們最好加上括號來消除可能的不確定因素
提示
任何名字以: 結尾的方法都向右邊綁定而不是左邊
最後注意當你使用scala 命令的時候無論是交互式還是使用腳本看上去都好像可以在類型之外定義全局變量和方法這其實是一個假象解釋器實際上把所有定義都包含在一個匿名的類型中然後才去生成JVM 或者NET CLR 字節碼
領域特定語言
領域特定語言也稱為DSL為特定的問題領域提供了一種方便的語意來表達自己的目標比如SQL 為處理與數據庫打交道的問題提供了剛剛好的編程語言功能使之成為一個領域特定語言
有些DSL 像SQL 一樣是自我包含的而今使用成熟語言來實現DSL 使之成為母語言的一個子集變得流行起來這允許程序員充分利用宿主語言來涵蓋DSL 不能覆蓋到的邊緣情況而且節省了寫詞法分析器解析器和其它語言基礎的時間
Scala 的豐富靈活的語法使得寫DSL 輕而易舉你可以把下面的例子看作使用Specs 庫(參見Specs 章節)來編寫行為驅動開發[BDD] 程序的風格
// codeexamples/Rounding/specsscriptscala // Example fragment of a Specs script Doesnt 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) finderfindNerds mustEqual List(Rick Moranis Woody Allen) } } 注意這段代碼和英語語法的相似性this should test that in the following scenario(這應該在以下場景中測試它)this value must equal that value (這個值必須等於那個值)等等這個例子使用了華麗的Specs 庫它提供了一套高效的DSL 來用於行為驅動開發測試和工程方法學通過最大化利用Scala 的自有語法和諸多方法Specs 測試組即使對於非開發人員來說也是可讀的
這只是對Scala 強大的DSL 的一個簡單嘗試我們會在後面看到更多其它例子以及在討論更高級議題的時候學習如何編寫你自己的DSL(參見《第章 Scala 的領域特定語言》)
Scala if 指令
即使是最常見的語言特性在Scala 裡也被增強了讓我們來看看簡單的if 指令和大多數語言一樣Scala 的if 測試一個條件表達式然後根據結果為真或假來跳轉到響應語句塊中一個簡單的例子
// codeexamples/Rounding/ifscriptscala if ( + == ) { println(Hello from ) } else if ( + == ) { println(Hello from Remedial Math class?) } else { println(Hello from a nonOrwellian future) } 在Scala 中與眾不同的是if 和其它幾乎所有指令實際上都是表達式所以我們可以把一個if 表達式的結果賦值給其它(變量)像下面這個例子所展示的
// codeexamples/Rounding/assignedifscriptscala val configFile = new javaioFile(myapprc) val configFilePath = if (configFileexists()) { configFilegetAbsolutePath() } else { configFilecreateNewFile() configFilegetAbsolutePath() } 注意 if 語句是表達式意味著它們有值在這個例子裡configFilePath 的值就是if 表達式的值它處理了配置文件不存在的情況並且返回了文件的絕對路徑這個值現在可以在程序中被重用了if 表達式的值只有在被使用到的時候才會被計算
因為在Scala 裡if 語句是一個表達式所以就不需要C 類型子語言的三重條件表達式了你不會在Scala 裡看到x ? doThis() : doThat() 這樣的代碼因為Scala 提供了一個即強大又更具有可讀性的機制
如果我們在上面的例子裡省略else 字句會發生什麼?在scala 解釋器裡輸入下面的代碼會告訴我們發生什麼
scala> val configFile = new javaioFile(~/myapprc) configFile: javaioFile = ~/myapprc scala> val configFilePath = if (configFileexists()) { | configFilegetAbsolutePath() | } configFilePath: Unit = () scala> 注意現在configFilePath 是Unit 類型了(之前是String)類型推斷選擇了一個滿足if 表達式所有結果的類型Unit 是唯一的可能因為沒有值也是一個可能的結果
Scala for 推導語句
Scala 另外一個擁有豐富特性的類似控制結構是for 循環在Scala 社區中也被稱為for 推導語句或者for 表達式語言的這個功能絕對對得起一個花哨的名字因為它可以做一些很酷的戲法
實際上術語推導(comprehension)來自於函數式編程它表達了這樣個一個觀點我們正在遍歷某種集合推導我們所發現的然後從中計算出一些新的東西出來
一個簡單的小狗例子
讓我們從一個基本的for 表達式開始
// codeexamples/Rounding/basicforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) for (breed < dogBreeds) println(breed) 你可能已經猜到了這段代碼的意思是對於列表dogBreeds 裡面的每一個元素創建一個臨時變量叫breed並賦予這個元素的值然後打印出來把< 操作符看作一個箭頭引導集合中一個一個的元素到那個我們會在for 表達式內部引用的局部變量中去這個左箭頭操作符被稱為生成器之所以這麼叫是因為它從一個集合裡產生獨立的值來給一個表達式用
過濾器
那如果我們需要更細的粒度呢? Scala 的for 表達式通過過濾器來我們指定集合中的哪些元素是我們希望使用的所以要在我們的狗品種列表裡找到所有的梗類犬我們可以把上面的例子改成下面這樣
// codeexamples/Rounding/filteredforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) for (breed < dogBreeds if ntains(Terrier) ) println(breed) 如果需要給一個for 表達式添加多於一個的過濾器用分號隔開它們
// codeexamples/Rounding/doublefilteredforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) for (breed < dogBreeds if ntains(Terrier); if !breedstartsWith(Yorkshire) ) println(breed) 現在你已經找到了所有不出生於約克郡的梗類犬但願也知道了過濾器在過程中是多麼的有用
產生器
如果說你不想把過濾過的集合打印出來而是希望把它放到程序的另外一部分去處理呢?yeild 關鍵字就是用for 表達式來生成新集合的關鍵在下面的例子中注意我們把for 表達式包裹在了一對大括號中就像我們定義任何一個語句塊一樣
提示
for 表達式可以用括號或者大括號來定義但是使用大括號意味著你不必用分號來分割你的過濾器大部分時間裡你會在有一個以上過濾器賦值的時候傾向使用大括號
// codeexamples/Rounding/yieldingforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) val filteredBreeds = for { breed < dogBreeds if ntains(Terrier) if !breedstartsWith(Yorkshire) } yield breed 在for 表達式的每一次循環中被過濾的結果都會產生一個名為breed 的值這些結果會隨著每運行而累積最後的結果集合被賦給值filteredBreeds(正如我們上面用if 指令做的那樣)由foryield 表達式產生的集合類型會從被遍歷的集合類型中推斷在這個例子裡filteredBreeds 的類型是List[String]因為它是類型為List[String] 的dogBreeds 列表的一個子集
擴展的作用域
Scala 的for 推導語句最後一個有用的特性是它有能力把在定義在for 表達式第一部分裡的變量用在後面的部分裡這個例子是一個最好的說明
// codeexamples/Rounding/scopedforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) for { breed < dogBreeds upcasedBreed = breedtoUpperCase() } println(upcasedBreed) 注意即使沒有聲明upcaseBreed 為一個val你也可以在你的for 表達式主體內部使用它這個方法對於想在遍歷集合的時候轉換元素的時候來說是很理想的
最後在《第章 應用程序設計》的Options 和For 推導語句章節我們會看到使用Options 和for 推導語句可以大大地減少不必要的null 和空判斷從而減少代碼數量
其它循環結構
Scala 有幾種其它的循環結構
Scala while 循環
和許多語言類似while 循環在條件為真的時候會持續執行一段代碼塊例如下面的代碼在下一個星期五同時又是號之前每天打印一句抱怨的話
// codeexamples/Rounding/whilescriptscala // WARNING: This script runs for a LOOOONG time! import javautilCalendar def isFridayThirteen(cal: Calendar): Boolean = { val dayOfWeek = calget(CalendarDAY_OF_WEEK) val dayOfMonth = calget(CalendarDAY_OF_MONTH) // Scala returns the result of the last expression in a method (dayOfWeek == CalendarFRIDAY) && (dayOfMonth == ) } while (!isFridayThirteen(CalendargetInstance())) { println(Today isnt Friday the th Lame) // sleep for a day Threadsleep() } 你可以在下面找到一張表它列舉了所有在while 循環中工作的條件操作符
Scala dowhile 循環
和上面的while 循環類似一個dowhile 循環當條件表達式為真時持續執行一些代碼唯一的區別是dowhile 循環在運行代碼塊之後才進行條件檢查要從 數到我們可以這樣寫
// codeexamples/Rounding/dowhilescriptscala var count = do { count += println(count) } while (count < ) 這也展示了在Scala 中遍歷一個集合還有一種更優雅的方式我們會在下一節看到
生成器表達式
還記得我們在討論for 循環的時候箭頭操作符嗎(<)?我們也可以讓它在這裡工作讓我們來整理一下上面的dowhile 循環
// codeexamples/Rounding/generatorscriptscala for (i < to ) println(i) 這就是所有需要的了是Scala 的RichInt(富整型)使得這個簡潔的單行代碼成為可能編譯器執行了一個隱式轉換把一個Int (整型)轉換成了RichInt 類型(我們會在《第章 Scala 對象系統》的Scala 類型結構 章節以及《第章 Scala 函數式編程》的隱式轉換 章節中討論這些轉換)RichInt 定義了以訛to 方法它接受另外一個整數然後返回一個RangeInclusive 的實例也就是說Inclusive 是Rang 伴生對象(Companion Object我們在《第章 從 分到 分Scala 介紹》中間要介紹過參考《第章 Scala 高級面向對象編程》獲取更多信息)的一個嵌套類類Range 的這個嵌套類繼承了一系列方法來和序列以及可迭代的數據結構交互包括那些在for 循環中必然會使用到的
順便說一句如果你想從 數到 但是不包括 你可以使用until 來代替to比如for (i < until )
這樣就一幅清晰的圖畫展示了Scala 的內部類庫是如何結合起來形成簡單易用的語言結構的
注意
當和大多數語言的循環一起工作時你可以使用break 來跳出循環或者continue 來繼續下一個迭代Scala 沒有這兩個指令但是當編寫地道的Scala 代碼時它們是不必要的你應該使用條件表達式來測試一個循環是否應該繼續或者利用遞歸更好的方法是在這之前就用過濾器來出去循環中復雜的條件狀態然而因為大眾需求 版本的Scala 加入了對break 的支持不過是以庫的一個方法實現而不是內建的break 關鍵字
條件操作符
Scala 從Java 和它的前輩身上借用了絕大多數的條件操作符你可以在下面的if 指令while 循環以及其它任何可以運用條件判斷的地方發現它們
我們會在《第章 Scala 高級面向對象編程》的對象的相等 章節中更深入討論對象相等性例如我們會看到== 在Scala 和Java 中有著不同的含義除此以外這些操作符大家應該都很熟悉所以讓我們繼續前進到一些新的激動人心的特性上去
模式匹配
模式匹配是從函數式語言中引入的強大而簡潔的多條件選擇跳轉方式你也可以把模式匹配想象成你最喜歡的C 類語言的case 指令當然是打了激素的在典型的case 指令中通常只允許對序數類型進行匹配產生一些這樣的表達式在i 為 的case 裡打印一個消息在i 為 的case裡離開程序而有了Scala 的模式匹配你的case 可以包含類型通配符序列甚至是對象變量的深度檢查
一個簡單的匹配
讓我們從模擬拋硬幣匹配一個布爾值開始
// codeexamples/Rounding/matchbooleanscriptscala 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 風格的case 語句對吧?唯一的區別是最後一個case 使用了下劃線_ 通配符它匹配了所有上面的case 中沒有定義的情況所以它和JavaC# 中的switch 指令的default 關鍵字作用相同
模式匹配是貪婪的只有第一個匹配的情況會贏所以如果你在所有case 前方一個case _ 語句那麼編譯器會在下一個條件拋出一個無法執行到的代碼的錯誤因為沒人能跨過那個default 條件
提示
使用case _ 來作為默認的滿足所有的匹配
那如果我們希望獲得匹配的變量呢?
匹配中的變量
// codeexamples/Rounding/matchvariablescriptscala import scalautilRandom val randomInt = new Random()nextInt() randomInt match { case => println(lucky seven!) case otherNumber => println(boo got boring ol + otherNumber) } 在這個例子裡我們把通配符匹配的值賦給了一個變量叫otherNumber然後在下面的表達式中打印出來如果我們生成了一個我們會對它稱頌道德反之我們則詛咒它讓我們經歷了一個不幸運的數字
類型匹配
這些例子甚至還沒有開始接觸到Scala 的模式匹配特性的最表面讓我們來嘗試一下類型匹配
// codeexamples/Rounding/matchtypescriptscala val sundries = List( Hello 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 中拉出所有元素包括了StringDoubleInt和Char對於前三種類型我們讓用戶知道我們拿到了那種類型以及它們的值當我們拿到其它的類型(Char)我們簡單地讓用戶知道值我們可以添加更多的類型到那個列表它們會被最後默認的通配符case 捕捉
序列匹配
鑒於用Scala 工作通常意味著和序列打交道要是能和列表數組的長度和內容來匹配豈不美哉?下面的例子就做到了它測試了兩個列表來檢查它們是否包含個元素並且第二個元素是
// codeexamples/Rounding/matchseqscriptscala val willWork = List( ) val willNotWork = List( ) val empty = List() for (l < List(willWork willNotWork empty)) { l match { case List(_ _ _) => println(Four elements with the nd being ) case List(_*) => println(Any other list with or more elements) } } 在第二個case 裡我們使用了一個特殊的通配符來匹配一個任意大小的List甚至個元素任何元素的值都行你可以在任何序列匹配的最後使用這個模式來解除長度制約
回憶一下我們提過的List 的cons 方法::表達式a :: list 在一個列表前加入一個元素你也可以使用這個操作符來從一個列表中解出頭和尾
// codeexamples/Rounding/matchlistscriptscala val willWork = List( ) val willNotWork = List( ) 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 參數l 進行匹配像下面這樣開始一個方法定義可能看起來比較奇怪
def processList(l: List[Any]): Unit = l match { } 用省略號來隱藏細節以後應該會更加清楚一些processList 方法實際上是一個跨越了好幾行的單指令
它先匹配head :: tail這時head 會被賦予這個列表的第一個元素tail 會被賦予列表剩余的部分也就是說我們使用:: 來從列表中解出頭和尾當這個case 匹配的時候它打印出頭然後遞歸調用processList 來處理列表尾
第二個case 匹配空列表Nil它打印出一行的最後一個字符然後終止遞歸
元組匹配(以及守衛)
另外如果我們只是想測試我們是否有一個有 個元素的元組我們可以進行元組匹配
// codeexamples/Rounding/matchtuplescriptscala val tupA = (Good Morning!) val tupB = (Guten Tag!) for (tup < List(tupA tupB)) { tup match { case (thingOne thingTwo) if thingOne == Good => println(A twotuple starting with Good) case (thingOne thingTwo) => println(This has two things: + thingOne + and + thingTwo) } } 例子裡的第二個case我們已經解出了元組裡的值並且附給了局部變量然後在結果表達式中使用了這些變量
在第一個case 裡我們加入了一個新的概念守衛(Guard)這個元組後面的if 條件是一個守衛這個守衛會在匹配的時候進行評估但是只會解出本case 的變量守衛在構造cases 的時候提供了額外的尺度在這個例子裡兩個模式的唯一區別就是這個守衛表達式但是這樣足夠編譯器來區分它們了
提示
回憶一下模式匹配的cases 會被按順序依次被評估例如如果你的第一個case 比第二個case 更廣那麼第二個case 就不會被執行到(不可執行到的代碼會導致一個編譯錯誤)你可以在模式匹配的最後包含一個default 默認case可以使用下劃線通配符或者有含義的變量名當使用變量時它不應該顯式聲明為任何類型除非是Any這樣它才能匹配所有情況另外一方面嘗試通過設計讓你的代碼規避這樣全盤通吃的條件保證它只接受指定的意料之中的條目
Case 類匹配
讓我們來嘗試一次深度匹配在我們的模式匹配中檢查對象的內容
// codeexamples/Rounding/matchdeepscriptscala case class Person(name: String age: Int) val alice = new Person(Alice ) val bob = new Person(Bob ) val charlie = new Person(Charlie ) for (person < List(alice bob charlie)) { person match { case Person(Alice ) => println(Hi Alice!) case Person(Bob ) => println(Hi Bob!) case Person(name age) => println(Who are you + age + yearold person named + name + ?) } } 從上面例子的輸出中我們可以看出可憐的Charlie 被無視了
Hi Alice! Hi Bob! Who are you yearold person named Charlie? 我們收線定義了一個case 類一個特殊類型的類我們會在《第章 Scala 高級面向對象編程》的Case 類章節中學到更多細節現在我們只需要知道一個case 類允許精煉的簡單對象的構造以及一些預定義的方法然後我們的模式匹配通過檢查傳入的Person case 類的值來查找Alice 和BobCharlie 則直到最後那個饑不擇食的case 才被捕獲盡管他和Bob 有一樣的年齡但是我們同時也檢查了名字屬性
我們後面會看到這種類型的模式匹配和Actor 配合工作時會非常有用Case 類經常會被作為消息發送到Actor對一個對象的內容進行深度模式匹配是分析這些消息的方便方式
正則表達式匹配
正則表達式用來從有著非正式結構的字符串中提取數據是很方便的但是對結構性數據(就是類似XML或者JSON 那樣的格式)則不是正則表達式是幾乎所有現代編程語言的共有特性之一通常被簡稱為regexes(regex 的復數Regular Expression 的簡稱)它們提供了一套簡明的語法來說明復雜的匹配其中一種通常被翻譯成後台狀態機來獲得優化的性能
如果已經在其它編程語言中使用正則表達式那麼Scala 的應該不會讓你感覺到驚訝讓我們來看一個例子
// codeexamples/Rounding/matchregexscriptscala 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 Book: title=War and Peace authors=Leo Tolstoy Magazine: title=The Atlantic issue=February 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) } } 我們從兩個正則表達式開始其中一個記錄書的信息另外一個記錄雜志在一個字符串上調用r 會把它變成一個正則表達式我們是用了原始(三重引號)字符串來避免諸多雙重轉義的反斜槓如果你覺得字符串的r 轉換方法不是很清晰你也可以通過創建Regex 類的實例來定義正則表達式比如new Regex(W)
注意每一個正則表達式都定義了兩個捕捉組由括號表示每一個組捕獲記錄上的一個單獨字段自如書的標題或者作者Scala 的正則表達式會把這些捕捉組翻譯成抽取器每個匹配都會把捕獲結果設置到到對應的字段去要是沒有捕捉到就設為null
這在實際中有什麼意義呢?如果提供給正則表達式的文本匹配了case BookExtractorRE(titleauthors) 會把第一個捕捉組賦給title第二個賦給authors我們可以在case 語句的右邊使用這些值正如我們在上面的例子裡看到的抽取器中的變量名title 和author 是隨意的從捕捉組來的匹配結果會簡單地從左往右被賦值你可以叫它們任何名字
這就是Scala 正則表達式的簡要介紹scalautilmatchingRegex 類提供了幾個方便的方法來查找和替代字符串中的匹配不管是所有的匹配還是第一個好好利用它們
我們不會在這裡涵蓋書寫正則表達式的細節Scala 的Regex 類使用了對應平台的正則表達式API(就是Java或者NET 的)參考這些API 的文檔來獲取詳細信息不同語言之間可能會存在微妙的差別
在Case 字句中綁定嵌套變量
有時候你希望能夠綁定一個變量到匹配中的一個對象同時又能在嵌套的對象中指定匹配的標准我們修改一下前面一個例子來匹配map 的鍵值對我們把同樣的Person 對象作為值員工ID 作為鍵我們會給Person 加一個屬性- 角色用來指定對應的實例是類型層次結構中的哪一種
// codeexamples/Rounding/matchdeeppairscriptscala 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 Developer) val bob = new Person(Bob Manager) val charlie = new Person(Charlie Developer) for (item < Map( > alice > bob > charlie)) { item match { case (id p @ Person(_ _ Manager)) => format(%s is overpaidn p) case (id p @ Person(_ _ _)) => format(%s is underpaidn p) } } 這個case 對象和我們之前看到的單體對象一樣就是多了一些特殊的case 類所屬的行為我們最關心的是嵌套在case 子句的p @ Person() 我們在閉合元組裡匹配特定類型的Person 對象我們同時希望把Person 賦給一個變量這樣我們就能夠打印它
Person(AliceDeveloper) is underpaid Person(BobManager) is overpaid Person(CharlieDeveloper) is underpaid 如果我們在Person 本身使用匹配標准我們可以直接寫 p: Person例如前面的match 字句可以寫成這樣
item match { case (id p: Person) => prole match { case Manager => format(%s is overpaidn p) case _ => format(%s is underpaidn p) } } 主意p @ Person() 語法給了我們一個把嵌套的match 語句平坦化成一個語句的方法這類似於我們在正則表達式中使用捕捉組來提取我們需要的子字符串時來替代把一個字符串分隔成好幾個的方法你可以使用任何一種你偏好的方法
使用trycatch 和finally 語句
通過使用函數式構造和強類型特性Scala 鼓勵減少對異常和異常處理依賴的編程風格但是當Scala 和Java 交互時異常還是很普遍的
注意
Scala 不支持Java 那樣的異常檢查(Checked Exception)即使在Java 中異常是檢查的在Scala 中也會被轉換為未檢查異常在方法的聲明中也沒有throws 子句不過有一個@throws 注解可以用於和Java 交互參見《第章 應用程序設計》的注解章節
感謝Scala 實際上把異常處理作為了另外一種模式匹配允許我們在遇到多樣化的異常時能做出更聰明的決定讓我們實際地來看一個例子
// codeexamples/Rounding/trycatchscriptscala import javautilCalendar val then = null val now = CalendargetInstance() try { pareTo(then) } catch { case e: NullPointerException => println(One was null!); Systemexit() case unknown => println(Unknown exception + unknown); Systemexit() } finally { println(It all worked out) Systemexit() } 在上面的例子裡我們顯示地撲捉了NullPointerException 異常它在嘗試把一個Calendar 實例和null 被拋出我們同時也將unknown 定義為捕捉所有異常的字句以防萬一如果我們沒有硬編碼使得程序失敗finally 塊會被執行到用戶會被告知一切正常
注意
你可以使用一個下劃線(Scala 的標准通配符)作為占位符來捕捉任意類型的異常(不騙你它可以匹配模式匹配表達式的任何case)然而如此你就不能再訪問下面表達式中的異常了如果需要你還可以命名那個異常例如如果你需要打印出異常信息就像我們在前一個例子中的全獲性case 中做的一樣e 或者ex 都是一個不錯的名字
有了模式匹配Scala 異常操作的處理對於熟悉JavaRubyPython 和其它主流語言的人來說應該很容易上手而且一樣的你可以通過寫throw new MyBadException() 來拋出異常這就是有關異常的一切了
模式匹配結束語
模式匹配在使用恰當時是一個強大的優雅的從對象中抽取信息的方式回顧《第章 從 分到 分Scala 介紹》中我們強調了模式匹配和多態之間的協作大多數時候你希望能夠在清楚類結構的時候避免switch語句因為它們必須在每次結構改變的同時被改變
在我們的畫畫執行者(Actor)例子中我們使用了模式匹配來分離不同的消息種類但是我們使用了多態來畫出我們傳給它的圖形我們可以修改Shape 繼承結構Actor 部分的代碼卻不需要修改
在你遇到需要從對象內部提取數據的設計問題時模式匹配也有用但是僅限一些特殊的情況JavaBean 規格的一個沒有預料到的結果是它鼓勵人們通過getters 和setters 來暴露對象內部的字段這從來都不應該是一個默認的決策對狀態信息的存取應該被封裝並且只在對於該類型有邏輯意義的時候被暴露和對其抽象的觀察一致
相反地在你需要通過可控方式獲取信息的時候考慮使用模式匹配正如我們即將在《第章 Scala 高級面向對象編程》中的取消應用(Unapply)章節看到的我們所展示的模式匹配例子使用了預定義的unapply 方法來從實例中獲取信息這些方法讓你在不知道實現細節的同時獲取了這些信息實際上unapply 方法返回的信息可能是實例中實際信息的變種
最後當設計模式匹配指令時對於默認case 的依賴要小心在什麼情況下以上都不匹配才是正確的答案?它可能象征著設計需要被完善以便於你更精確地知道所有可能發生的匹配我們會在《第章 Scala 對象系統》的完成類(sealed class)結構討論完成類的結構時學習到其中一種技術
枚舉
還記得我們上一個涉及到很多種狗的例子嗎?在思考這些程序的類型時我們可能會需要一個頂層的Breed 類型來記錄一定數量的breeds 這樣一個類型被稱為枚舉類型它所包含的值被稱為枚舉值
雖然枚舉是許多程序語言的內置支持Scala 走了一條不同的路把它作為標准庫的一個類來實現這意味著Scala 中沒有Java 和C# 那樣特殊的枚舉語法相反你只是定義個對象讓它從Enumeration 類繼承因此在字節碼的層次Scala 枚舉和JavaC# 中構造的枚舉沒有任何聯系
這裡有一個例子
// codeexamples/Rounding/enumerationscriptscala object Breed extends Enumeration { val doberman = Value(Doberman Pinscher) val yorkie = Value(Yorkshire Terrier) val scottie = Value(Scottish Terrier) val dane = Value(Great Dane) val portie = Value(Portuguese Water Dog) } // print a list of breeds and their IDs println(IDtBreed) for (breed < Breed) println(breedid + t + breed) // print a list of Terrier breeds println(nJust Terriers:) Breedfilter(_toStringendsWith(Terrier))foreach(println) 運行時你會得到如下輸出
ID Breed Doberman Pinscher Yorkshire Terrier Scottish Terrier Great Dane Portuguese Water Dog Just Terriers: Yorkshire Terrier Scottish Terrier 你可以看到我們的Breed 枚舉類型包含了幾種Value 類型的值像下面的例子所展示的
val doberman = Value(Doberman Pinscher) 每一個聲明實際上都調用了一個名為Value 的方法它接受一個字符串參數我們使用這個方法來給每一個枚舉值賦一個長的品種名稱也就是上面輸出中的ValuetoString 方法所返回的值
注意類型和方法名字都為Value 並沒有名稱空間的沖突我們還有其他Value 方法的重載其中一個沒有參數另外一個接受整型ID 值還有一個同時接收整型和字符串參數這些Value 方法返回一個Value 對象它們會把這些值加到枚舉值的集合中去
實際上Scala 的枚舉類支持和集合協作需要的一般方法所以我們可以簡單地在循環中遍歷這些種類或者通過名字過濾它們上面的輸出也展示了枚舉中的每一個Value 都被自動賦予一個數字標識除非你調用其中一個Value 方法顯式指定ID 值
你常常希望給你的枚舉值可讀的名字就像我們這裡做的一樣然而有些時候你可能不需要它們這裡有另一個從Scala 文檔中中改編過來的枚舉例子
// codeexamples/Rounding/daysenumerationscriptscala object WeekDay extends Enumeration { type WeekDay = Value val Mon Tue Wed Thu Fri Sat Sun = Value } import WeekDay_ def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun) WeekDay filter isWorkingDay foreach println 運行這段腳本會產生如下輸出
Main$$anon$$WeekDay() Main$$anon$$WeekDay() Main$$anon$$WeekDay() Main$$anon$$WeekDay() Main$$anon$$WeekDay() 當名字沒有用接受字符串的Value 方法構造的時候ValuetoString 打印出來的名字是由編譯器生成的捆綁了自動生成的ID 值
注意我們導入了WeekDay_這使得每一個枚舉值比如MonTue 等都暴露在了可見域裡否則你必須寫完整的WeekDayMonWeekDayTue 等
同時import 使得類型別名類型 WeekDay = Value 也暴露在了可見域裡我們在isWorkingDay 方法中接受一個該類型的參數如果你不定義這樣的別名你就要像這樣聲明這個方法 def isWorkingDay(d:WeekDayValue)
因為Scala 的枚舉值是通常的對象你可以使用任何val 對象來指示不同的枚舉值然而擴展枚舉有幾個優勢比如它自動把所有值作為集合以供遍歷等正如我們的例子所示它同時也自動對每一個值都賦予一個唯一的整型ID
Case 類(參見《第章 Scala 高級面向對象編程》的Case 類章節)在Scala 經常被作為枚舉的替代品因為它們的用例經常涉及到模式匹配我們會在《第章 應用程序設計》的枚舉 vs 模式匹配章節重溫這個話題
概括及下章預告
我們已經在這章中涵蓋了很多基礎內容我們了解了Scala 的語法有多靈活以及它如何作用於創建域特定語言然後我們探索了Scala 增強的循環結構和條件表達式我們實驗了不同的模式匹配作為對我們熟悉的caseswitch 指令的一種強大增強最後我們學習了如何封裝枚舉中的值
你現在應該准備好閱讀更多的Scala 代碼了但是這門語言仍然有很多的知識可以讓你充實起來在下一章我們會探索Scala 對面向對象編程的支持從traits (特性)開始
From:http://tw.wingwit.com/Article/program/Java/hx/201311/25822.html