新聞中心
本文為《Programming Scala》的中文譯文《Scala 編程指南》的第二章,在《Scala語言編程入門指南》我們介紹了Scala語言編程的入門,在上一章中我們以幾個撩撥性質(zhì)的Scala 代碼范例作為章節(jié)結(jié)束,在本章中我們將詳細介紹如何使用Scala 來寫出精煉的,靈活的代碼。

10年積累的成都網(wǎng)站制作、成都網(wǎng)站建設(shè)經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認識你,你也不認識我。但先網(wǎng)站設(shè)計后付款的網(wǎng)站建設(shè)流程,更有鹽湖免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
推薦專題:Scala編程語言
章節(jié)概要
在這一章我們將討論如何使用Scala 來寫出精煉的,靈活的代碼。我們會討論文件和包的組織結(jié)構(gòu),導(dǎo)入其他的類型和變量聲明,一些語法習(xí)慣和概念。我們會著重討論Scala 簡明的語法如何幫助你更好更快地工作。
Scala 的語法對于書寫腳本特別有用。單獨的編譯和運行步驟對于簡單的,僅有少量獨立于Scala 提供的庫之外的程序不是必須的。你可以用scala 命令一次性編譯和運行這些程序。如果你已經(jīng)下載了本書的實例代碼,它們中的許多小程序可以用scala 命令來運行,比如scala filename.scala。參見每一章節(jié)代碼實例中的README.txt 可以獲取更多細節(jié)。也可以參見《第14章 - Scala 工具,庫和IDE 支持》中的“命令行工具”章節(jié)來獲取更多使用scala 命令的信息。
分號
你可能已經(jīng)注意到,在上一章的代碼示例中很少有分號出現(xiàn)。你可以使用分號來隔離各個聲明和表達式,就像Java,C,PHP 以及其他類似的語言一樣。然而在大多數(shù)情況下,Scala 的行為和許多腳本語言一樣把一行的結(jié)尾看作是聲明或者表達式的結(jié)尾。當一個聲明或者表達式太長,一行不夠的時候,Scala 通常可以推斷你在什么時候要在下一行繼續(xù),就像這個例子中一樣。
- // 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 介紹》的“初嘗并發(fā)”章節(jié)中的ShapeDrawingActor 示例里面使用了這樣的技巧。
- case "exit" => println("exiting..."); exit
這樣的代碼也可以寫成如下的樣子。
- ...
- case "exit" =>
- println("exiting...")
- exit
- ...
你可能會想為什么在case... => 這行的后面不需要用大括號{ } 把兩個語句括起來。。如果你想,你可以這么做,但是編譯器其實會知道你什么時候會到達語句塊的結(jié)尾,因為它會看到下一個case 塊或者終結(jié)所有case 塊的大括號。
省略可選的分號意味著更少的符號輸入和更少的符號混亂。把各個語句隔離到它們自己單獨的行上面可以提高你的代碼的可讀性。
變量聲明
當你聲明一個變量的時候,Scala 允許你來決定它是不變的(只讀的)還是可變的(可讀寫的)。一個不變的“變量”可以用val 關(guān)鍵字來聲明(想象一個值對象)。
- val array: Array[String] = new Array(5)
更準確的說,這個引用array 不能被修改指向另外一個Array (數(shù)組),但是這個數(shù)組本身可以被修改,正如下面的scala 會話中演示的。
- 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 必須被初始化,也就是說在聲明的時候就必須定義。
一個可變的變量用關(guān)鍵字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 聲明時即定義的規(guī)則有一些例外。這兩個關(guān)鍵字都可以被用作構(gòu)造函數(shù)參數(shù)。當作為構(gòu)造函數(shù)參數(shù)時,這些可變或者不可變的變量會在一個對象被實例化的時候被初始化。兩個關(guān)鍵字可以在抽象類型中被用來聲明“抽象”(沒有初始化的)的變量。同時,繼承類型可以重寫在父類型中聲明的值。我們會在《第5章 - Scala 基礎(chǔ)面向?qū)ο缶幊獭分杏懻撨@些例外。
Scala 鼓勵你在任何可能的時候使用不可變的值。正如我們即將看到的,這會促進更佳的面向?qū)ο笤O(shè)計,而且這和“純”函數(shù)式編程的原則相一致。
注意
var 和val 關(guān)鍵字指定了該引用能否被修改指向另一個對象。它們并不指定它們引用的對象是否可變。
方法聲明
我們在《第1章 - 從0分到60分:Scala 介紹》中見到了幾個如何定義方法的例子,它們都是類的成員函數(shù)。方法定義由一個def 關(guān)鍵字開始,緊接著是可選的參數(shù)列表,一個冒號“:” 和方法的返回類型,一個等于號“=”,最后是方法的主體。如果你不寫等于號和方法主體,那么方法會被隱式聲明為“抽象”。包含它的類型于是也是一個抽象類型。我們會在《第5章,Scala 基礎(chǔ)面向?qū)ο缶幊獭分性敿氂懻摮橄箢愋汀?/p>
我們剛才說到“可選的參數(shù)列表”,這意味著一個或更多。Scala 可以讓你為方法定義一個以上的參數(shù)列表。這是級聯(lián)方法(currying methods)所需要的。我們會在《第8章 - Scala 函數(shù)式編程》中的“級聯(lián)(Currying)章節(jié)討論它。這個功能對于定義你自己的域特定語言(DSLs)也很有幫助。我們會在《第11章 - Scala 中的域特定語言》 中看到它。注意,每一個參數(shù)列表會被括號所包圍,并且所有的參數(shù)由逗號隔開。
如果一個方法的主體包含多于一個的表達式,你必須用大括號{ } 來把它們包起來。你可以在方法主體只有一個表達式的時候省略大括號。
方法的默認參數(shù)和命名參數(shù)(Scala 版本2.8)
許多語言都允許你為一個方法的一個或多個參數(shù)定義默認值。考慮下面的腳本,一個StringUtil 對象允許你用一個用戶定義的分隔符來連接字符串。
- // code-examples/TypeLessDoMore/string-util-v1-script.scala
- // Version 1 of "StringUtil".
- object StringUtil {
- def joiner(strings: List[String], separator: String): String =
- strings.mkString(separator)
- def joiner(strings: List[String]): String = joiner(strings, " ")
- }
- import StringUtil._ // Import the joiner methods.
- println( joiner(List("Programming", "Scala")) )
實際上,有兩個“重載”的jioner 方法。第二個方法使用了一個空格作為“默認”分隔符。寫兩個函數(shù)似乎有點浪費,如果我們能消除第二個joiner 方法,在第一個jioner 方法里為separator 參數(shù)聲明一個默認值,那就太好了。事實上,在Scala 2.8 版本里,你可以這么做。
- // code-examples/TypeLessDoMore/string-util-v2-v28-script.scala
- // Version 2 of "StringUtil" for Scala v2.8 only.
- object StringUtil {
- def joiner(strings: List[String], separator: String = " "): String =
- strings.mkString(separator)
- }
- import StringUtil._ // Import the joiner methods.println(joiner(List("Programming", "Scala")))
對于早些版本的Scala 還有另外一種選擇。你可以使用隱式參數(shù),我們會在《第8章 - Scala 函數(shù)式編程》的“隱式函數(shù)參數(shù)”章節(jié)討論。
2.8 版本的Scala 提供了另外一種對方法參數(shù)列表進行增強,就是命名參數(shù)。我們實際上可以用多種方法重寫上一個例子的最后一行。下面所有的println 語句在功能上都是一致的。
- println(joiner(List("Programming", "Scala")))
- println(joiner(strings = List("Programming", "Scala")))
- println(joiner(List("Programming", "Scala"), " ")) // #1
- println(joiner(List("Programming", "Scala"), separator = " ")) // #2
- println(joiner(strings = List("Programming", "Scala"), separator = " "))
為什么這樣有用呢?第一,如果你為方法參數(shù)選擇了好的名字,那么你對那些函數(shù)的調(diào)用事實上為每一個參數(shù)記載了一個名字。舉例來說,比較注釋#1 和#2 的兩行。在第一行,第二個參數(shù)“ ”的用處可能不是很明顯。在第二行中,我們提供了參數(shù)名separator,同時也暗示了參數(shù)的用處。
第二個好處則是你可以以任何順序指定參數(shù)的順序。結(jié)合默認值,你可以像下面這樣寫代碼
- // code-examples/TypeLessDoMore/user-profile-v28-script.scala
- // Scala v2.8 only.
- object OptionalUserProfileInfo {
- val UnknownLocation = ""
- val UnknownAge = -1
- val UnknownWebSite = ""
- }
- class OptionalUserProfileInfo(
- location: String = OptionalUserProfileInfo.UnknownLocation,
- age: Int = OptionalUserProfileInfo.UnknownAge,
- webSite: String = OptionalUserProfileInfo.UnknownWebSite)
- println( new OptionalUserProfileInfo )
- println( new OptionalUserProfileInfo(age = 29) )
- println( new OptionalUserProfileInfo(age = 29, location="Earth") )
OptionalUserProfileInfo 為你的下一個Web 2.0 社交網(wǎng)站提供了“可選的”用戶概要信息。它定義了所有字段的默認值。這段腳本在創(chuàng)建實例的時候提供了0個或者更多的命名參數(shù)。而參數(shù)的順序卻是任意的。
在這個我們展示的例子里,常量值被用來作為默認值。大多數(shù)支持默認參數(shù)的語言只允許編譯時能決定的常量或者值作為默認值。然而,在Scala 里,任何表達式都可以被作為默認值,只要它可以在被使用的時候正確編譯。比如說,一個表達式不能引用類或者對象主體內(nèi)才被計算的實例字段,但是它可以引用一個方法或者一個單例對象。
一個類似的限制是一個參數(shù)的默認表達式不能引用列表中的另外一個參數(shù),除非被引用的參數(shù)出現(xiàn)在列表的更前面,或者參數(shù)已經(jīng)被級聯(lián)(我們會在《第8章 - Scala 函數(shù)式編程》的“級聯(lián)”這一章節(jié)詳細討論)。
最后,還有一個對命名參數(shù)的約束就是一旦你為一個方法掉哦那個指定了參數(shù)名稱,那么剩下的在這個參數(shù)之后的所有參數(shù)都必須是命名參數(shù)。比如,new OptionalUserProfileInfo(age =29, "Earch") 就不能被編譯,因為第二個參數(shù)不是通過命名方式調(diào)用的。
我們會在《第6章 - Scala 高級面向?qū)ο缶幊獭分械摹癈ase Class(案例類)”中看到另外一個使用命名參數(shù)和默認參數(shù)的例子。
嵌套方法定義
方法定義也可以被嵌套。這里是一個階乘計算器的實現(xiàn),我們會使用一種常規(guī)的方法,通過調(diào)用第二個,嵌套的方法來完成計算。
- // code-examples/TypeLessDoMore/factorial-script.scala
- def factorial(i: Int): Int = {
- def fact(i: Int, accumulator: Int): Int = {
- if (i <= 1)
- accumulator
- else
- fact(i - 1, i * accumulator)
- }
- fact(i, 1)
- }
- println( factorial(0) )
- println( factorial(1) )
- println( factorial(2) )
- println( factorial(3) )
- println( factorial(4) )
- println( factorial(5) )
第二個方法遞歸地調(diào)用了自己,傳遞一個accumulator 參數(shù),這個參數(shù)是計算結(jié)果累積的地方。注意,我們當計數(shù)器i 達到1 的時候返回了累積的值。(我們會忽略負整數(shù)。實際上這個函數(shù)在i<0 的時候會返回1 。)在嵌套方法的定義后面,factorial 以傳入值i 和初始accumulator 值1 來調(diào)用它。
就像很多語言中聲明局部變量一樣,一個嵌套方法盡在方法內(nèi)部可見。如果你嘗試在factorial 之外去調(diào)用fact,你會得到一個編譯錯誤。
你注意到了嗎,我們兩次把i 作為一個參數(shù)名字,第一次是在factorial 方法里,然后是在嵌套的fact 方法里。就像在其它許多語言中一樣,在fact 中的i 參數(shù)會屏蔽掉外面factorial 的i 參數(shù)。這樣很好,因為我們在fact 中不需要在外面的i 的值。我們只在第一次調(diào)用fact 的時候需要它,也就是在factorial 的最后。
那如果我們需要使用定義在嵌套函數(shù)外面的變量呢?考慮下面的例子。
- // code-examples/TypeLessDoMore/count-to-script.scala
- def countTo(n: Int):Unit = {
- def count(i: Int): Unit = {
- if (i <= n) {
- println(i)
- count(i + 1)
- }
- }
- count(1)
- }
- countTo(5)
注意嵌套方法count 使用了作為參數(shù)傳入countTo 的n 的值。這里沒有必要把n 作為參數(shù)傳給count。因為count 嵌套在countTo 里面,所以n對于count 來說是可見的。
字段(成員變量)的聲明可以用可見程度關(guān)鍵字來做前綴,就像Java 和C# 這樣的語言一樣。和非嵌套方法的生命類似,這些嵌套方法也可以用這些關(guān)鍵字來修飾。我們會在《第5章 - Scala 面向?qū)ο缶幊獭分械摹翱梢姸纫?guī)則”章節(jié)來討論可見度的規(guī)則和對應(yīng)的關(guān)鍵字。
#p#
類型推斷
靜態(tài)類型書寫的代碼可能會非常冗長,考慮下面典型的Java 聲明。
- import java.util.Map;
- import java.util.HashMap;
- ...
- Map intToStringMap = new HashMap();
我們不得不兩次指明參數(shù)類型。(Scala 使用類型注解作為顯式類型聲明的方式,比如HashMap。)
Scala 支持類型推斷(參考,例如[ 類型推斷] 和[Pierce2002,Benjamin C. Pierce, 類型與編程語言, 麻省理工出版社, 2002])。即使沒有顯示的類型注解,語言的編譯器仍可以從上下文中分辨出相當多的類型信息。這里是Scala 的聲明,使用了類型信息的推斷。
- import java.util.Map
- import java.util.HashMap
- ...
- val intToStringMap: Map[Integer, String] = new HashMap
回憶在第1章中Scala 使用方括號來指明范型類型參數(shù)。我們在等號左邊指定了Map[Integer, String]。(我們在例子中還是繼續(xù)使用Java 的類型。)在右邊,我們實例化了一個我們實際需要的對象,一個HashMap,但是我們不用重復(fù)地聲明類型參數(shù)。
再補充一點,假設(shè)我們實際上并不關(guān)心實例的類型是不是Map (Java 的接口類型)。我們只需要知道它是HashMap 類型。
- import java.util.Map
- import java.util.HashMap
- ...
- val intToStringMap2 = new HashMap[Integer, String]
這樣的聲明不需要在左邊指定類型注解,因為所有需要的類型信息都已經(jīng)在右邊有了。編譯器自動給intToStringMap2 賦予HashMap[Integer, String] 類型。
類型推斷對方法也有用。在大多數(shù)情況下,方法的返回類型可以被推斷,所以“:”和返回類型可以被省略。然而,對于所有的方法參數(shù)來說,類型注解是必須的。
像Haskell(參見,例如[O'Sullivan2009, Bryan O’Sullivan, John Goerzen, and Don Steward, Real World Haskell, O’Reilly Media, 2009] 這樣的純函數(shù)式語言使用類似于Hindley-Milner(參見[Spiewak2008] 獲取簡單摘要的解釋)的算法來做類型推斷。用這些語言寫出的代碼需要比Scala 更少的類型注解,因為Scala 的類型推斷算法得同時支持面向?qū)ο箢愋秃秃瘮?shù)式類型。所以,Scala 比Haskell 這樣的語言需要更多的類型注解。這里有一份關(guān)于Scala 何時需要顯式類型注解規(guī)則的總結(jié)。
顯式類型注解在何時是必要的。
從實用性來講,你必須為下列情況提供顯式的類型注解:
1。變量聲明,除非你給變量賦予了一個值。(比如,val name = "Programming Scala")
2。所有的方法參數(shù)。(比如,def deposit(amount: Money)
3。下列情況中的方法返回值:
a 當你在方法里顯式調(diào)用return 的時候 (即使是在最后)。
b 當一個方法是遞歸的時候。
c 當方法是重載的,并且其中一個調(diào)用了另外一個的時候。主調(diào)用的方法必須有一個返回類型的注解。
d 當推斷的返回類型比你所想要的更普通時,比如Any。
注意
Any 類型是Scala 類型結(jié)構(gòu)的根類型(參見《第7章 - Scala 對象系統(tǒng)的更多細節(jié)》中的“Scala 類型結(jié)構(gòu)”章節(jié))。如果一段代碼意外地返回類一個Any 類型的值,那么很可能類型推斷器不能算出需要返回的類型,所以選擇了最有可能的最通常的類型。
讓我們來看一些需要顯式聲明方法返回類型的例子。在下面的腳本中,upCase 方法有一個有條件的返回語句,返回非0長度的字符串。
- // code-examples/TypeLessDoMore/method-nested-return-script.scala
- // ERROR: Won't compile until you put a String return type on upCase.
- def upCase(s: String) = {
- if (s.length == 0)
- return s // ERROR - forces return type of upCase to be declared.
- else
- s.toUpperCase()
- }
- println( upCase("") )
- println( upCase("Hello") )
運行這段腳本你會獲得如下錯誤。
- ... 6: error: method upCase has return statement; needs result type
- return s
- ^
你可以通過把方法第一行改成如下樣子來修正這個錯誤。
- def upCase(s: String): String = {
實際上,對于這段腳本,另外一種解決辦法是刪除return 關(guān)鍵字。沒有它代碼也可以很好的工作,但是它闡明了我們的目的。
遞歸方法也需要顯式的返回類型?;貞浳覀冊谏弦徽轮小扒短追椒ǖ亩x”章節(jié)看到的factorial 方法。讓我們來刪除嵌套的fact 方法的:Int 返回類型。
- // code-examples/TypeLessDoMore/method-recursive-return-script.scala
- // ERROR: Won't compile until you put an Int return type on "fact".
- def factorial(i: Int) = {
- def fact(i: Int, accumulator: Int) = {
- if (i <= 1)
- accumulator
- else
- fact(i - 1, i * accumulator) // ERROR
- }
- fact(i, 1)
- }
現(xiàn)在不能編譯了。
- ... 9: error: recursive method fact needs result type
- fact(i - 1, i * accumulator)
- ^
重載的方法有時候也需要顯式返回類型。當一個這樣的方法調(diào)用另外一個時,我們必須給調(diào)用者加上返回類型,如下面的例子。
- // code-examples/TypeLessDoMore/method-overloaded-return-script.scala
- // Version 1 of "StringUtil" (with a compilation error).
- // ERROR: Won't compile: needs a String return type on the second "joiner".
- object StringUtil {
- def joiner(strings: List[String], separator: String): String =
- strings.mkString(separator)
- def joiner(strings: List[String]) = joiner(strings, " ") // ERROR
- }
- import StringUtil._ // Import the joiner methods.
- println( joiner(List("Programming", "Scala")) )
兩個joiner 方法把一系列字符串串在一起。第一個方法還接受一個參數(shù)來作為分隔符。第二個方法調(diào)用第一個方法,并且傳入一個空格作為“默認”分隔符。
如果你運行這段腳本,你會獲得如下錯誤。
- ... 9: error: overloaded method joiner needs result type
- def joiner(strings: List[String]) = joiner(strings, "")
因為第二個jioner 方法調(diào)用了第一個,它需要一個顯示的String 返回類型。它必須看起來像這樣。
- def joiner(strings: List[String]): String = joiner(strings, " ")
最后的一種場景的關(guān)系可能比較微妙,比你期望的類型更通用的類型可能會被推斷返回。你通常會把函數(shù)返回值賦給擁有更特定類型變量的時候遇到這樣的錯誤。比如,你希望獲得一個String,但是函數(shù)推斷返回類型為Any。讓我們來看一個設(shè)計好的例子來反映會發(fā)生這種bug 的場景。
- // code-examples/TypeLessDoMore/method-broad-inference-return-script.scala
- // ERROR: Won't compile. Method actually returns List[Any], which is too "broad".
- def makeList(strings: String*) = {
- if (strings.length == 0)
- List(0) // #1
- else
- strings.toList
- }
- val list: List[String] = makeList() // ERROR
運行這段腳本會獲得如下錯誤。
- ...11: error: type mismatch;
- found : List[Any]
- required: List[String]
- val list: List[String] = makeList()
- ^
我們希望makeList 能返回一個List[String],但是當strings.length 等于0 時,我們錯誤地假設(shè)List(0) 是一個空的列表并且將其返回。實際上,我們返回了一個有一個元素0 的List[Int] 對象。我們應(yīng)該返回List()。因為else 表達式后返回了strings.toList 的返回值List[String],方法的推斷返回類型就是離List[Int] 和List[String] 最近的公共父類型List[Any]。主意,編譯錯誤并不是在函數(shù)定義的時候出現(xiàn)。我們只有當把makeList 返回值賦給一個List[String] 類型得變量的時候才看到這個錯誤。
在這種情況下,修正bug 才是正道。另外,有時候并沒有bug,只是編譯器需要一些顯式聲明的“幫助”來返回正確的類型。研究一下那些似乎返回了非期望類型的方法。以我們的經(jīng)驗,如果你修改了方法后發(fā)現(xiàn)它返回了比期望的更一般的類型,那么在這種情況下加上顯式返回類型聲明。
另一種避免這樣的麻煩的方式是永遠為方法返回值聲明類型,特別是為公用API 定義方法的時候。讓我們重新來看我們的StringUtil 例子來理解為什么顯式聲明是一個好主意(從[Smith2009a] 改寫)。
這里是我們的StringUtil “API",和一個新的方法,toCollection。
- // code-examples/TypeLessDoMore/string-util-v3.scala
- // Version 3 of "StringUtil" (for all versions of Scala).
- object StringUtil {
- def joiner(strings: List[String], separator: String): String =
- strings.mkString(separator)
- def joiner(strings: List[String]): String = strings.mkString(" ")
- def toCollection(string: String) = string.split(' ')
- }
toCollection 方法以空格來分割字符串,然后返回一個包含這些子字符串的Array(數(shù)組)。返回類型是推斷出的,我們會看到,這會是一個潛在的問題所在。這個方法是計劃中的,但是會展示我們的重點。下面是一個使用StringUtil 的這個方法的客戶端。
- // code-examples/TypeLessDoMore/string-util-client.scala
- import StringUtil._
- object StringUtilClient {
- def main(args: Array[String]) = {
- args foreach { s => toCollection(s).foreach { x => println(x) } }
- }
- }
如果你用scala 編譯這些文件,你就能像這樣運行客戶端。
- $ scala -cp ... StringUtilClient "Programming Scala"
- Programming
- Scala
注意
類路徑參數(shù) -cp,使用了scalac 寫出class 文件的目錄,默認是當前目錄(比如,使用-cp.)。如果你使用了下載的代碼示例中的編譯過程,那些class 文件會被寫到build 目錄中區(qū)(使用scalac -d build ...)。在這個例子里,使用 -cp build.
這個時候,一切都工作正常。但是現(xiàn)在想象一下代碼庫擴大以后,StringUtil 和它的客戶端被分別編譯然后捆綁到不同的jar 文件中去。再想象一下StringUtil 的維護者決定返回一個List 來替代原來的默認值。
- object StringUtil {
- ...
- def toCollection(string: String) = string.split(' ').toList // changed!
- }
唯一的區(qū)別是最后的對toList 的調(diào)用,把一個Array 轉(zhuǎn)換成了List。重新編譯StringUtil 并且部署為jar 文件。然后運行相同的客戶端,先不要重新編譯。
- $ scala -cp ... StringUtilClient "Programming Scala"
- java.lang.NoSuchMethodError: StringUtil$.toCollection(...
- at StringUtilClient$$anonfun$main$1.apply(string-util-client.scala:6)
- at StringUtilClient$$anonfun$main$1.apply(string-util-client.scala:6)
- ...
發(fā)生了什么?當客戶端被編譯的時候,StringUtil.toCollection 返回了一個Array。然后toCollection 被修改為返回一個List。在兩個版本里,方法返回值都是被推斷出來的。因此,客戶端也必須被重新編譯。
然而,如果顯式地聲明返回類型是Seq,作為Array 和List 的共同父類型,這樣的實現(xiàn)就不會對客戶端要求重新編譯。
注意
當開發(fā)獨立于客戶端的API 的時候,顯式地聲明方法返回類型,并且盡可能使用更一般的返回類型。這在API 被聲明為抽象方法時尤其重要。(參見,比如《第4章 - 特性》。)
還有另外一種場景需要考慮集合聲明的使用,比如val map = Map(),就像下面這個例子。
- val map = Map()
- map.update("book", "Programming Scala")
- ... 3: error: type mismatch;
- found : java.lang.String("book")
- required: Nothing
- map.update("book", "Programming Scala")
- ^
發(fā)生了什么?范型類型Map 的類型參數(shù)在map 被創(chuàng)建時被推斷為[Nothing, Nothing]。(我們會在《第7章 - Scala 對象系統(tǒng)》的“Scala 類型結(jié)構(gòu)”章節(jié)討論Nothing。但是它的名字本身就解釋了自己?。┪覀儑L試插入一對不匹配的String,String 鍵值對。叫它拿都去不了的地圖吧!解決方案是,在初始化map 聲明的時候指出參數(shù)類型,例如val map = Map[String, String]() 或者指定初始值以便于map 參數(shù)被推斷,例如val map = Map("Programming"->"Scala")。
最后,還有一個推斷返回類型可能導(dǎo)致不可預(yù)知的令人困擾的結(jié)果[Scala 提示]的詭異行為。考慮下面的scala 對話例子。
- scala> def double(i: Int) { 2 * i }
- double: (Int)Unit
- scala> println(double(2))
- ()
為什么第二個命令打印出() 而不是4?仔細看scala 解釋器給出的第一個命令的返回值,double: (Int)Unit。我們定義了一個方法叫double,接受一個Int 參數(shù),返回Unit。方法并不像我們期望的那樣返回Int。
造成這樣意外結(jié)果的原因是在方法定義中缺少的等于號。下面是我們實際上需要的定義。
- scala> def double(i: Int) = { 2 * i }
- double: (Int)Int
- scala> println(double(2))
注意double 主體前的等于號?,F(xiàn)在,輸出說明我們定義了一個返回Int 的double,第二個命令完成了我們期望的工作。
這樣的行為是有原因的。Scala 把主體之前的部分包含等于號作為函數(shù)定義,而一個函數(shù)在函數(shù)式編程中永遠都有返回值。另一方面來說,當Scala 看到一個函數(shù)主體而沒有等于號前綴時,它就假設(shè)程序員希望這個方法變成一個“過程”定義,希望獲得由返回值Unit 帶來的副作用。而在實際中,結(jié)果往往是程序員簡單地忘記了插入等于號!
警告
當方法的放回類型被推斷而你又沒有在方法主體的大括號前使用等于號的時候,即使方法的最后一個表達式是另外一個類型的值,Scala 也會推斷出一個Unit 返回類型。
順便說一句,之前我們修正bug 前打印出來的() 是哪里來的?事實上這是Unit 類型單體實例的真正名字?。ㄟ@個名字是函數(shù)式編程的協(xié)定。)
#p#
常值
一個對象經(jīng)常會用一個常值來初始化,比如val book = "Programming Scala"。下面我們來討論一下Scala 支持的常值種類。這里我們只討論字符常值。我們會在后面遇到函數(shù)(被用作值,而不是成員方法),tuples,Lists,Maps 等的常值語法的時候再繼續(xù)討論。
整數(shù)(Integer)
整數(shù)常值可以表達為:十進制,十六進制,或者八進制。細節(jié)總結(jié)參見“表2.1, 整數(shù)常值”
| 種類 | 格式 | 例子 |
| 十進制 | 0 ,或者非零數(shù)字后面跟隨0 個或者多個十進制字符 (0 - 9) | 0, 1, 321 |
| 十六進制 | 0x 后面跟隨一個或多個十六進制字符 (0-9, A-F, a-f) | 0xFF, 0x1a3b |
| 八進制 | 0 后面跟隨一個或多個八進制字符 (0-7) | 013, 077 |
對于長整型值,必須在常值的后面加上L 或者l 字符。否則會被判定為普通整型。整數(shù)的有效值由被賦值的變量類型來決定。表2.2,“整型數(shù)的允許范圍(包括邊界)” 定義了整數(shù)的極限,包括邊界值。
| 目標類型 | 最小值 (包括) | 最大值 (包括) |
| Long(長整型) | ?263 | 263 - 1 |
| Int (整型) | ?231 | 231 - 1 |
| Short (短整型) | ?215 | 215 - 1 |
| Char (字符) | 0 | 216 - 1 |
| Byte (字節(jié)) | ?27 | 27 - 1 |
如果一個整數(shù)的值超出了允許范圍,就會發(fā)生編譯錯誤,比如下面這個例子。
- scala > val i = 12345678901234567890
- :1: error: integer number too large
- val i = 12345678901234567890
- scala> val b: Byte = 128
- :4: error: type mismatch;
- found : Int(128)
- required: Byte
- val b: Byte = 128
- ^
- scala> val b: Byte = 127
- b: Byte = 127
浮點數(shù)(Float)
Float 由0 個或多個數(shù)字,加上一個點號,再加上0 個或多個數(shù)字組成。如果在點號前面沒有數(shù)字,比如數(shù)字比1.0 要小,那么在點號后面必須有一個或者多個數(shù)字。對于浮點數(shù),需要在常值的最后加上F 或者f 。否則默認判定為雙精度浮點數(shù)(Double)。你可以選擇給一個雙精度浮點數(shù)加上D 或者d。
浮點數(shù)可以用指數(shù)方法表達。指數(shù)部分的格式是e 或者E,加上一個可選的+或者-,再加上一個或多個數(shù)字。
這里有一些浮點數(shù)的例子。
- 0.
- .0
- 0.0
- 3.
- 3.14
- .14
- 0.14
- 3e5
- 3E5
- 3.E5
- 3.e5
- 3.e+5
- 3.e-5
- 3.14e-5
- 3.14e-5f
- 3.14e-5F
- 3.14e-5d
- 3.14e-5D
Float 遵循了IEEE 754 32位單精度二進制浮點數(shù)值的規(guī)范。Double 遵循了IEEE 754 64位雙精度二進制浮點數(shù)值的規(guī)范。
警告
為了防止解析時的二義性,如果一個浮點數(shù)后面跟隨著一個字母開頭的符號,你必須在浮點數(shù)后面跟隨至少一個空格。比如,表達式1.toString 返回整數(shù)1 的字符串形式,而1. toString 則返回浮點數(shù)1.(0) 的字符串形式。
布爾值
布爾值可以是true (真) 或者false (假)。被賦值的變量的類型會被推斷為Boolean。
- scala> val b1 = true
- b1: Boolean = true
- scala> val b2 = false
- b2: Boolean = false
字符常值
一個字符常值是一個單引號內(nèi)的可打印的Unicode 字符或者一個轉(zhuǎn)義序列。一個可以用Unicode 值0 到255 表示的字符也可以用一個八進制轉(zhuǎn)義來表示:一把反斜杠加上最多3個八進制字符序列。如果在字符或者字符串中反斜杠后面不是一個有效的轉(zhuǎn)義序列則會出現(xiàn)編譯錯誤。
這里有一些例子.
- ’A’
- ’\u0041’ // 'A' in Unicode
- ’\n’
- '\012' // '\n' in octal
- ’\t’
有效的轉(zhuǎn)義序列參見:表格2.3 “字符轉(zhuǎn)義序列”
| 序列 | Unicode | 含義 |
| \b | \u0008 | backspace BS (退格) |
| \t | \u0009 | horizontal tab HT (水平Tab) |
| \n | \u000a | lin 本文名稱:Scala編程指南更少的字更多的事 文章地址:http://www.5511xx.com/article/ccecjcp.html |


咨詢
建站咨詢
