Scala編程指南——用更少的字作更多的事

    本文爲《Programming Scala》的中文譯文《Scala 編程指南》的第二章,在《Scala語言編程入門指南》咱們介紹了Scala語言編程的入門,在上一章中咱們以幾個撩撥性質的Scala 代碼範例做爲章節結束,在本章中咱們將詳細介紹如何使用Scala 來寫出精煉的,靈活的代碼。java

章節概要程序員

    在這一章咱們將討論如何使用Scala 來寫出精煉的,靈活的代碼。咱們會討論文件和包的組織結構,導入其餘的類型和變量聲明,一些語法習慣和概念。咱們會着重討論Scala 簡明的語法如何幫助你更好更快地工做。web

    Scala 的語法對於書寫腳本特別有用。單獨的編譯和運行步驟對於簡單的,僅有少許獨立於Scala 提供的庫以外的程序不是必須的。你能夠用scala 命令一次性編譯和運行這些程序。若是你已經下載了本書的實例代碼,它們中的許多小程序能夠用scala 命令來運行,好比scala filename.scala。參見每一章節代碼實例中的README.txt 能夠獲取更多細節。也能夠參見《第14章 - Scala 工具,庫和IDE 支持》中的「命令行工具」章節來獲取更多使用scala 命令的信息。正則表達式

分號算法

    你可能已經注意到,在上一章的代碼示例中不多有分號出現。你可使用分號來隔離各個聲明和表達式,就像Java,C,PHP 以及其餘相似的語言同樣。然而在大多數狀況下,Scala 的行爲和許多腳本語言同樣把一行的結尾看做是聲明或者表達式的結尾。當一個聲明或者表達式太長,一行不夠的時候,Scala 一般能夠推斷你在何時要在下一行繼續,就像這個例子中同樣:shell

// code-examples/TypeLessDoMore/semicolon-example-script.scala  
// Trailing equals sign indicates more code on next line   
def equalsign = {  
  val reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine = 
    "wow that was a long value name"
  println(reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine)   
}  
  
// Trailing opening curly brace indicates more code on next line   
def equalsign2(s: String) = {    
  println("equalsign2: " + s)  
}  

// Trailing comma, operator, etc. indicates more code on next line  
def commas(s1: String,
           s2: String) = {  
  println("comma: " + s1 +    
          ", " + s2)   
}

當你須要在同一行中放置多個聲明或者表達式的時候,你可使用分號來隔開它們。咱們在《第1章 - 從0分到60分:Scala 介紹》的「初嘗併發」章節中的ShapeDrawingActor 示例裏面使用了這樣的技巧。
編程

case "exit" => println("exiting..."); exit

這樣的代碼也能夠寫成以下的樣子。
小程序

case "exit" =>   
  println("exiting...")
  exit

你可能會想爲何在case... => 這行的後面不須要用大括號{ } 把兩個語句括起來。若是你願意,你能夠這麼作,可是編譯器其實會知道你何時會到達語句塊的結尾,由於它會看到下一個case 塊或者終結全部case 塊的大括號。api

    省略可選的分號意味着更少的符號輸入和更少的符號混亂。把各個語句隔離到它們本身單獨的行上面能夠提升你的代碼的可讀性。數組

變量聲明

    當你聲明一個變量的時候,Scala 容許你來決定它是不變的(只讀的)仍是可變的(可讀寫的)。一個不變的「變量」能夠用val 關鍵字來聲明(想象一個值對象)。

val array: Array[String] = new Array(5)

更準確的說,這個引用變量array 不能被修改而指向另一個Array (數組),可是這個數組自己能夠被修改,正以下面的演示:

scala> val array: Array[String] = new Array(5)  

  
array: Array[String] = Array(null, null, null, null, null)  

  
scala> array = new Array(2)  

  
:5: error: reassignment to val  

  
       array = new Array(2)  

  
        ^  

  
scala> array(0) = "Hello"  

  
scala> array  

  
res3: Array[String] = Array(Hello, null, null, null, null)  

  
scala>

一個不變的val 必須被初始化,也就是說在聲明的時候就必須定義。

一個可變的變量用關鍵字var 來聲明。

scala> var stockPrice: Double = 100.  

  
stockPrice: Double = 100.0  

  
scala> stockPrice = 10.  

  
stockPrice: Double = 10.0  

  
scala>

Scala 同時也要求你在聲明一個var 時將其初始化。你能夠在須要的時候給一個var 賦予新的值。這裏再次嚴謹說明一下:引用stockPrice 能夠被修改指向一個不同的Double 對象(好比10)。在這個例子裏,stockPrice 引用的對象不能被修改,由於Double 在Scala 裏是不可變的。

    在這裏,對於val 和var 聲明時即定義的規則有一些例外。這兩個關鍵字均可以被用做構造函數參數。看成爲構造函數參數時,這些可變或者不可變的變量會在一個對象被實例化的時候被初始化。兩個關鍵字能夠在抽象類型中被用來聲明「抽象」(沒有初始化的)的變量。同時,繼承類型能夠重寫在父類型中聲明的值。咱們會在《第5章 - Scala 基礎面向對象編程》中討論這些例外。

Scala 鼓勵你在任何可能的時候使用不可變的值。正如咱們即將看到的,這會促進更佳的面向對象設計,並且這和「純」函數式編程的原則相一致。

注意

var 和val 關鍵字指定了該引用可否被修改指向另外一個對象。它們並不指定它們引用的對象是否可變。

方法聲明

咱們在《第1章 - 從0分到60分:Scala 介紹》中見到了幾個如何定義方法的例子,它們都是類的成員函數。方法定義由一個def 關鍵字開始,緊接着是可選的參數列表,一個冒號「:」 和方法的返回類型,一個等於號「=」,最後是方法的主體。若是你不寫等於號和方法主體,那麼方法會被隱式聲明爲「抽象」。包含它的類型因而也是一個抽象類型。咱們會在《第5章,Scala 基礎面向對象編程》中詳細討論抽象類型。

咱們剛纔說到「可選的參數列表」,這意味着一個或更多。Scala 可讓你爲方法定義一個以上的參數列表。這是級聯方法(currying methods)所須要的。咱們會在《第8章 - Scala 函數式編程》中的「級聯(Currying)章節討論它。這個功能對於定義你本身的域特定語言(DSLs)也頗有幫助。咱們會在《第11章 - Scala 中的域特定語言》 中看到它。注意,每個參數列表會被括號所包圍,而且全部的參數由逗號隔開。

若是一個方法的主體包含多於一個的表達式,你必須用大括號{ } 來把它們包起來。你能夠在方法主體只有一個表達式的時候省略大括號。

方法的默認參數和命名參數(Scala 版本2.8)

許多語言都容許你爲一個方法的一個或多個參數定義默認值。考慮下面的腳本,一個StringUtil 對象容許你用一個用戶定義的分隔符來鏈接字符串。

 
  1. // code-examples/TypeLessDoMore/string-util-v1-script.scala  
  2. // Version 1 of "StringUtil".  
  3. object StringUtil {  
  4.   def joiner(strings: List[String], separator: String): String =  
  5.     strings.mkString(separator)  
  6.   def joiner(strings: List[String]): String = joiner(strings, " ")  
  7. }  
  8. import StringUtil._  // Import the joiner methods.  
  9. println( joiner(List("Programming", "Scala")) ) 

實際上,有兩個「重載」的jioner 方法。第二個方法使用了一個空格做爲「默認」分隔符。寫兩個函數彷佛有點浪費,若是咱們能消除第二個joiner 方法,在第一個jioner 方法裏爲separator 參數聲明一個默認值,那就太好了。事實上,在Scala 2.8 版本里,你能夠這麼作。

 
  1. // code-examples/TypeLessDoMore/string-util-v2-v28-script.scala  
  2. // Version 2 of "StringUtil" for Scala v2.8 only.  
  3. object StringUtil {  
  4.   def joiner(strings: List[String], separator: String = " "): String =  
  5.     strings.mkString(separator)  
  6. }  
  7. import StringUtil._  // Import the joiner methods.println(joiner(List("Programming", "Scala")))  

