我們在這一節會先介紹一下Scala編譯器和交互環境(REPL)。然後討論類型推斷,接著是方法聲明(跟你所熟悉的Java方式不太一樣)。這兩個特性能幫你減少大量的套路化代碼,從而提高生產力。
我們會談到Scala的代碼封包方式和更強大的import
語句,然後詳細講解一下Scala中的循環和控制結構。這些特性植根於跟Java差異巨大的編程傳統,所以我們會借此機會討論一下Scala的函數式編程,包括函數式的循環結構、match
表達式和函數字面值。
看過這些之後,本章剩下的大部分內容對你來說都沒什麼問題了,你可以自信地說自己有能力成為一名Scala程序員了。來吧,現在我們就開始討論編譯器和內置的交互環境。
9.3.1 使用編譯器和REPL
Scala是編譯型語言,所以執行Scala程序通常要把它們先編譯成.class文件,然後在類路徑上有scala-library.jar(Scala運行時類庫)的JVM環境中執行。
如果你還沒裝Scala,請在繼續閱讀之前參見附錄C,瞭解如何安裝Scala。樣例程序(9.1.1中的HelloWorld)可以用scalac HelloWorld.scala
編譯(如果你正好在HelloWorld.scala文件所在的目錄中)。
一旦得到.class文件,就可以用命令scala HelloWorld
執行它了。這個命令會啟動帶著Scala運行時環境的JVM,然後進入類文件指定的main方法。
除了編譯和運行,Scala還有個內置的交互環境,有點像第8章講的Groovy控制台。但不像Groovy,Scala是在命令行環境裡實現的。這就是說在典型的Unix/Linux環境(Path
設置正確)中,你可以敲入scala
,它就會在終端窗口內打開,而不會再彈出一個新窗口。
注意 這類交互環境有時被稱為讀入—計算—輸出(Read-Eval-Print)循環,或簡稱為REPL。這在動態語言中很常見。在REPL環境中,前面輸入的那些行的計算結果還在,在後面的表達式和計算中還可以用。在本章的剩餘部分,我們偶爾會用REPL環境來演示Scala語法。
現在我們開始討論下一個大特性:Scala的高級類型推斷。
9.3.2 類型推斷
在讀前面的代碼時你可能已經注意到了,我們在聲明變量hello
為val
時,沒有指明它是什麼類型。因為它很「明顯」是個字符串。表面上來看這有點像Groovy,變量沒有類型(Groovy是動態類型語言),但其實Scala代碼中所發生的事情完全不同。
Scala是靜態類型語言(所以變量確實有明確的類型),但它的編譯器能分析源碼,並且一般都能根據上下文推斷出應該是什麼類型。如果Scala自己能確定是什麼類型,就不用你親自告訴它了。
這就是類型推斷,我們已經提過好幾次了。Scala在這方面的能力非常突出—以致於開發人員經常在行雲流水一樣的代碼中忘記靜態類型。這經常讓Scala更有動態語言的「感覺」。
Java中的類型推斷
Java也有類型推斷的能力,雖然有限,但確實有。最明顯的例子就是我們在第1章見到的泛型鑽石語法。Java的類型推斷通常是用在賦值語句等號右邊的值上。Scala通常是推斷變量而不是值的類型,但它的確也能推斷值的類型。
你已經見過其中最簡單的例子了:關鍵字var
和val
,Scala根據賦給變量的值來推斷它們的類型。Scala類型推斷的另一個重要應用是方法聲明。我們來看個例子(Scala的AnyRef
就是Java中的Object
):
def len(obj : AnyRef) = {
obj.toString.length
}
這是一個類型推斷的方法。通過檢查它返回代碼中的java.lang.String#length
的類型(int
),編譯器知道這個方法要返回Int
類型的值。注意,這個方法沒有顯式指定返回類型,我們也不需要用return
關鍵字。實際上,如果你放了一個顯式的return
在這裡,像這樣:
def len(obj : AnyRef) = {
return obj.toString.length
}
會得到一個編譯時錯誤:
error: method len has return statement; needs result type
return obj.toString.length
^
如果你連def
中的=也省略了,編譯器會假定這個方法會返回Unit
(就跟Java裡返回void
一樣)。
除了前面那些限制,還有兩個類型推斷受限的區域:
- 方法聲明中參數的類型——傳給方法的參數必須指定類型;
- 遞歸函數——Scala編譯器不能推斷遞歸函數的返回類型。
關於Scala的方法,我們討論的東西已經不少了,但還算不上系統化的討論,所以我們來鞏固一下你已經學過的東西。
9.3.3 方法
你已經見過怎麼用def
關鍵字定義方法了。隨著你對Scala越來越熟悉,關於Scala的方法,還有些你應該知道的重要事實。
Scala沒有
static
關鍵字。跟Java中的static
方法對應的方法必須放在Scala的object
(單例)結構中。稍後我們會向你介紹相關概念:伴生對象。跟Groovy(或Clojure)相比,Scala語言的運行時要重得多。Scala類中可能會有很多由平台自動生成的額外方法。
方法調用是Scala的核心概念。在Scala中沒有Java中那種意義的操作符。
對於哪些字符可以出現在方法的名稱中,Scala比Java更靈活。特別是那些在其他語言中作為操作符的字符,在Scala中可能是合法的方法名(比如加號
+
)。
間接方法調用(前面講過)中有Scala把方法調用和操作符合併到一起的線索。舉個例子,比如要把兩個整型相加。在Java中,應該是寫一個a+b
這樣的表達式。在Scala中你也可以這樣寫,但不止這樣,還可以寫成a.+(b)
。換句話說,你調用了a
上的+
方法,並把b
作為參數傳給它。這就是Scala不再把操作符當做一個獨立概念的秘密。
注意 你可能已經注意到了,
a.+(b)
是在a
上調用方法。但原始類型的變量a
怎麼會有方法呢?9.4節會給出完整的解釋。但現在,你只要知道Scala的類型系統認為所有東西都是對象,所以你可以在任何東西上調用方法,即便是Java裡的原始類型變量也行。
你已經見過一個用def
關鍵字聲明方法的例子了。我們再來看一個例子,一個實現階乘函數的簡單遞歸方法:
def fact(base : Int) : Int = {
if (base <= 0)
return 1
else
return base * fact(base - 1)
}
對於所有負數,這個函數都返回1,這算是作弊吧。實際上,負數的階乘是不存在的,但大家都是朋友嘛。它看起來有點像Java:有返回類型(Int
),並用return
關鍵字表明把哪個值交回給調用者。唯一需要注意的就是在函數體代碼塊定義之前額外符號=
。
Scala中還有另外一個Java中沒有概念:局部函數。它是在另外一個函數內部(並且僅在這一作用域內有效)定義的函數。如果開發人員想要一個輔助函數,又不想把實現細節暴露給外部,這是一個簡單的辦法。在Java中除了用private
方法之外別無選擇,但這個函數對於同一類的其他方法都是可見的。但在Scala中,你只要這樣寫就行了:
def fact2(base : Int) : Int = {
def factHelper(n : Int) : Int = {
return fact2(n-1)
}
if (base <= 0)
return 1
else
return base * factHelper(base)
}
factHelper
在fact2
的封閉作用域之外絕對是不可見的。
接下來,我們去看看Scala如何處理代碼的組織和導入。
9.3.4 導入
Scala對包的使用跟Java一樣,關鍵字也一樣,分別是package
和import
。Scala可以毫無障礙地導入和使用Java的包和類。Scala的var
或val
變量可以引用任何Java類的實例,不需要任何特殊的語法或處理:
import java.io.File
import java.net._
import scala.collection.{Map, Seq}
import java.util.{Date => UDate}
頭兩行代碼跟Java裡的標準導入和通配符導入一樣。第三行用一行導入一個包裡的多個類。最後一行在導入時指定了類的別名(避免縮寫衝突出現)。
跟Java不一樣,Scala中的import
可以出現在代碼中的任何位置(不僅限於文件頂部),這樣你就可以把import
當做文件的一部分分離出來。Scala也有默認導入,即所有.scala文件默認都會導入scala._
。這裡有很多有用的函數,包括我們已經討論過的一些,比如println
。對於所有默認導入的完整細節,請參見www.scala-lang.org/上的API文檔。
我們接下來討論怎麼控制Scala程序的執行流。這可能和你熟悉的Java跟Groovy有些差異。
9.3.5 循環和控制結構
Scala在控制和循環結構上引入了幾個有點繞的創新。在我們向你介紹這些不熟悉的形式之前,先來看幾個老朋友,比如標準的while
循環:
var counter = 1
while (counter <= 10) {
println(\".\" * counter)
counter = counter + 1
}
還有do-while
形式:
var counter = 1
do {
println(\".\" * counter)
counter = counter + 1
} while (counter <= 10)
另一個是基本的for
循環:
for (i <- 1 to 10) println(i)
看起來都很好。但Scala更靈活,比如條件for
循環:
for (i <- 1 to 10; if i %2 == 0) println(i)
還能在多個變量上循環,比如:
for (x <- 1 to 5; y <- 1 to x)
println(\" \" * (x - y) + x.toString * y)
這些多出來的形式源於Scala實現這些結構的根本性差異。Scala用函數式編程中的概念(列表推導式)來實現for
循環。
列表推導式的一般概念是對一個列表中的元素進行轉換(或過濾,比如在用條件for
循環時)。這會產生一個新列表,然後在其中的每個元素上逐次運行for
循環體中的代碼。
甚至把要過濾的列表和for
代碼塊分開都是有可能的,用yield
關鍵字。比如下面這段代碼:
val xs = for (x <- 2 to 11) yield fact(x)
for (factx <- xs) println(factx)
這段代碼先設置新集合xs
,然後用第二個for
循環逐一輸出其中的值。如果你需要一個創建一次、使用多次的集合,這個極其好用。
這一結構能成立是因為Scala支持函數式編程,我們接下來就去看看Scala如何實現函數式思想。
9.3.6 Scala的函數式編程
我們在7.5.2節提起過,Scala把函數當做內置的值。這就是說函數可以放進var
或val
中,並和其他任何值所受的對待毫無二致。這被稱為函數字面值(或匿名函數),它們是Scala世界觀的重要組成部分。
在Scala中寫函數字面值非常簡單。其中的關鍵是箭頭=>
,Scala用它來表示取得參數列表並傳遞到代碼塊中:
(<函數參數列表>) => { ... 作為代碼塊的函數體 ... }
我們用Scala的交互環境來演示一下。下面這個例子中定義的函數接受一個Int
參數,然後乘以2:
scala> val doubler = (x : Int) => { 2 * x }
doubler: (Int) => Int = <function1>
scala> doubler(3)
res4: Int = 6
scala> doubler(4)
res5: Int = 8
注意看Scala怎麼推斷doubler
的類型。它的類型是「接受一個Int
並返回Int
的函數」。這樣的類型用Java的類型系統還不能以令人完全滿意的方式表示。你看,調用doubler
就是用標準的調用語法。
我們把這個概念再向前推進一點。在Scala中,函數字面值只是值。並且是函數返回的值。這就是說你可以寫一個生產函數的函數——接受一個值並返回一個新的函數字面值。
比如說,可以定義一個命名為adder
的函數字面值。adder
能生產一個給它們的參數加上一個常量的函數:
scala> val adder = (n : Int) => { (x : Int) => x + n }
adder: (Int) => (Int) => Int = <function1>
scala> val plus2 = adder(2)
plus2: (Int) => Int = <function1>
scala> plus2(3)
res2: Int = 5
scala> plus2(4)
res3: Int = 6
看到了吧,Scala對函數字面值支持得很好。實際上,Scala代碼一般都能用非常函數的世界觀來編寫,同時也能用更加命令式的風格編寫。現在我們所做的不過是剛剛涉足Scala的函數式編程能力,但重要的是知道它們在那裡。
在下一節中,我們會討論Scala的對象模型和面向對像方式的細節。在一些重要方面,Scala的一些先進的特性使得它對面向對象的處理方式跟Java差異很大。