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

10年積累的成都網(wǎng)站制作、成都網(wǎng)站建設(shè)經(jīng)驗(yàn),可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識你,你也不認(rèn)識我。但先網(wǎng)站設(shè)計(jì)后付款的網(wǎng)站建設(shè)流程,更有鹽湖免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
推薦專題:Scala編程語言
章節(jié)概要
在這一章我們將討論如何使用Scala 來寫出精煉的,靈活的代碼。我們會討論文件和包的組織結(jié)構(gòu),導(dǎo)入其他的類型和變量聲明,一些語法習(xí)慣和概念。我們會著重討論Scala 簡明的語法如何幫助你更好更快地工作。
Scala 的語法對于書寫腳本特別有用。單獨(dú)的編譯和運(yùn)行步驟對于簡單的,僅有少量獨(dú)立于Scala 提供的庫之外的程序不是必須的。你可以用scala 命令一次性編譯和運(yùn)行這些程序。如果你已經(jīng)下載了本書的實(shí)例代碼,它們中的許多小程序可以用scala 命令來運(yùn)行,比如scala filename.scala。參見每一章節(jié)代碼實(shí)例中的README.txt 可以獲取更多細(xì)節(jié)。也可以參見《第14章 - Scala 工具,庫和IDE 支持》中的“命令行工具”章節(jié)來獲取更多使用scala 命令的信息。
分號
你可能已經(jīng)注意到,在上一章的代碼示例中很少有分號出現(xiàn)。你可以使用分號來隔離各個(gè)聲明和表達(dá)式,就像Java,C,PHP 以及其他類似的語言一樣。然而在大多數(shù)情況下,Scala 的行為和許多腳本語言一樣把一行的結(jié)尾看作是聲明或者表達(dá)式的結(jié)尾。當(dāng)一個(gè)聲明或者表達(dá)式太長,一行不夠的時(shí)候,Scala 通??梢酝茢嗄阍谑裁磿r(shí)候要在下一行繼續(xù),就像這個(gè)例子中一樣。
- // 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)
- }
當(dāng)你需要在同一行中放置多個(gè)聲明或者表達(dá)式的時(shí)候,你可以使用分號來隔開它們。我們在《第1章 - 從0分到60分:Scala 介紹》的“初嘗并發(fā)”章節(jié)中的ShapeDrawingActor 示例里面使用了這樣的技巧。
- case "exit" => println("exiting..."); exit
這樣的代碼也可以寫成如下的樣子。
- ...
- case "exit" =>
- println("exiting...")
- exit
- ...
你可能會想為什么在case... => 這行的后面不需要用大括號{ } 把兩個(gè)語句括起來。。如果你想,你可以這么做,但是編譯器其實(shí)會知道你什么時(shí)候會到達(dá)語句塊的結(jié)尾,因?yàn)樗鼤吹较乱粋€(gè)case 塊或者終結(jié)所有case 塊的大括號。
省略可選的分號意味著更少的符號輸入和更少的符號混亂。把各個(gè)語句隔離到它們自己單獨(dú)的行上面可以提高你的代碼的可讀性。
變量聲明
當(dāng)你聲明一個(gè)變量的時(shí)候,Scala 允許你來決定它是不變的(只讀的)還是可變的(可讀寫的)。一個(gè)不變的“變量”可以用val 關(guān)鍵字來聲明(想象一個(gè)值對象)。
- val array: Array[String] = new Array(5)
更準(zhǔn)確的說,這個(gè)引用array 不能被修改指向另外一個(gè)Array (數(shù)組),但是這個(gè)數(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>
一個(gè)不變的val 必須被初始化,也就是說在聲明的時(shí)候就必須定義。
一個(gè)可變的變量用關(guān)鍵字var 來聲明。
- scala> var stockPrice: Double = 100.
- stockPrice: Double = 100.0
- scala> stockPrice = 10.
- stockPrice: Double = 10.0
- scala>
Scala 同時(shí)也要求你在聲明一個(gè)var 時(shí)將其初始化。你可以在需要的時(shí)候給一個(gè)var 賦予新的值。這里再次嚴(yán)謹(jǐn)說明一下:引用stockPrice 可以被修改指向一個(gè)不一樣的Double 對象(比如10)。在這個(gè)例子里,stockPrice 引用的對象不能被修改,因?yàn)镈ouble 在Scala 里是不可變的。
在這里,對于val 和var 聲明時(shí)即定義的規(guī)則有一些例外。這兩個(gè)關(guān)鍵字都可以被用作構(gòu)造函數(shù)參數(shù)。當(dāng)作為構(gòu)造函數(shù)參數(shù)時(shí),這些可變或者不可變的變量會在一個(gè)對象被實(shí)例化的時(shí)候被初始化。兩個(gè)關(guān)鍵字可以在抽象類型中被用來聲明“抽象”(沒有初始化的)的變量。同時(shí),繼承類型可以重寫在父類型中聲明的值。我們會在《第5章 - Scala 基礎(chǔ)面向?qū)ο缶幊獭分杏懻撨@些例外。
Scala 鼓勵你在任何可能的時(shí)候使用不可變的值。正如我們即將看到的,這會促進(jìn)更佳的面向?qū)ο笤O(shè)計(jì),而且這和“純”函數(shù)式編程的原則相一致。
注意
var 和val 關(guān)鍵字指定了該引用能否被修改指向另一個(gè)對象。它們并不指定它們引用的對象是否可變。
方法聲明
我們在《第1章 - 從0分到60分:Scala 介紹》中見到了幾個(gè)如何定義方法的例子,它們都是類的成員函數(shù)。方法定義由一個(gè)def 關(guān)鍵字開始,緊接著是可選的參數(shù)列表,一個(gè)冒號“:” 和方法的返回類型,一個(gè)等于號“=”,最后是方法的主體。如果你不寫等于號和方法主體,那么方法會被隱式聲明為“抽象”。包含它的類型于是也是一個(gè)抽象類型。我們會在《第5章,Scala 基礎(chǔ)面向?qū)ο缶幊獭分性敿?xì)討論抽象類型。
我們剛才說到“可選的參數(shù)列表”,這意味著一個(gè)或更多。Scala 可以讓你為方法定義一個(gè)以上的參數(shù)列表。這是級聯(lián)方法(currying methods)所需要的。我們會在《第8章 - Scala 函數(shù)式編程》中的“級聯(lián)(Currying)章節(jié)討論它。這個(gè)功能對于定義你自己的域特定語言(DSLs)也很有幫助。我們會在《第11章 - Scala 中的域特定語言》 中看到它。注意,每一個(gè)參數(shù)列表會被括號所包圍,并且所有的參數(shù)由逗號隔開。
如果一個(gè)方法的主體包含多于一個(gè)的表達(dá)式,你必須用大括號{ } 來把它們包起來。你可以在方法主體只有一個(gè)表達(dá)式的時(shí)候省略大括號。
方法的默認(rèn)參數(shù)和命名參數(shù)(Scala 版本2.8)
許多語言都允許你為一個(gè)方法的一個(gè)或多個(gè)參數(shù)定義默認(rèn)值??紤]下面的腳本,一個(gè)StringUtil 對象允許你用一個(gè)用戶定義的分隔符來連接字符串。
- // 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")) )
實(shí)際上,有兩個(gè)“重載”的jioner 方法。第二個(gè)方法使用了一個(gè)空格作為“默認(rèn)”分隔符。寫兩個(gè)函數(shù)似乎有點(diǎn)浪費(fèi),如果我們能消除第二個(gè)joiner 方法,在第一個(gè)jioner 方法里為separator 參數(shù)聲明一個(gè)默認(rèn)值,那就太好了。事實(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ù)列表進(jìn)行增強(qiáng),就是命名參數(shù)。我們實(shí)際上可以用多種方法重寫上一個(gè)例子的最后一行。下面所有的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í)上為每一個(gè)參數(shù)記載了一個(gè)名字。舉例來說,比較注釋#1 和#2 的兩行。在第一行,第二個(gè)參數(shù)“ ”的用處可能不是很明顯。在第二行中,我們提供了參數(shù)名separator,同時(shí)也暗示了參數(shù)的用處。
第二個(gè)好處則是你可以以任何順序指定參數(shù)的順序。結(jié)合默認(rèn)值,你可以像下面這樣寫代碼
- // 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 為你的下一個(gè)Web 2.0 社交網(wǎng)站提供了“可選的”用戶概要信息。它定義了所有字段的默認(rèn)值。這段腳本在創(chuàng)建實(shí)例的時(shí)候提供了0個(gè)或者更多的命名參數(shù)。而參數(shù)的順序卻是任意的。
在這個(gè)我們展示的例子里,常量值被用來作為默認(rèn)值。大多數(shù)支持默認(rèn)參數(shù)的語言只允許編譯時(shí)能決定的常量或者值作為默認(rèn)值。然而,在Scala 里,任何表達(dá)式都可以被作為默認(rèn)值,只要它可以在被使用的時(shí)候正確編譯。比如說,一個(gè)表達(dá)式不能引用類或者對象主體內(nèi)才被計(jì)算的實(shí)例字段,但是它可以引用一個(gè)方法或者一個(gè)單例對象。
一個(gè)類似的限制是一個(gè)參數(shù)的默認(rèn)表達(dá)式不能引用列表中的另外一個(gè)參數(shù),除非被引用的參數(shù)出現(xiàn)在列表的更前面,或者參數(shù)已經(jīng)被級聯(lián)(我們會在《第8章 - Scala 函數(shù)式編程》的“級聯(lián)”這一章節(jié)詳細(xì)討論)。
最后,還有一個(gè)對命名參數(shù)的約束就是一旦你為一個(gè)方法掉哦那個(gè)指定了參數(shù)名稱,那么剩下的在這個(gè)參數(shù)之后的所有參數(shù)都必須是命名參數(shù)。比如,new OptionalUserProfileInfo(age =29, "Earch") 就不能被編譯,因?yàn)榈诙€(gè)參數(shù)不是通過命名方式調(diào)用的。
我們會在《第6章 - Scala 高級面向?qū)ο缶幊獭分械摹癈ase Class(案例類)”中看到另外一個(gè)使用命名參數(shù)和默認(rèn)參數(shù)的例子。
嵌套方法定義
方法定義也可以被嵌套。這里是一個(gè)階乘計(jì)算器的實(shí)現(xiàn),我們會使用一種常規(guī)的方法,通過調(diào)用第二個(gè),嵌套的方法來完成計(jì)算。
- // 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) )
第二個(gè)方法遞歸地調(diào)用了自己,傳遞一個(gè)accumulator 參數(shù),這個(gè)參數(shù)是計(jì)算結(jié)果累積的地方。注意,我們當(dāng)計(jì)數(shù)器i 達(dá)到1 的時(shí)候返回了累積的值。(我們會忽略負(fù)整數(shù)。實(shí)際上這個(gè)函數(shù)在i<0 的時(shí)候會返回1 。)在嵌套方法的定義后面,factorial 以傳入值i 和初始accumulator 值1 來調(diào)用它。
就像很多語言中聲明局部變量一樣,一個(gè)嵌套方法盡在方法內(nèi)部可見。如果你嘗試在factorial 之外去調(diào)用fact,你會得到一個(gè)編譯錯(cuò)誤。
你注意到了嗎,我們兩次把i 作為一個(gè)參數(shù)名字,第一次是在factorial 方法里,然后是在嵌套的fact 方法里。就像在其它許多語言中一樣,在fact 中的i 參數(shù)會屏蔽掉外面factorial 的i 參數(shù)。這樣很好,因?yàn)槲覀冊趂act 中不需要在外面的i 的值。我們只在第一次調(diào)用fact 的時(shí)候需要它,也就是在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。因?yàn)閏ount 嵌套在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])。即使沒有顯示的類型注解,語言的編譯器仍可以從上下文中分辨出相當(dāng)多的類型信息。這里是Scala 的聲明,使用了類型信息的推斷。
- import java.util.Map
- import java.util.HashMap
- ...
- val intToStringMap: Map[Integer, String] = new HashMap
回憶在第1章中Scala 使用方括號來指明范型類型參數(shù)。我們在等號左邊指定了Map[Integer, String]。(我們在例子中還是繼續(xù)使用Java 的類型。)在右邊,我們實(shí)例化了一個(gè)我們實(shí)際需要的對象,一個(gè)HashMap,但是我們不用重復(fù)地聲明類型參數(shù)。
再補(bǔ)充一點(diǎn),假設(shè)我們實(shí)際上并不關(guān)心實(shí)例的類型是不是Map (Java 的接口類型)。我們只需要知道它是HashMap 類型。
- import java.util.Map
- import java.util.HashMap
- ...
- val intToStringMap2 = new HashMap[Integer, String]
這樣的聲明不需要在左邊指定類型注解,因?yàn)樗行枰念愋托畔⒍家呀?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 更少的類型注解,因?yàn)镾cala 的類型推斷算法得同時(shí)支持面向?qū)ο箢愋秃秃瘮?shù)式類型。所以,Scala 比Haskell 這樣的語言需要更多的類型注解。這里有一份關(guān)于Scala 何時(shí)需要顯式類型注解規(guī)則的總結(jié)。
顯式類型注解在何時(shí)是必要的。
從實(shí)用性來講,你必須為下列情況提供顯式的類型注解:
1。變量聲明,除非你給變量賦予了一個(gè)值。(比如,val name = "Programming Scala")
2。所有的方法參數(shù)。(比如,def deposit(amount: Money)
3。下列情況中的方法返回值:
a 當(dāng)你在方法里顯式調(diào)用return 的時(shí)候 (即使是在最后)。
b 當(dāng)一個(gè)方法是遞歸的時(shí)候。
c 當(dāng)方法是重載的,并且其中一個(gè)調(diào)用了另外一個(gè)的時(shí)候。主調(diào)用的方法必須有一個(gè)返回類型的注解。
d 當(dāng)推斷的返回類型比你所想要的更普通時(shí),比如Any。
注意
Any 類型是Scala 類型結(jié)構(gòu)的根類型(參見《第7章 - Scala 對象系統(tǒng)的更多細(xì)節(jié)》中的“Scala 類型結(jié)構(gòu)”章節(jié))。如果一段代碼意外地返回類一個(gè)Any 類型的值,那么很可能類型推斷器不能算出需要返回的類型,所以選擇了最有可能的最通常的類型。
讓我們來看一些需要顯式聲明方法返回類型的例子。在下面的腳本中,upCase 方法有一個(gè)有條件的返回語句,返回非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") )
運(yùn)行這段腳本你會獲得如下錯(cuò)誤。
- ... 6: error: method upCase has return statement; needs result type
- return s
- ^
你可以通過把方法第一行改成如下樣子來修正這個(gè)錯(cuò)誤。
- def upCase(s: String): String = {
實(shí)際上,對于這段腳本,另外一種解決辦法是刪除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)
- ^
重載的方法有時(shí)候也需要顯式返回類型。當(dāng)一個(gè)這樣的方法調(diào)用另外一個(gè)時(shí),我們必須給調(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")) )
兩個(gè)joiner 方法把一系列字符串串在一起。第一個(gè)方法還接受一個(gè)參數(shù)來作為分隔符。第二個(gè)方法調(diào)用第一個(gè)方法,并且傳入一個(gè)空格作為“默認(rèn)”分隔符。
如果你運(yùn)行這段腳本,你會獲得如下錯(cuò)誤。
- ... 9: error: overloaded method joiner needs result type
- def joiner(strings: List[String]) = joiner(strings, "")
因?yàn)榈诙€(gè)jioner 方法調(diào)用了第一個(gè),它需要一個(gè)顯示的String 返回類型。它必須看起來像這樣。
- def joiner(strings: List[String]): String = joiner(strings, " ")
最后的一種場景的關(guān)系可能比較微妙,比你期望的類型更通用的類型可能會被推斷返回。你通常會把函數(shù)返回值賦給擁有更特定類型變量的時(shí)候遇到這樣的錯(cuò)誤。比如,你希望獲得一個(gè)String,但是函數(shù)推斷返回類型為Any。讓我們來看一個(gè)設(shè)計(jì)好的例子來反映會發(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
運(yùn)行這段腳本會獲得如下錯(cuò)誤。
- ...11: error: type mismatch;
- found : List[Any]
- required: List[String]
- val list: List[String] = makeList()
- ^
我們希望makeList 能返回一個(gè)List[String],但是當(dāng)strings.length 等于0 時(shí),我們錯(cuò)誤地假設(shè)List(0) 是一個(gè)空的列表并且將其返回。實(shí)際上,我們返回了一個(gè)有一個(gè)元素0 的List[Int] 對象。我們應(yīng)該返回List()。因?yàn)閑lse 表達(dá)式后返回了strings.toList 的返回值List[String],方法的推斷返回類型就是離List[Int] 和List[String] 最近的公共父類型List[Any]。主意,編譯錯(cuò)誤并不是在函數(shù)定義的時(shí)候出現(xiàn)。我們只有當(dāng)把makeList 返回值賦給一個(gè)List[String] 類型得變量的時(shí)候才看到這個(gè)錯(cuò)誤。
在這種情況下,修正bug 才是正道。另外,有時(shí)候并沒有bug,只是編譯器需要一些顯式聲明的“幫助”來返回正確的類型。研究一下那些似乎返回了非期望類型的方法。以我們的經(jīng)驗(yàn),如果你修改了方法后發(fā)現(xiàn)它返回了比期望的更一般的類型,那么在這種情況下加上顯式返回類型聲明。
另一種避免這樣的麻煩的方式是永遠(yuǎn)為方法返回值聲明類型,特別是為公用API 定義方法的時(shí)候。讓我們重新來看我們的StringUtil 例子來理解為什么顯式聲明是一個(gè)好主意(從[Smith2009a] 改寫)。
這里是我們的StringUtil “API",和一個(gè)新的方法,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 方法以空格來分割字符串,然后返回一個(gè)包含這些子字符串的Array(數(shù)組)。返回類型是推斷出的,我們會看到,這會是一個(gè)潛在的問題所在。這個(gè)方法是計(jì)劃中的,但是會展示我們的重點(diǎn)。下面是一個(gè)使用StringUtil 的這個(gè)方法的客戶端。
- // 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 編譯這些文件,你就能像這樣運(yùn)行客戶端。
- $ scala -cp ... StringUtilClient "Programming Scala"
- Programming
- Scala
注意
類路徑參數(shù) -cp,使用了scalac 寫出class 文件的目錄,默認(rèn)是當(dāng)前目錄(比如,使用-cp.)。如果你使用了下載的代碼示例中的編譯過程,那些class 文件會被寫到build 目錄中區(qū)(使用scalac -d build ...)。在這個(gè)例子里,使用 -cp build.
這個(gè)時(shí)候,一切都工作正常。但是現(xiàn)在想象一下代碼庫擴(kuò)大以后,StringUtil 和它的客戶端被分別編譯然后捆綁到不同的jar 文件中去。再想象一下StringUtil 的維護(hù)者決定返回一個(gè)List 來替代原來的默認(rèn)值。
- object StringUtil {
- ...
- def toCollection(string: String) = string.split(' ').toList // changed!
- }
唯一的區(qū)別是最后的對toList 的調(diào)用,把一個(gè)Array 轉(zhuǎn)換成了List。重新編譯StringUtil 并且部署為jar 文件。然后運(yùn)行相同的客戶端,先不要重新編譯。
- $ 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ā)生了什么?當(dāng)客戶端被編譯的時(shí)候,StringUtil.toCollection 返回了一個(gè)Array。然后toCollection 被修改為返回一個(gè)List。在兩個(gè)版本里,方法返回值都是被推斷出來的。因此,客戶端也必須被重新編譯。
然而,如果顯式地聲明返回類型是Seq,作為Array 和List 的共同父類型,這樣的實(shí)現(xiàn)就不會對客戶端要求重新編譯。
注意
當(dāng)開發(fā)獨(dú)立于客戶端的API 的時(shí)候,顯式地聲明方法返回類型,并且盡可能使用更一般的返回類型。這在API 被聲明為抽象方法時(shí)尤其重要。(參見,比如《第4章 - 特性》。)
還有另外一種場景需要考慮集合聲明的使用,比如val map = Map(),就像下面這個(gè)例子。
- 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)建時(shí)被推斷為[Nothing, Nothing]。(我們會在《第7章 - Scala 對象系統(tǒng)》的“Scala 類型結(jié)構(gòu)”章節(jié)討論Nothing。但是它的名字本身就解釋了自己?。┪覀儑L試插入一對不匹配的String,String 鍵值對。叫它拿都去不了的地圖吧!解決方案是,在初始化map 聲明的時(shí)候指出參數(shù)類型,例如val map = Map[String, String]() 或者指定初始值以便于map 參數(shù)被推斷,例如val map = Map("Programming"->"Scala")。
最后,還有一個(gè)推斷返回類型可能導(dǎo)致不可預(yù)知的令人困擾的結(jié)果[Scala 提示]的詭異行為??紤]下面的scala 對話例子。
- scala> def double(i: Int) { 2 * i }
- double: (Int)Unit
- scala> println(double(2))
- ()
為什么第二個(gè)命令打印出() 而不是4?仔細(xì)看scala 解釋器給出的第一個(gè)命令的返回值,double: (Int)Unit。我們定義了一個(gè)方法叫double,接受一個(gè)Int 參數(shù),返回Unit。方法并不像我們期望的那樣返回Int。
造成這樣意外結(jié)果的原因是在方法定義中缺少的等于號。下面是我們實(shí)際上需要的定義。
- scala> def double(i: Int) = { 2 * i }
- double: (Int)Int
- scala> println(double(2))
注意double 主體前的等于號。現(xiàn)在,輸出說明我們定義了一個(gè)返回Int 的double,第二個(gè)命令完成了我們期望的工作。
這樣的行為是有原因的。Scala 把主體之前的部分包含等于號作為函數(shù)定義,而一個(gè)函數(shù)在函數(shù)式編程中永遠(yuǎn)都有返回值。另一方面來說,當(dāng)Scala 看到一個(gè)函數(shù)主體而沒有等于號前綴時(shí),它就假設(shè)程序員希望這個(gè)方法變成一個(gè)“過程”定義,希望獲得由返回值Unit 帶來的副作用。而在實(shí)際中,結(jié)果往往是程序員簡單地忘記了插入等于號!
警告
當(dāng)方法的放回類型被推斷而你又沒有在方法主體的大括號前使用等于號的時(shí)候,即使方法的最后一個(gè)表達(dá)式是另外一個(gè)類型的值,Scala 也會推斷出一個(gè)Unit 返回類型。
順便說一句,之前我們修正bug 前打印出來的() 是哪里來的?事實(shí)上這是Unit 類型單體實(shí)例的真正名字?。ㄟ@個(gè)名字是函數(shù)式編程的協(xié)定。)
#p#
常值
一個(gè)對象經(jīng)常會用一個(gè)常值來初始化,比如val book = "Programming Scala"。下面我們來討論一下Scala 支持的常值種類。這里我們只討論字符常值。我們會在后面遇到函數(shù)(被用作值,而不是成員方法),tuples,Lists,Maps 等的常值語法的時(shí)候再繼續(xù)討論。
整數(shù)(Integer)
整數(shù)常值可以表達(dá)為:十進(jìn)制,十六進(jìn)制,或者八進(jìn)制。細(xì)節(jié)總結(jié)參見“表2.1, 整數(shù)常值”
| 種類 | 格式 | 例子 |
| 十進(jìn)制 | 0 ,或者非零數(shù)字后面跟隨0 個(gè)或者多個(gè)十進(jìn)制字符 (0 - 9) | 0, 1, 321 |
| 十六進(jìn)制 | 0x 后面跟隨一個(gè)或多個(gè)十六進(jìn)制字符 (0-9, A-F, a-f) | 0xFF, 0x1a3b |
| 八進(jìn)制 | 0 后面跟隨一個(gè)或多個(gè)八進(jìn)制字符 (0-7) | 013, 077 |
對于長整型值,必須在常值的后面加上L 或者l 字符。否則會被判定為普通整型。整數(shù)的有效值由被賦值的變量類型來決定。表2.2,“整型數(shù)的允許范圍(包括邊界)” 定義了整數(shù)的極限,包括邊界值。
| 目標(biāo)類型 | 最小值 (包括) | 最大值 (包括) |
| Long(長整型) | ?263 | 263 - 1 |
| Int (整型) | ?231 | 231 - 1 |
| Short (短整型) | ?215 | 215 - 1 |
| Char (字符) | 0 | 216 - 1 |
| Byte (字節(jié)) | ?27 | 27 - 1 |
如果一個(gè)整數(shù)的值超出了允許范圍,就會發(fā)生編譯錯(cuò)誤,比如下面這個(gè)例子。
- 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
浮點(diǎn)數(shù)(Float)
Float 由0 個(gè)或多個(gè)數(shù)字,加上一個(gè)點(diǎn)號,再加上0 個(gè)或多個(gè)數(shù)字組成。如果在點(diǎn)號前面沒有數(shù)字,比如數(shù)字比1.0 要小,那么在點(diǎn)號后面必須有一個(gè)或者多個(gè)數(shù)字。對于浮點(diǎn)數(shù),需要在常值的最后加上F 或者f 。否則默認(rèn)判定為雙精度浮點(diǎn)數(shù)(Double)。你可以選擇給一個(gè)雙精度浮點(diǎn)數(shù)加上D 或者d。
浮點(diǎn)數(shù)可以用指數(shù)方法表達(dá)。指數(shù)部分的格式是e 或者E,加上一個(gè)可選的+或者-,再加上一個(gè)或多個(gè)數(shù)字。
這里有一些浮點(diǎn)數(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位單精度二進(jìn)制浮點(diǎn)數(shù)值的規(guī)范。Double 遵循了IEEE 754 64位雙精度二進(jìn)制浮點(diǎn)數(shù)值的規(guī)范。
警告
為了防止解析時(shí)的二義性,如果一個(gè)浮點(diǎn)數(shù)后面跟隨著一個(gè)字母開頭的符號,你必須在浮點(diǎn)數(shù)后面跟隨至少一個(gè)空格。比如,表達(dá)式1.toString 返回整數(shù)1 的字符串形式,而1. toString 則返回浮點(diǎn)數(shù)1.(0) 的字符串形式。
布爾值
布爾值可以是true (真) 或者false (假)。被賦值的變量的類型會被推斷為Boolean。
- scala> val b1 = true
- b1: Boolean = true
- scala> val b2 = false
- b2: Boolean = false
字符常值
一個(gè)字符常值是一個(gè)單引號內(nèi)的可打印的Unicode 字符或者一個(gè)轉(zhuǎn)義序列。一個(gè)可以用Unicode 值0 到255 表示的字符也可以用一個(gè)八進(jìn)制轉(zhuǎn)義來表示:一把反斜杠加上最多3個(gè)八進(jìn)制字符序列。如果在字符或者字符串中反斜杠后面不是一個(gè)有效的轉(zhuǎn)義序列則會出現(xiàn)編譯錯(cuò)誤。
這里有一些例子.
- ’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 網(wǎng)站標(biāo)題:Scala編程指南更少的字更多的事 分享路徑:http://m.5511xx.com/article/ccecjcp.html |


咨詢
建站咨詢