對於早些版本的Scala 還有另一種選擇。你可使用隱式參數,咱們會在《第8章 - Scala 函數式編程》的「隱式函數參數」章節討論。

2.8 版本的Scala 提供了另一種對方法參數列表進行加強,就是命名參數。咱們實際上能夠用多種方法重寫上一個例子的最後一行。下面全部的println 語句在功能上都是一致的。

 
  1. println(joiner(List("Programming", "Scala")))  
  2. println(joiner(strings = List("Programming", "Scala")))  
  3. println(joiner(List("Programming", "Scala"), " "))   // #1  
  4. println(joiner(List("Programming", "Scala"), separator = " ")) // #2  
  5. println(joiner(strings = List("Programming", "Scala"), separator = " ")) 

爲何這樣有用呢?第一,若是你爲方法參數選擇了好的名字,那麼你對那些函數的調用事實上爲每個參數記載了一個名字。舉例來講,比較註釋#1 和#2 的兩行。在第一行,第二個參數「 」的用處可能不是很明顯。在第二行中,咱們提供了參數名separator,同時也暗示了參數的用處。

第二個好處則是你能夠以任何順序指定參數的順序。結合默認值,你能夠像下面這樣寫代碼

 
  1. // code-examples/TypeLessDoMore/user-profile-v28-script.scala  
  2. // Scala v2.8 only.  
  3. object OptionalUserProfileInfo {  
  4.   val UnknownLocation = "" 
  5.   val UnknownAge = -1  
  6.   val UnknownWebSite = "" 
  7. }  
  8.  
  9. class OptionalUserProfileInfo(  
  10.   location: String = OptionalUserProfileInfo.UnknownLocation,  
  11.   age: Int         = OptionalUserProfileInfo.UnknownAge,  
  12.   webSite: String  = OptionalUserProfileInfo.UnknownWebSite)  
  13.  
  14. println( new OptionalUserProfileInfo )  
  15. println( new OptionalUserProfileInfo(age = 29) )  
  16. println( new OptionalUserProfileInfo(age = 29location="Earth") )  

OptionalUserProfileInfo 爲你的下一個Web 2.0 社交網站提供了「可選的」用戶概要信息。它定義了全部字段的默認值。這段腳本在建立實例的時候提供了0個或者更多的命名參數。而參數的順序倒是任意的。

在這個咱們展現的例子裏,常量值被用來做爲默認值。大多數支持默認參數的語言只容許編譯時能決定的常量或者值做爲默認值。然而,在Scala 裏,任何表達式均可以被做爲默認值,只要它能夠在被使用的時候正確編譯。好比說,一個表達式不能引用類或者對象主體內才被計算的實例字段,可是它能夠引用一個方法或者一個單例對象。

一個相似的限制是一個參數的默認表達式不能引用列表中的另一個參數,除非被引用的參數出如今列表的更前面,或者參數已經被級聯(咱們會在《第8章 - Scala 函數式編程》的「級聯」這一章節詳細討論)。

最後,還有一個對命名參數的約束就是一旦你爲一個方法掉哦那個指定了參數名稱,那麼剩下的在這個參數以後的全部參數都必須是命名參數。好比,new OptionalUserProfileInfo(age =29, "Earch") 就不能被編譯,由於第二個參數不是經過命名方式調用的。

咱們會在《第6章 - Scala 高級面向對象編程》中的「Case Class(案例類)」中看到另一個使用命名參數和默認參數的例子。

嵌套方法定義

方法定義也能夠被嵌套。這裏是一個階乘計算器的實現,咱們會使用一種常規的方法,經過調用第二個,嵌套的方法來完成計算。

 
  1. // code-examples/TypeLessDoMore/factorial-script.scala  
  2. def factorial(i: Int): Int = {  
  3.   def fact(i: Int, accumulator: Int): Int = {  
  4.     if (i <= 1)  
  5.       accumulator  
  6.     else  
  7.       fact(i - 1, i * accumulator)  
  8.   }  
  9.   fact(i, 1)  
  10. }  
  11.  
  12. println( factorial(0) )  
  13. println( factorial(1) )  
  14. println( factorial(2) )  
  15. println( factorial(3) )  
  16. println( factorial(4) )  
  17. println( factorial(5) )  

第二個方法遞歸地調用了本身,傳遞一個accumulator 參數,這個參數是計算結果累積的地方。注意,咱們當計數器i 達到1 的時候返回了累積的值。(咱們會忽略負整數。實際上這個函數在i<0 的時候會返回1 。)在嵌套方法的定義後面,factorial 以傳入值i 和初始accumulator 值1 來調用它。

就像不少語言中聲明局部變量同樣,一個嵌套方法盡在方法內部可見。若是你嘗試在factorial 以外去調用fact,你會獲得一個編譯錯誤。

你注意到了嗎,咱們兩次把i 做爲一個參數名字,第一次是在factorial 方法裏,而後是在嵌套的fact 方法裏。就像在其它許多語言中同樣,在fact 中的i 參數會屏蔽掉外面factorial 的i 參數。這樣很好,由於咱們在fact 中不須要在外面的i 的值。咱們只在第一次調用fact 的時候須要它,也就是在factorial 的最後。

那若是咱們須要使用定義在嵌套函數外面的變量呢?考慮下面的例子。

 
  1. // code-examples/TypeLessDoMore/count-to-script.scala  
  2. def countTo(n: Int):Unit = {  
  3.   def count(i: Int): Unit = {  
  4.     if (i <= n) {  
  5.       println(i)  
  6.       count(i + 1)  
  7.     }  
  8.   }  
  9.   count(1)  
  10. }  
  11. countTo(5) 

注意嵌套方法count 使用了做爲參數傳入countTo 的n 的值。這裏沒有必要把n 做爲參數傳給count。由於count 嵌套在countTo 裏面,因此n對於count 來講是可見的。

字段(成員變量)的聲明能夠用可見程度關鍵字來作前綴,就像Java 和C# 這樣的語言同樣。和非嵌套方法的生命相似,這些嵌套方法也能夠用這些關鍵字來修飾。咱們會在《第5章 - Scala 面向對象編程》中的「可見度規則」章節來討論可見度的規則和對應的關鍵字。

類型推斷

靜態類型書寫的代碼可能會很是冗長,考慮下面典型的Java 聲明。

 
  1. import java.util.Map;  
  2. import java.util.HashMap;  
  3. ...  
  4. Map intToStringMap = new HashMap(); 

咱們不得不兩次指明參數類型。(Scala 使用類型註解做爲顯式類型聲明的方式,好比HashMap。)

Scala 支持類型推斷(參考,例如[ 類型推斷] 和[Pierce2002,Benjamin C. Pierce, 類型與編程語言, 麻省理工出版社, 2002])。即便沒有顯示的類型註解,語言的編譯器仍能夠從上下文中分辨出至關多的類型信息。這裏是Scala 的聲明,使用了類型信息的推斷。

 
  1. import java.util.Map  
  2. import java.util.HashMap  
  3. ...  
  4. val intToStringMap: Map[Integer, String] = new HashMap 

回憶在第1章中Scala 使用方括號來指明範型類型參數。咱們在等號左邊指定了Map[Integer, String]。(咱們在例子中仍是繼續使用Java 的類型。)在右邊,咱們實例化了一個咱們實際須要的對象,一個HashMap,可是咱們不用重複地聲明類型參數。

再補充一點,假設咱們實際上並不關心實例的類型是否是Map (Java 的接口類型)。咱們只須要知道它是HashMap 類型。

 
  1. import java.util.Map  
  2. import java.util.HashMap  
  3. ...  
  4. val intToStringMap2 = new HashMap[Integer, String] 

這樣的聲明不須要在左邊指定類型註解,由於全部須要的類型信息都已經在右邊有了。編譯器自動給intToStringMap2 賦予HashMap[Integer, String] 類型。

類型推斷對方法也有用。在大多數狀況下,方法的返回類型能夠被推斷,因此「:」和返回類型能夠被省略。然而,對於全部的方法參數來講,類型註解是必須的。

像Haskell(參見,例如[O'Sullivan2009, Bryan O’Sullivan, John Goerzen, and Don Steward, Real World Haskell, O’Reilly Media, 2009] 這樣的純函數式語言使用相似於Hindley-Milner(參見[Spiewak2008] 獲取簡單摘要的解釋)的算法來作類型推斷。用這些語言寫出的代碼須要比Scala 更少的類型註解,由於Scala 的類型推斷算法得同時支持面向對象類型和函數式類型。因此,Scala 比Haskell 這樣的語言須要更多的類型註解。這裏有一份關於Scala 什麼時候須要顯式類型註解規則的總結。

顯式類型註解在什麼時候是必要的。

從實用性來說,你必須爲下列狀況提供顯式的類型註解:

1。變量聲明,除非你給變量賦予了一個值。(好比,val name = "Programming Scala")

2。全部的方法參數。(好比,def deposit(amount: Money)

3。下列狀況中的方法返回值:

a 當你在方法裏顯式調用return 的時候 (即便是在最後)。

b 當一個方法是遞歸的時候。

c 當方法是重載的,而且其中一個調用了另一個的時候。主調用的方法必須有一個返回類型的註解。

d 當推斷的返回類型比你所想要的更普通時,好比Any。

注意

Any 類型是Scala 類型結構的根類型(參見《第7章 - Scala 對象系統的更多細節》中的「Scala 類型結構」章節)。若是一段代碼意外地返回類一個Any 類型的值,那麼極可能類型推斷器不能算出須要返回的類型,因此選擇了最有可能的最一般的類型。

讓咱們來看一些須要顯式聲明方法返回類型的例子。在下面的腳本中,upCase 方法有一個有條件的返回語句,返回非0長度的字符串。

 
  1. // code-examples/TypeLessDoMore/method-nested-return-script.scala  
  2. // ERROR: Won't compile until you put a String return type on upCase.  
  3. def upCase(s: String) = {  
  4.   if (s.length == 0)  
  5.     return s    // ERROR - forces return type of upCase to be declared.  
  6.   else  
  7.     s.toUpperCase()  
  8. }  
  9.  
  10. println( upCase("") )  
  11. println( upCase("Hello") )  

運行這段腳本你會得到以下錯誤。

 
  1. ... 6: error: method upCase has return statement; needs result type  
  2.         return s  
  3.          ^ 

你能夠經過把方法第一行改爲以下樣子來修正這個錯誤。

 
  1. def upCase(s: String): String = { 

實際上,對於這段腳本,另一種解決辦法是刪除return 關鍵字。沒有它代碼也能夠很好的工做,可是它闡明瞭咱們的目的。

遞歸方法也須要顯式的返回類型。回憶咱們在上一章中「嵌套方法的定義」章節看到的factorial 方法。讓咱們來刪除嵌套的fact 方法的:Int 返回類型。

 
  1. // code-examples/TypeLessDoMore/method-recursive-return-script.scala  
  2. // ERROR: Won't compile until you put an Int return type on "fact".  
  3. def factorial(i: Int) = {  
  4.   def fact(i: Int, accumulator: Int) = {  
  5.     if (i <= 1)  
  6.       accumulator  
  7.     else  
  8.       fact(i - 1, i * accumulator)  // ERROR  
  9.   }  
  10.   fact(i, 1)  

如今不能編譯了。

 
  1. ... 9: error: recursive method fact needs result type  
  2.             fact(i - 1, i * accumulator)  
  3.              ^ 

重載的方法有時候也須要顯式返回類型。當一個這樣的方法調用另一個時,咱們必須給調用者加上返回類型,以下面的例子。

 
  1. // code-examples/TypeLessDoMore/method-overloaded-return-script.scala  
  2. // Version 1 of "StringUtil" (with a compilation error).  
  3. // ERROR: Won't compile: needs a String return type on the second "joiner".  
  4. object StringUtil {  
  5.   def joiner(strings: List[String], separator: String): String =  
  6.     strings.mkString(separator)  
  7.   def joiner(strings: List[String]) = joiner(strings, " ")   // ERROR  
  8. }  
  9. import StringUtil._  // Import the joiner methods.  
  10. println( joiner(List("Programming", "Scala")) ) 

兩個joiner 方法把一系列字符串串在一塊兒。第一個方法還接受一個參數來做爲分隔符。第二個方法調用第一個方法,而且傳入一個空格做爲「默認」分隔符。

若是你運行這段腳本,你會得到以下錯誤。

 
  1. ... 9: error: overloaded method joiner needs result type  
  2. def joiner(strings: List[String]) = joiner(strings, "") 

由於第二個jioner 方法調用了第一個,它須要一個顯示的String 返回類型。它必須看起來像這樣。

 
  1. def joiner(strings: List[String]): String = joiner(strings, " ") 

最後的一種場景的關係可能比較微妙,比你指望的類型更通用的類型可能會被推斷返回。你一般會把函數返回值賦給擁有更特定類型變量的時候遇到這樣的錯誤。好比,你但願得到一個String,可是函數推斷返回類型爲Any。讓咱們來看一個設計好的例子來反映會發生這種bug 的場景。

 
  1. // code-examples/TypeLessDoMore/method-broad-inference-return-script.scala  
  2. // ERROR: Won't compile. Method actually returns List[Any], which is too "broad".  
  3. def makeList(strings: String*) = {  
  4.   if (strings.length == 0)  
  5.     List(0)  // #1  
  6.   else  
  7.     strings.toList  
  8. }  
  9. val list: List[String] = makeList()  // ERROR 

運行這段腳本會得到以下錯誤。

 
  1. ...11: error: type mismatch;  
  2. found   : List[Any]  
  3. required: List[String]  
  4. val list: List[String] = makeList()  
  5.                           ^ 

咱們但願makeList 能返回一個List[String],可是當strings.length 等於0 時,咱們錯誤地假設List(0) 是一個空的列表而且將其返回。實際上,咱們返回了一個有一個元素0 的List[Int] 對象。咱們應該返回List()。由於else 表達式後返回了strings.toList 的返回值List[String],方法的推斷返回類型就是離List[Int] 和List[String] 最近的公共父類型List[Any]。主意,編譯錯誤並非在函數定義的時候出現。咱們只有當把makeList 返回值賦給一個List[String] 類型得變量的時候纔看到這個錯誤。

在這種狀況下,修正bug 纔是正道。另外,有時候並無bug,只是編譯器須要一些顯式聲明的「幫助」來返回正確的類型。研究一下那些彷佛返回了非指望類型的方法。以咱們的經驗,若是你修改了方法後發現它返回了比指望的更通常的類型,那麼在這種狀況下加上顯式返回類型聲明。

另外一種避免這樣的麻煩的方式是永遠爲方法返回值聲明類型,特別是爲公用API 定義方法的時候。讓咱們從新來看咱們的StringUtil 例子來理解爲何顯式聲明是一個好主意(從[Smith2009a] 改寫)。

這裏是咱們的StringUtil 「API",和一個新的方法,toCollection。

 
  1. // code-examples/TypeLessDoMore/string-util-v3.scala  
  2. // Version 3 of "StringUtil" (for all versions of Scala).  
  3. object StringUtil {  
  4.   def joiner(strings: List[String], separator: String): String =  
  5.     strings.mkString(separator)  
  6.   def joiner(strings: List[String]): String = strings.mkString(" ")  
  7.   def toCollection(string: String) = string.split(' ')  

toCollection 方法以空格來分割字符串,而後返回一個包含這些子字符串的Array(數組)。返回類型是推斷出的,咱們會看到,這會是一個潛在的問題所在。這個方法是計劃中的,可是會展現咱們的重點。下面是一個使用StringUtil 的這個方法的客戶端。

 
  1. // code-examples/TypeLessDoMore/string-util-client.scala  
  2. import StringUtil._  
  3. object StringUtilClient {  
  4.   def main(args: Array[String]) = {  
  5.     args foreach { s => toCollection(s).foreach { x => println(x) } }  
  6.   }  

若是你用scala 編譯這些文件,你就能像這樣運行客戶端。

 
  1. $ scala -cp ... StringUtilClient "Programming Scala"  
  2. Programming  
  3. Scala 

注意

類路徑參數 -cp,使用了scalac 寫出class 文件的目錄,默認是當前目錄(好比,使用-cp.)。若是你使用了下載的代碼示例中的編譯過程,那些class 文件會被寫到build 目錄中區(使用scalac -d build ...)。在這個例子裏,使用 -cp build.

這個時候,一切都工做正常。可是如今想象一下代碼庫擴大之後,StringUtil 和它的客戶端被分別編譯而後捆綁到不一樣的jar 文件中去。再想象一下StringUtil 的維護者決定返回一個List 來替代原來的默認值。

 
  1. object StringUtil {  
  2.   ...  
  3.   def toCollection(string: String) = string.split(' ').toList  // changed!  

惟一的區別是最後的對toList 的調用,把一個Array 轉換成了List。從新編譯StringUtil 而且部署爲jar 文件。而後運行相同的客戶端,先不要從新編譯。

 
  1. $ scala -cp ... StringUtilClient "Programming Scala"  
  2. java.lang.NoSuchMethodError: StringUtil$.toCollection(...  
  3.   at StringUtilClient$$anonfun$main$1.apply(string-util-client.scala:6)  
  4.   at StringUtilClient$$anonfun$main$1.apply(string-util-client.scala:6)  
  5. ... 

發生了什麼?當客戶端被編譯的時候,StringUtil.toCollection 返回了一個Array。而後toCollection 被修改成返回一個List。在兩個版本里,方法返回值都是被推斷出來的。所以,客戶端也必須被從新編譯。

然而,若是顯式地聲明返回類型是Seq,做爲Array 和List 的共同父類型,這樣的實現就不會對客戶端要求從新編譯。

注意

當開發獨立於客戶端的API 的時候,顯式地聲明方法返回類型,而且儘量使用更通常的返回類型。這在API 被聲明爲抽象方法時尤爲重要。(參見,好比《第4章 - 特性》。)

還有另一種場景須要考慮集合聲明的使用,好比val map = Map(),就像下面這個例子。

 
  1. val map = Map()  
  2. map.update("book", "Programming Scala")  
  3. ... 3: error: type mismatch;  
  4. found   : java.lang.String("book")  
  5. required: Nothing  
  6. map.update("book", "Programming Scala")  
  7.             ^ 

發生了什麼?範型類型Map 的類型參數在map 被建立時被推斷爲[Nothing, Nothing]。(咱們會在《第7章 - Scala 對象系統》的「Scala 類型結構」章節討論Nothing。可是它的名字自己就解釋了本身!)咱們嘗試插入一對不匹配的String,String 鍵值對。叫它拿都去不了的地圖吧!解決方案是,在初始化map 聲明的時候指出參數類型,例如val map = Map[String, String]() 或者指定初始值以便於map 參數被推斷,例如val map = Map("Programming"->"Scala")。

最後,還有一個推斷返回類型可能致使不可預知的使人困擾的結果[Scala 提示]的詭異行爲。考慮下面的scala 對話例子。

 
  1. scala> def double(i: Int) { 2 * i }  
  2. double: (Int)Unit  
  3. scala> println(double(2))  
  4. () 

爲何第二個命令打印出() 而不是4?仔細看scala 解釋器給出的第一個命令的返回值,double: (Int)Unit。咱們定義了一個方法叫double,接受一個Int 參數,返回Unit。方法並不像咱們指望的那樣返回Int。

形成這樣意外結果的緣由是在方法定義中缺乏的等於號。下面是咱們實際上須要的定義。

 
  1. scala> def double(i: Int) = { 2 * i }  
  2. double: (Int)Int  
  3. scala> println(double(2)) 

注意double 主體前的等於號。如今,輸出說明咱們定義了一個返回Int 的double,第二個命令完成了咱們指望的工做。

這樣的行爲是有緣由的。Scala 把主體以前的部分包含等於號做爲函數定義,而一個函數在函數式編程中永遠都有返回值。另外一方面來講,當Scala 看到一個函數主體而沒有等於號前綴時,它就假設程序員但願這個方法變成一個「過程」定義,但願得到由返回值Unit 帶來的反作用。而在實際中,結果每每是程序員簡單地忘記了插入等於號!

警告

當方法的放回類型被推斷而你又沒有在方法主體的大括號前使用等於號的時候,即便方法的最後一個表達式是另一個類型的值,Scala 也會推斷出一個Unit 返回類型。

順便說一句,以前咱們修正bug 前打印出來的() 是哪裏來的?事實上這是Unit 類型單體實例的真正名字!(這個名字是函數式編程的協定。)

常值

一個對象常常會用一個常值來初始化,好比val book = "Programming Scala"。下面咱們來討論一下Scala 支持的常值種類。這裏咱們只討論字符常值。咱們會在後面遇到函數(被用做值,而不是成員方法),tuples,Lists,Maps 等的常值語法的時候再繼續討論。

整數(Integer)

整數常值能夠表達爲:十進制,十六進制,或者八進制。細節總結參見「表2.1, 整數常值」

種類  格式  例子
十進制 0 ,或者非零數字後面跟隨0 個或者多個十進制字符 (0 - 9) 0, 1, 321
十六進制 0x 後面跟隨一個或多個十六進制字符 (0-9, A-F, a-f) 0xFF, 0x1a3b
八進制 0 後面跟隨一個或多個八進制字符 (0-7) 013, 077

對於長整型值,必須在常值的後面加上L 或者l 字符。不然會被斷定爲普通整型。整數的有效值由被賦值的變量類型來決定。表2.2,「整型數的容許範圍(包括邊界)」 定義了整數的極限,包括邊界值。

目標類型  最小值 (包括) 最大值 (包括)
Long(長整型) −263 263 - 1
Int (整型) −231 231 - 1
Short (短整型) −215 215 - 1
Char (字符) 216 - 1
Byte (字節) −27 27 - 1

若是一個整數的值超出了容許範圍,就會發生編譯錯誤,好比下面這個例子。

 
  1. scala > val i = 12345678901234567890 
  2. :1: error: integer number too large  
  3.        val i = 12345678901234567890 
  4. scala> val b: Byte = 128 
  5. :4: error: type mismatch;  
  6. found   : Int(128)  
  7. required: Byte  
  8.        val b: Byte = 128 
  9.                      ^  
  10.  
  11. scala> val b: Byte = 127 
  12. b: Byte = 127 


 浮點數(Float)

Float 由0 個或多個數字,加上一個點號,再加上0 個或多個數字組成。若是在點號前面沒有數字,好比數字比1.0 要小,那麼在點號後面必須有一個或者多個數字。對於浮點數,須要在常值的最後加上F 或者f 。不然默認斷定爲雙精度浮點數(Double)。你能夠選擇給一個雙精度浮點數加上D 或者d。

浮點數能夠用指數方法表達。指數部分的格式是e 或者E,加上一個可選的+或者-,再加上一個或多個數字。
 
這裏有一些浮點數的例子。

 
  1. 0.  
  2. .0  
  3. 0.0  
  4. 3.  
  5. 3.14  
  6. .14  
  7. 0.14  
  8. 3e5  
  9. 3E5  
  10. 3.E5  
  11. 3.e5  
  12. 3.e+5  
  13. 3.e-5  
  14. 3.14e-5  
  15. 3.14e-5f  
  16. 3.14e-5F  
  17. 3.14e-5d  
  18. 3.14e-5D  

Float 遵循了IEEE 754 32位單精度二進制浮點數值的規範。Double 遵循了IEEE 754 64位雙精度二進制浮點數值的規範。

警告

爲了防止解析時的二義性,若是一個浮點數後面跟隨着一個字母開頭的符號,你必須在浮點數後面跟隨至少一個空格。好比,表達式1.toString 返回整數1 的字符串形式,而1. toString 則返回浮點數1.(0) 的字符串形式。

布爾值

布爾值能夠是true (真) 或者false (假)。被賦值的變量的類型會被推斷爲Boolean。

 
  1. scala> val b1 = true 
  2. b1: Boolean = true 
  3.  
  4. scala> val b2 = false 
  5. b2: Boolean = false 

字符常值

一個字符常值是一個單引號內的可打印的Unicode 字符或者一個轉義序列。一個能夠用Unicode 值0 到255 表示的字符也能夠用一個八進制轉義來表示:一把反斜槓加上最多3個八進制字符序列。若是在字符或者字符串中反斜槓後面不是一個有效的轉義序列則會出現編譯錯誤。

這裏有一些例子.

 
  1. ’A’  
  2. ’\u0041’  // 'A' in Unicode  
  3. ’\n’  
  4. '\012'    // '\n' in octal  
  5. ’\t’  

有效的轉義序列參見:表格2.3 「字符轉義序列」

序列 Unicode 含義
\b \u0008 backspace BS (退格)
\t \u0009 horizontal tab HT (水平Tab)
\n \u000a linefeed LF (換行)
\f \u000c form feed FF (換頁)
\r \u000d carriage return CR (光標返回)
\" \u0022 double quote " (雙引號)
\’ \u0027 single quote ’ (單引號)
\\ \u0009 backslash \(反斜槓)
字符串常值

一個字符串是在雙引號內或者3重雙引號內的一系列字符,好比"""..."""。

對於雙引號內的字符串,容許的字符集和字符常值的範圍同樣。不過,若是字符傳中出現了雙引號,那麼它必須用一個字符來轉義表示。這裏有一些例子。

 
  1. "Programming\nScala"  
  2. "He exclaimed, \"Scala is great!\""  
  3. "FirsttSecond"  

被三重雙引號包含的字符串也被稱爲多行字符串。這些字符串能夠跨越好幾行;換行符也會被做爲字符串的一部分。它們能夠包含任何字符,包括一個或者兩個雙重雙引號,可是不能有三重雙引號。它們對於包含非有效Unicode 或者轉義序列字符的字符串來講頗有用,就和使用表2.3 字符轉義序列裏的有效序列的同樣。一個典型的例子就是正則表達式,咱們會在《第3章 - Scala 本質》中討論它。不過,若是轉義字符在這裏不會被解釋(轉義)。

這裏有3個示例字符串.

 
  1. """Programming\nScala"""  
  2. """He exclaimed, "Scala is great!" """  
  3. """First line\n  
  4. Second line\t  
  5.  
  6. Fourth line"""  

主意咱們在第二個例子裏面必須在結尾的""" 前加一個空格防止解析錯誤。若是想要把結束「Scala is great」 的引號轉義,好比「Scala is great!\」 ,是不行的。

拷貝而後粘貼這些字符串到scala 解釋器。對前面的字符串例子也作同樣的事情。看看他們是如何被解釋的。

符號常值

Scala 支持符號,就是一些限定字符串。若是兩個符號有一樣的「名字」,好比一樣的字符序列,那麼它們本質上會指向內存中一樣的對象。符號在Scala 中的使用頻率要低於其它語言,好比Ruby, Smalltalk 和Lisp。它們對於Map 的Key 來講比字符串更有用。

一個符號常值是一個單引號',跟着一個字母,再跟着0個或多個數字和字母。注意,像'1 這樣的表達式是無效的,由於編譯器會認爲這是一個不完整的字符常值。

符號常值'id 是表達式scala.Symbol("id") 的簡寫。

注意

若是你想建立包含空格的符號,使用這樣的方式:scala.Symbol("Programming Scala ")。全部的空格都會被保留。

元組(Tuples)

你有多少次想要給一個方法返回兩個或者更多的值?在許多語言,好比Java 中,你只有不多的選擇,並且都不是上選。你能夠傳入一個參數給方法,而後爲全部或者一些返回值而修改它,這看起來很難看。或者,你能夠聲明一些小的結構性類,可讓你保存兩個或更多的值,而後返回那個類的一個實例。

Scala 則支持元組(tuples),一個包含兩個或更多元素的組合,一般能夠用一對小括號加上逗號分隔的元素序列來建立它們,好比(x1, x2, ...)。xi 的類型相互之間沒有任何關係,你可使用混合的或者一致的類型。這些「組合」 會被實例化成scala.TupleN 的實例,N 是裏面元素的數目。Scala API 定義了N 從1 到22 的22個單獨的TupleN 類。Tuple 實例是隻讀的,第一類的值。因此你能夠把他們賦值給變量,把他們當值來傳遞,做爲返回值由方法來返回。

下面的例子展現了Tuple 的使用.

 
  1.  
  2. // code-examples/TypeLessDoMore/tuple-example-script.scala  
  3.  
  4. def tupleator(x1: Any, x2: Any, x3: Any) = (x1, x2, x3)  
  5.  
  6. val t = tupleator("Hello", 1, 2.3)  
  7. println( "Print the whole tuple: " + t )  
  8. println( "Print the first item:  " + t._1 )  
  9. println( "Print the second item: " + t._2 )  
  10. println( "Print the third item:  " + t._3 )  
  11.  
  12. val (t1, t2, t3) = tupleator("World", '!', 0x22)  
  13. println( t1 + " " + t2 + " " + t3 )  
  14.  

運行這段腳本會得到以下輸出。

 
  1. Print the whole tuple: (Hello,1,2.3)  
  2. Print the first item:  Hello  
  3. Print the second item: 1  
  4. Print the third item:  2.3  
  5. World ! 34  

方法tupleator 簡單地返回一個包含輸入參數的3元組。第一個語句使用這個方法把返回的元組賦值給變量t。下面4個語句用各類方法來打印t。第一個打印語句調用了Tuple3.toString,把元素周圍加上括號。下面的3個語句分別打印了t 的各個元素。表達式t._N 獲取第N 個元素,從1開始,不是0 (這個選擇遵循了函數式編程的習慣)。

最後兩行展現了咱們能夠把元組表達式做爲左值。咱們聲明3個val,t1, t2, t3,來存儲在元組裏的每個元素。本質上講,元組的元素會被自動提取出來。
 
注意咱們是怎麼在元組裏混合類型的。你可使用在《第1章 - 從0 分到60 分,Scala 介紹》裏介紹的scala 交互模式來看得更清楚一些。
 
不用任何腳本參數來調用scala 命令。在scala> 提示符後,輸入val t = ("Hello", 1, 2.3) 而後觀察輸出結果,你會看到元組裏各個元素的類型。

 
  1. scala> val t = ("Hello",1,2.3)  
  2. t: (java.lang.String, Int, Double) = (Hello,1,2.3) 

值得注意的是有許多方法能夠定義一個元組。咱們已經使用了最多見的括號語法,可是你能夠可使用兩個值之間的箭頭操做符,或者和tuple 有關的類的工廠方法來創建元組。

 
  1. scala> 1 -> 2  
  2. res0: (Int, Int) = (1,2)  
  3.  
  4. scala> Tuple2(1, 2)  
  5. res1: (Int, Int) = (1,2)  
  6.  
  7. scala> Pair(1, 2)  
  8. res2: (Int, Int) = (1,2)  
  9.    
  10.  

Option, Some, 和 None: 避免使用 null

咱們會在《第7章 - Scala 對象系統》中的「Scala 類型結構」 章節來討論標準類型結構。可是,咱們如今要理解3個有用的類,即Option 和它的兩個子類Some 和None。

大多數語言都有一個特殊的關鍵字或者對象來表示一個對象引用的是「無」。在Java,它是null;在Ruby,它是nil。在Java 裏,null 是一個關鍵字,不是一個對象,因此對它調用任何方法都是非法的。可是這對語言設計者來講是一件使人疑惑的選擇。爲何要在程序員但願返回一個對象的時候返回一個關鍵字呢?
 
爲了讓全部東西都是對象的目標更加一致,也爲了遵循函數式編程的習慣,Scala 鼓勵你在變量和函數返回值可能不會引用任何值的時候使用Option 類型。在沒有值的時候,使用None,這是Option 的一個子類。若是有值能夠引用,就使用Some 來包含這個值。Some 也是Option 的子類。

注意

None 被聲明爲一個對象,而不是一個類,由於咱們只須要它的一個實例。這樣,它多少有點像null 關鍵字,但它倒是一個實實在在的,有方法的對象。

你能夠在下面這個例子裏看到Option,Some 和None 通力協做。咱們會建立一張美國的各州首府的Map。

 
  1. // code-examples/TypeLessDoMore/state-capitals-subset-script.scala  
  2.  
  3. val stateCapitals = Map(  
  4.   "Alabama" -> "Montgomery",  
  5.   "Alaska"  -> "Juneau",  
  6.   // ...  
  7.   "Wyoming" -> "Cheyenne")  
  8.  
  9. println( "Get the capitals wrapped in Options:" )  
  10. println( "Alabama: " + stateCapitals.get("Alabama") )  
  11. println( "Wyoming: " + stateCapitals.get("Wyoming") )  
  12. println( "Unknown: " + stateCapitals.get("Unknown") )  
  13.  
  14. println( "Get the capitals themselves out of the Options:" )  
  15. println( "Alabama: " + stateCapitals.get("Alabama").get )  
  16. println( "Wyoming: " + stateCapitals.get("Wyoming").getOrElse("Oops!") )  
  17. println( "Unknown: " + stateCapitals.get("Unknown").getOrElse("Oops2!") )  

用來方便定義Map 的鍵值對的-> 語法會在《第7章 - Scala 對象系統》的「預約義對象」 章節被詳細討論。如今,咱們但願專一在在兩組println 語句上,它們展現了咱們在從map 取值的時候會發生什麼。若是你用scala 命令來運行這段腳本,你會得到以下輸出。

 
  1. Get the capitals wrapped in Options:  
  2. Alabama: Some(Montgomery)  
  3. Wyoming: Some(Cheyenne)  
  4. Unknown: None  
  5. Get the capitals themselves out of the Options:  
  6. Alabama: Montgomery  
  7. Wyoming: Cheyenne  
  8. Unknown: Oops2! 

第一組println 語句在get 返回的實例上隱式調用了toString。由於Map.get 返回的值會在有匹配的值時被自動包在Some 裏,因此咱們其實是調用了Some 或者None 的實例的toString。主意Scala 庫並不在Map 裏存儲Some,它只在值被取出的時候被外包。相反的,若是咱們要取的Key 沒有對應的值,那麼None 對象就會被返回,而不是null。從這3個語句裏的最後一個println 能夠看出來。

第二組println 語句展現了更多細節。在調用Map.get 以後,它們還對每一個Option 實例調用了get 或者getOrElse 來獲取它們實際包含的值。Option.get 要求Option 不爲空,也就是說,這個Option 實例必須是一個Some。在這個例子裏,get 返回被Some 包含道的值,正如println 所展現的,是阿拉巴馬的首府。然而,若是這個Option 是一個None,None.get 會拋出一個NoSuchElementException 異常。
咱們也能夠選用另一個方法,getOrElse,就像最後兩個println 展現的。這個方法在這個Option 是Some 的實例時返回對應的值,而在是None 的實例時返回傳入的參數。換句話說,傳入getOrElse 的參數其實是默認返回值。

因此,getOrElse 是二者中比較保守的方法。它避免了可能的異常。咱們會在《第13章 - 程序設計》中的「異常和替代方法」 章節討論get VS. getOrElse 的優缺點。

注意,由於Map.get 返回一個Option,它也就本身說明了指定的Key 可能不會有匹配的值。map 處理這種狀況的方法是返回None。許多語言會在沒有實際值返回時選擇返回null(或者等價物)。你可能會根據經驗來指望一個null。使用Option 使得方法的行爲在簽名中更加明顯,也就是更加的「自我說明」。
 
實際上,多虧Scala 的靜態類型,你並不能錯誤地嘗試在一個可能爲null 的值上調用方法。雖然在Java 中這是個很容易犯的錯誤,它在Scala 卻通不過編譯,由於你必須先把值從Option 中取出來。因此,Option 的使用極強地鼓勵了更加彈性的編程習慣。
 
由於Scala 運行在JVM 和.NET 上,並且必須和其它的庫合做,因此Scala 必須支持null。然而,你應該儘可能在你的代碼中避免使用null。Tony Hoare,在1965年工做在一個名叫ALGOL W 的面嚮對象語言上時發明了null 引用,他稱他的發明爲一個「百萬美金的錯誤」[Hoare2009]。別再爲這個名言作貢獻了:)
 
那麼,你該如何寫一個返回Option 的方法呢?這裏有一個可能的被Map 的具體子類使用的get 的實現(Map.get 自己是抽象的)。要獲取更穩健的版本,參考Scala 庫源代碼發佈包中的scala.collection.immutable.HashMap 的get 的實現。

 
  1. def get(key: A): Option[B] = {  
  2.   if (contains(key))  
  3.     new Some(getValue(key))  
  4.   else  
  5.     None  

contains 方法也在Map 中定義。它在map 包含特定key 對應的值時返回true。getValue 方法應該是一個內部方法,用來從底層存儲的實現中獲取對應的值,不管它是什麼。

主意getValue 的返回值是如何被包裹在Some[B] 裏的(B 的類型是推斷的)。然而,若是調用contains(key) 返回false,那麼None 對象就會被返回。

你能夠在你的方法返回Option 的時候使用一樣的方式。咱們會在下面的章節中繼續探索Option 的其它用處。它在Scala 代碼中的廣泛使用使之成爲一個重要的須要掌握的概念。

用文件和名稱空間來組織代碼

Java 用包來處理名稱空間,Scala 借鑑了這一律念,可是提供了更加靈活的語法。就像文件名不必定要和類型名稱一致同樣,包的結構和文件夾結構也不必定要一致。因此,你能夠獨立於文件的物理結構來構建你的包。

下面的例子在包com.example.mypkg 下定義了類MyClass,使用的是常規的Java 語法。

 
  1. // code-examples/TypeLessDoMore/package-example1.scala  
  2. package com.example.mypkg  
  3. class MyClass {  
  4.   // ...  
  5. }  
  6.  

下一個例子展現如何使用Scala 包的嵌套語法來定義包。這和C# 的namespace (名稱空間)語法以及Ruby 中相似namespace 的modules (模塊)語法。

 
  1. // code-examples/TypeLessDoMore/package-example2.scala  
  2.  
  3. package com {  
  4.   package example {  
  5.     package pkg1 {  
  6.       class Class11 {  
  7.         def m = "m11" 
  8.       }  
  9.       class Class12 {  
  10.         def m = "m12" 
  11.       }  
  12.     }  
  13.  
  14.     package pkg2 {  
  15.       class Class21 {  
  16.         def m = "m21" 
  17.         def makeClass11 = {  
  18.           new pkg1.Class11  
  19.         }  
  20.         def makeClass12 = {  
  21.           new pkg1.Class12  
  22.         }  
  23.       }  
  24.     }  
  25.  
  26.     package pkg3.pkg31.pkg311 {  
  27.       class Class311 {  
  28.         def m = "m21" 
  29.       }  
  30.     }  
  31.   }  
  32. }  
  33.  

在包com.example 下咱們定義了另外兩個包pkg1 和pkg2。而在這兩個包下咱們又一共定義了3個類。類Class21 的方法makeClass11 和makeClass12 舉例說明了如何引用其它包內的類型。你也能夠用它們的完整路徑com.example.pkg1.Class11 和com.example.pkg1.Class12 來引用這些類。

包pkg3.pkg31.pkg311 展現了在一個語句裏連接幾個包的語法。你大可沒必要給每個包都寫一個單獨的package 語句。

咱們遵循Java 的習慣,給Scala 庫的根包 取名爲scala。

警告

Scala 不容許在scala 解釋器直接運行寫有包聲明語句的腳本。緣由是解釋器在把腳本編譯成字節碼以前必須先把腳本語句轉換爲有效的Scala 代碼。參見《第14章 - Scala 工具,庫和IDE 支持》的「Scala 命令行工具」 章節獲取詳細信息。

導入類型和它們的成員

要使用包裏面的類型,你必須導入它們,就像你在Java 或相似語言中作的同樣。然而,和Java 相比,Scala 有更多的選項供你選擇。下面的例子舉例說明了幾種導入Java 類型的方式。

 
  1. // code-examples/TypeLessDoMore/import-example1.scala  
  2. import java.awt._  
  3. import java.io.File  
  4. import java.io.File._  
  5. import java.util.{Map, HashMap}  
  6.  

你能夠經過使用下劃線_ 做爲通配符導入一個包裏的全部類型,如第一行。你也能夠導入單個的Java 或者Scala 類型,例如第二行。

Java 使用星號* 做爲通配符來匹配包裏的全部類型或者在作靜態導入時導入類型內的全部靜態成員。在Scala 裏,星號被容許做爲函數名的一部分,因此咱們使用_ 來做爲通配符,正如咱們前面看到的同樣。

再看第三行,你能夠導入Java 類型中全部的靜態成員和字段。若是java.io.File 其實是一個Scala 對象,那麼這一行會導入那個對象的全部方法和字段,正如咱們前面所討論過的同樣。

最後,你能夠有選擇地導入你所關心的類型。在第四行,咱們只從java.util 包內導入了java.util.Map 和java.util.HashMap 類型。比較這樣的一行導入語句和以前在「類型信息推斷」章節的第一個例子中的兩行導入語句,它們在功能上是同樣的。

下一個例子展現了更多import 語句的高級用法。

 
  1. // code-examples/TypeLessDoMore/import-example2-script.scala  
  2. def writeAboutBigInteger() = {  
  3.   import java.math.BigInteger.{  
  4.     ONE => _,  
  5.     TEN,  
  6.     ZERO => JAVAZERO }  
  7.  
  8.   // ONE is effectively undefined  
  9.   // println( "ONE: "+ONE )  
  10.   println( "TEN: "+TEN )  
  11.   println( "ZERO: "+JAVAZERO )  
  12. }  
  13.  
  14. writeAboutBigInteger()  

這個例子展現了兩個特性。第一,咱們能夠把import 語句放在任何想要的地方,而不像Java 要求的那樣僅僅是文件的最上面。這個特性容許咱們定義更加狹小的導入空間。好比說,咱們不能在方法的定義範圍外引用導入的BigInteger 類型。另外一個優點是,它把import 語句放在了離實際須要的對象更近的地方。

第二個特性是,咱們能夠重命名導入的項目。首先,常量java.math.BigInteger.ONE 被重命名爲下劃線通配符。它使得ONE 對於其導入域來講變得不可見,無效。這對於想導入全部東西但有幾個除外的項目的時候頗有用。

而後,java.math.BigInteger.TEN 常量被導入,沒有重命名,因此能夠簡單地經過TEN 來引用它。

最後,java.math.BigInteger.ZERO 常量被賦予一個別名JAVAZERO。

在你想給一個東西一個更方便的名字,或者想避免和這個域下的其餘相同名稱的二義性的時候,別名會很是有用。

導入是相對的

有一件關於導入必需要知道的是,它們是相對的。主意下面的導入語句的註釋。

 
  1. // code-examples/TypeLessDoMore/relative-imports.scala  
  2.  
  3. import scala.collection.mutable._  
  4. import collection.immutable._         // Since "scala" is already imported  
  5. import _root_.scala.collection.jcl._  // full path from real "root"  
  6.  
  7. package scala.actors {  
  8.   import remote._                     // We're in the scope of "scala.actors"  
  9. }  
  10.  

注意在scala.actor 包的最後嵌入的那個導入語句是相對於scala.actors 包的域的。

[ScalaWiki] 在這裏有另一個例子。

雖然不多在導入的相對性上會遇到什麼麻煩,可是這個方便的特性有時候確實會讓人大吃一驚,特別是當你習慣於Java 那樣的絕對導入的時候。若是你獲得一個神祕的編譯錯誤指出某個包沒有找到,檢查一下導入語句的相對性,或者加上_root_. 前綴。其實,你可能會看到某些IDE 或者工具在你的代碼裏插入import _root_... 語句。如今你知道是什麼意思了。

警告

記住import 語句是相對的,不是絕對的。要建立絕對路徑,以_root_. 來做爲開頭。

抽象類型和參數化類型

咱們在《第1章 - 從0分到60 分:Scala 介紹》的「初嘗Scala」 章節中提到,Scala 支持參數化類型,和Java 中的範型很像。(咱們能夠交換使用這兩個術語,可是在Scala 社區中人們使用「參數化類型」更多,而在Java 社區中則是「範型」更多。)最明顯的差異就是語法,Scala 使用方括號[],而Java 使用尖括號<>。

舉例來講,一個字符串列表能夠被聲明爲以下形式。

 
  1. val languages: List[String] = ... 

和Java 的範型還有其它重要的區別,咱們會在《第12章 - Scala 類型系統》中的「理解參數化類型」章節進行探討。

如今,咱們在《第12章 - Scala 類型系統》進行更深刻的解釋以前要介紹另一個有用的細節。若是你在Scaladocs 中看一下scala.List 的聲明,你會發現它的聲明是寫成這樣的 ... class List[+A]。在A 前面的加號+ 意味着若是B 是A 的子類型,那麼List[B] 就是List[A] 的子類型。若是在類型參數以前有一個減號-,那麼剛好相反,若是聲明爲Foo[-A],則Foo[B] 會是Foo[A] 的父類型。

Scala 支持另外另外一種類型抽象機制稱爲抽象類型,在許多函數式編程語言中都有使用,好比Haskell。抽象類型在Java 引入範型時也被考慮過引入。咱們如今就來介紹它,由於你會在咱們深刻《第12章 - Scala 類型系統》以前就見到不少的例子。要了解兩種機制更細緻的比較,參見[Bruce1998(Kim Bruce,Martin Odersky,和Philip Wadler,「虛擬類型的一種靜態的安全的替代方式」,)]。

抽象類型能夠解決許多參數類型能夠解決的設計問題。然而,當兩種機制重疊時,卻並很少餘。每一種機制都有對特定設計問題的強項和弱點。

這裏是使用抽象類型的一個例子。

 
  1. // code-examples/TypeLessDoMore/abstract-types-script.scala  
  2.  
  3. import java.io._  
  4.  
  5. abstract class BulkReader {  
  6.   type In  
  7.   val source: In  
  8.   def read: String  
  9. }  
  10.  
  11. class StringBulkReader(val source: String) extends BulkReader {  
  12.   type In = String 
  13.   def read = source 
  14. }  
  15.  
  16. class FileBulkReader(val source: File) extends BulkReader {  
  17.   type In = File 
  18.   def read = {  
  19.     val in = new BufferedInputStream(new FileInputStream(source))  
  20.     val numBytes = in.available()  
  21.     val bytes = new Array[Byte](numBytes)  
  22.     in.read(bytes, 0, numBytes)  
  23.     new String(bytes)  
  24.   }  
  25. }  
  26.  
  27. println( new StringBulkReader("Hello Scala!").read )  
  28. println( new FileBulkReader(new File("abstract-types-script.scala")).read )  

用scala 來運行這段腳本會獲得以下輸出。

 
  1. Hello Scala!  
  2. import java.io._  
  3.  
  4. abstract class BulkReader {  
  5. ...  

抽象類型BulkReader 聲明瞭3個抽象成員,一個名爲In 的類型,一個val 字段source,和一個read 方法。和在Java 中同樣,Scala 中實例只能從具體類中建立,實體類必須有全部成員的定義。

繼承自它的子類StringBulkReader 和FileBulkReader 提供了三個抽象成員的實際定義。咱們會在《第5章 - Scala 基礎面向對象編程》中涵蓋類聲明的細節,在《第6章 - Scala 高級面向對象編程》的「在類和特性中重寫成員」 章節中涵蓋重寫成員聲明的細節。

如今,主意type 字段工做起來很像參數化類型的參數。實際上,咱們能夠重寫這個例子,只是有些許不一樣。

 
  1. abstract class BulkReader[In] {  
  2.   val source: In  
  3.   ...  
  4. }  
  5.  
  6. class StringBulkReader(val source: String) extends BulkReader[String] {...}  
  7. class FileBulkReader(val source: File) extends BulkReader[File] {...}  

只是對於參數化類型,若是咱們定義了In 類型爲String,那麼source 字段也必須被定義爲String。主意StringBulkReader 的read 方法簡單地返回了source 字段,而FileBulkReader 的read 方法讀取了文件的具體內容。

正如[Bruce1998] 演示的,參數化類型是集合的最佳選擇,也是Java 代碼中使用的最頻繁的地方。而抽象類型則對於類型族和其餘類型的場景最有用。

咱們會在《第12章 - Scala 類型系統》中討論Scala 抽象類型的細節。好比,咱們會看到如何限定可能會被使用的具體類型。

保留字(關鍵字)

表 2.4 「保留字」列出了Scala 的保留字,也被稱爲關鍵字,而且簡要描述了他們在[ScalaSpec2009] 定義下是如何被使用的。

表2.4 保留字

關鍵字 描述 參見…
abstract 聲明抽象類型。和Java 不同的是,這個關鍵字對於抽象成員來講一般不是必要的。 (類和對象基礎)《第5章 - Scala 基礎面向對象編程》
case 在一個match 表達式裏開始一個case 語句塊。 (模式匹配) 《第3章 - Scala 本質》
catch 開始一個語句塊來截取拋出的異常。 (使用try,catch,finally 語句塊)《第3章 - Scala 本質》
class 開始一個類的聲明。 (類和對象基礎)《第5章 - Scala 基礎面向對象編程》
def 開始一個方法聲明。 (方法聲明)《第2章 - 打更少的字,作更多的事》
do 開始一個do... while 循環。 (其它循環結構)《第3章 - Scala 本質》
else 對應一個if 語句塊開始一個else 語句塊。 (Scala  if 語句)《第3章 - Scala 本質》
extends 表示跟在後面的類或者特性這被聲明的類或者特性的父類型。 (父類)《第5章 - Scala 基礎面向對象編程》
FALSE 布爾值假。 (Scala 類型結構) 《第7章 - Scala 對象系統》
final 應用於類或者特性防止其它類型派生於它。應用於成員來防止在派生類或者特性中被重寫。 (嘗試重寫final 聲明)《第6章 - Scala 高級面向對象編程》
finally 在對應的try 語句塊後面開始一個不管異常是否被截取都會被執行的語句塊。 (使用try,catch,finally 語句塊)《第3章 - Scala 本質》
for 開始一個for 循環。 (理解Scala)《第3章 - Scala 本質》
forSome 用於約束容許使用的具體類的已存在類型聲明。 (已存在類型)《第12章 - Scala 類型系統》
if 開始一個if 語句塊。 (Scala 的if 語句)《第3章 - Scala 本質》
implicit 標識一個方法容許作爲類型隱式轉換器。標識一個方法的參數爲可選,只要在方法被調用的域裏有一個類型兼容的替代對象。 (隱式轉換)《第8章 - Scala 函數式編程》
import 導入一個或多個類型或者類型成員到當前域。 (導入類型和它的成員)《本章》
lazy 推遲對val 的估值。 (延遲計算值) 《第8章 - Scala 函數式編程》
match 開始一個模式匹配語句塊。 (模式匹配)《第3章 - Scala 本質》
new 建立一個類的實例。 (類和對象基礎)《第5章 - Scala 基礎面向對象編程》
null 一個沒有被賦值的引用類型變量的值。 (Scala 類型結構)《第7章 - Scala 對象系統》
object 開始一個單體模式的聲明。一個只有一個實例的類。 (類和對象:靜態成員呢?)《第5章 - Scala 基礎面向對象編程》
override 若是原來的成員沒有被標識爲final,重寫一個類或特性的具體成員。 (重寫類和特性的成員) 《第6章 - Scala 高級面向對象編程》
package 開始一個包的域聲明。 (用文件和名稱空間來組織代碼)《第2章 - 打更少的字,作更多的事》
private 限定一個聲明的可見域。 (可見性規則)《第5章 - Scala 基礎面向對象編程》
protected 限定一個聲明的可見域。 (可見性規則)《第5章 - Scala 基礎面向對象編程》
requires 不推薦使用. 曾用於自我類型(selftyping). (Scala 類型結構)《第7章 - Scala 對象系統》
return 從一個函數返回。 (初嘗Scala )《第1章 - 從0 分到60 分,Scala 介紹》
sealed 應用於父類,要求全部直接繼承的類都必須在同一個源文件中聲明。 (案例類)《第6章 - Scala 高級面向對象編程》
super 相似this,可是指向父類型。 (重寫抽象和具體方法)《第6章 - Scala 高級面向對象編程》
this 一個對象引用本身的方式。輔助構造函數的方法名。 (類和對象基礎)《第5章 - Scala 基礎面向對象編程》
throw 拋出一個異常。 (使用try,catch,finally 語句塊)《第3章 - Scala 本質》
trait 對一個類的實例加入額外狀態和行爲的混合模塊。 《第4章 - 特性》
try 開始一個語句塊包含對可能拋出異常的語句。 (使用try,catch,finally 語句塊)《第3章 - Scala 本質》
TRUE 布爾值真。 (Scala 類型結構)《第7章 - Scala 對象系統》
type 開始一個類型聲明。 (抽象類型和參數化類型) 《第2章 - 打更少的字,作更多的事》
val 開始一個只讀變量聲明。 (變量聲明)《第2章 - 打更少的字,作更多的事》
var 開始一個可讀寫變量聲明。 (變量聲明)《第2章 - 打更少的字,作更多的事》
while 開始一個while 循環。 (其它循環結構)《第3章 - Scala 本質》
with 對被聲明的類或者實例化的對象包含這個特性。 《第4章 - 特性》
yield 返回for 循環的結構以組成一個序列。 (Yield 語句)《第3章 - Scala 本質》
_ 一個佔位符,在imports,字面函數等地方使用。 許多地方都有涉及
: 標誌符和類型註解之間的分隔符。 (初嘗Scala)《第1章 - 從0 分到60 分,Scala 介紹》
= 賦值。 (初嘗Scala)《第1章 - 從0 分到60 分,Scala 介紹》
=> 在字面函數中使用,用於隔離參數列表和函數主體。 (字面函數和閉包)《第8章 - Scala 函數式編程》
<- 用於生成器表達式。 (理解Scala)《第3章 - Scala 本質》
<: 用於參數化類型和抽象類型聲明,用來約束容許的類型。 (類型邊界)《第12章 - Scala 類型系統》
<% 用於參數化類型和抽象類型的視覺邊界(view bounds) 聲明。 (類型邊界) 《第12章 - Scala 類型系統》
>: 用於參數化類型和抽象類型,用來約束容許的類型。 (類型邊界)《第12章 - Scala 類型系統》
# 用於類型投射(type projection)。 (類型投射)《第12章 - Scala 類型系統》
@ 標識一個註解。 (註解) 《第13章 - 應用程序設計》
(Unicode \u21D2) 等同 =>. (字面函數和閉包) 《第8章 - Scala 函數式編程》
(Unicode \u2190) 等同 <-. (理解Scala)《第3章 - Scala 本質》

注意break 和continue 沒有被列出。這些控制關鍵字在Scala 中並不存在。相反,Scala 鼓勵你使用一般狀況下足夠用,並且更少犯錯的函數式編程方法。咱們會在討論循環的時候討論一些其它的處理方法。(參見《第三章 - Scala 本質》的「生成器表達式」)。

有些Java 方法使用了Scala 的保留字,好比java.util.Scanner.match。爲了不編譯錯誤,用單反引號來括住它們,好比java.util.Scanner.‵match‵。

歸納

咱們從幾個方面見識了Scala 簡潔,可伸縮,高效的語法。咱們也描述了許多Scala 的特性。在下一章,咱們會在深刻Scala 對面向對象編程和函數式編程的支持前,完成對Scala 本質的講解。

相關文章
相關標籤/搜索