Scala学习(二)--- 控制结构和函数
| 控制結構和函數 |
摘要:
本篇主要學習在Scala中使用條件表達式、循環和函數,你會看到Scala和其他編程語言之間一個根本性的差異。在Java或C++中,我們把表達式(比如3+4)和語句(比如if語句)看做兩樣不同的東西。表達式有值,而語句執行動作。在Scala中,幾乎所有構造出來的語法結構都有值。這個特性使得程序更加精簡,也更易讀。本篇的要點包括:
1. 表達式有值
2. 塊也有值,是它最后一個表達式的值
3. Scala的for循環就像是"增強版"的Java for循環
4. 分號(在絕大多數情況下)不是必需的
5. void類型是Unit
6. 避免在函數定義中使用return
7. 注意別在函數式定義中漏掉了=
8. 異常的工作方式和Java或C++中基本一樣,不同的是你在catch語句中使用"模式匹配"
9. Scala沒有受檢異常
| 條件表達式 |
表達式的值
Scala的if/else語法結構,和Java或C++-樣。不過,在Scala中if/else表達式有值,這個值就是跟在if或else之后的表達式的值。例如:
if (x > 0) 1 else-1
上述表達式的值是1或-1,具體是哪一個取決于x的值。你可以將if/else表達式的值賦值給變量:
val S=if (x > 0) 1 else -1
這與如下語句的效果一樣:
if (x > 0) S=1 else S=-1
不過,第一種寫法更好,因為它可以用來初始化一個val。而在第二種寫法當中,S必須是var
Java和C++有一個 ? : 操作符用于同樣目的。如下表達式
x > 0 ? 1: -1 // Java或c++
等同于Scala表達式if(x>0) 1 else -1。不過,你不能在 ? : 表達式中插入語句。Scala的if/else將在Java和C++中分開的兩個語法結構if/else和? :結合在了一起
表達式的類型
在Scala中,每個表達式都有一個類型。舉例來說,表達式:
if(x > 0) 1 else-1
上述表達式的類型是lnt,因為兩個分支的類型都是Int。混合類型表達式,比如:
if (x > 0) "positive" else -1
上述表達式的類型是兩個分支類型的公共超類型。在本例中,其中一個分支是java.lang.String,而另一個分支是lnt。它們的公共超類型叫做Any 。如果else部分缺失了,比如:
if (x > 0) 1
那么有可能該語句沒有輸出值。但是在Scala中,每個表達式都應該有某種值。這個問題的解決方案是引入一個Unit類,寫做()。不帶else的這個if語句等同于:
if (x > 0) 1 else ()
你可以把()當做是表示"無有用值"的占位符,將Unit當做Java或C++中的void。從技術上講,void沒有值但是Unit有一個表示"無值"的值。如果你一定要深究的話,這就好比空的錢包和里面有一張寫著"沒錢"的無面值鈔票的錢包之間的區別
| 塊表達式和賦值 |
塊表達式
在java或C++中,塊語句是一個包含于{}中的語句序列。每當你需要在邏輯分支或循環中放置多個動作時,你都可以使用塊語句。在 Scala中,{}塊包含一系列表達式,其結果也是一個表達式。塊中最后一個表達式的值就是塊的值。
這個特性對于那種對某個val的初始化需要分多步完成的情況很有用。例如:
val distance={val dx = x - x0 ; val dy = y - y0 ; sqrt(dx*dx+dy*dy) }
{}塊的值取其最后一個表達式,在此處以紅色字體標出。變量dx和dy僅作為計算所需要的中間值,很干凈地對程序其他部分而言不可見了。
賦值
在Scala中,賦值動作本身是沒有值的或者更嚴格地說,它們的值是Unit類型的。你應該還記得,Unit類型等同于Java和C++中的void,而這個類型只有一個值,寫做()。一個以賦值語句結束的塊,比如
{ r=r*n;n-=1;}
上述塊表達式的值是Unit類型。這沒有問題,只是當我們定義函數時需要意識到這一點。由于賦值語句的值是Unit類型的,別把它們串接在一起:
x=y=1 //別這樣做
y=1的值是(),你幾乎不太可能想把一個Unit類型的值賦值給x。而在Java和C++中,賦值語句的值是被賦的那個值。在這些語言中,將賦值語句串接在一起是有意義的。
| 輸入輸出 |
輸出
如果要打印一個值,我們用print或println函數。后者在打印完內容后會追加一個換行符。舉例來說
print ("Answer : ")
println (42)
與下面的代碼輸出的內容相同:
println("Answer: "+42)
另外,還有一個帶有C風格格式化字符串的printf函數:
printf("Hello, %s! You are%d years old.\n", "Fred",42)
輸入
你可以用readLine函數從控制臺讀取一行輸入。如果要讀取數字、Boolean或者是字符,可以用readlnt、readDouble、 readByte、readShort、readLong、readFloat、readBoolean或者readChar。與其他方法不同,readLine帶一個參數作為提示字符串:
val name=readLine ("Your name: ")
print("Your age:")
val age=readlnt()
printf("Hello, %s! Next year, your will be %d.\n", name, age+1)
| 循環 |
While循環
Scala擁有與Java和C++相同的while和do循環。例如:
while (n>0) {
r=r*n
n-=1
}
for循環
Scala沒有與for ( 初始化變量;檢查變量是否滿足某條件;更新變量 ) 循環直接對應的結構。如果你需要這樣的循環,有兩個選擇:一是使用while循環,二是使用如下for句:
for (i <- 1 to n)
r=r*i
通過Richlnt類的這個to方法,1 to n這個調用返回數字1到數字n(含)的Range(區間)。下面的這個語法結構
for (i <- 表達式)
讓變量i遍歷<- 右邊的表達式的所有值。至于這個遍歷具體如何執行,則取決于表達式的類型。對于Scala集合比如Range而言,這個循環會讓i依次取得區間中的每個值。
遍歷字符串或數組時,你通常需要使用從0到n-1的區間。這個時候你可以用util方法而不是to方法。util方法返回一個并不包含上限的區間。
val s="Hello"
var sum=0
for (i <- 0 util s.length)//i的最后一個取值是s.length -1
sum+=s(i)
在本例中,事實上我們并不需要使用下標。你可以直接遍歷對應的字符序列:
var sum=0
for (ch <- "Hello" )
sum+=ch
在Scala中,對循環的使用并不如其他語言那么頻繁。通常我們可以通過對序列中的所有值,應用某個函數的方式來處理它們,而完成這項工作只需要一次方法調用即可
| 高級for循環和for推導式 |
在Scala中,for循環比起Java和C++的功能要豐富得多,下面將介紹其高級特性
生成器
你可以以變量 <- 表達式的形式提供多個生成器,用分號將它們隔開。例如:
for(i <- 1 to 3;j <- 1 to 3){
print(10*i+j+"\t") //將打印11 12 13 21 22 23 31 32 33
}
每個生成器都可以帶一個守衛,以if開頭的Boolean表達式:
for(i <- 1 to 3 ; j <- 1 to 3 if i!=j){
print(10*i+j+"\t") //將打印12 13 21 23 31 32
}
注意在if之前并沒有分號。
引入定義
除此之外,你可以使用任意多的定義,引入可以在循環中使用的變量:
for(i <- 1 to 3;form=4-i;j <- form to 3 ){
print(10*i+j+"\t") //將打印13 22 23 31 32 33
}
for 推導式
如果for循環的循環體以yield開始,則該循環會構造出一個集合,每次迭代生成集合中的一個值:
for(i <- 1 to 10) yield i%3 // 生成Vector(1,2,0,1,2,0,1,2,0,1)
這類循環叫做for推導式。for推導式生成的集合與它的第一個生成器是類型兼容的
for (c <- "Hello"; i <- 0 to 1) yield (c + i).toChar //將生成HIeflmlmop
for (i <- 0 to 1; c <- "Hello") yield (c + i).toChar //將生成Vector(H, e, l, l, o, I, f,m, m, p)
| 函數 |
普通函數
Scala除了方法外還支持函數。方法對對象進行操作,函數不是。C++也有函數,不過在Java中我們只能用靜態方法來模擬。要定義函數,你需要給出函數的名稱、參數和函數體,就像這樣:
def abs(x:Double) = if (x>0) x else -x
你必須給出所有參數的類型。不過,只要函數不是遞歸的,你就不需要指定返回類型。Scala編譯器可以通過=符號右側的表達式的類型推斷出返回類型。如果函數體需要多個表達式完成,可以用代碼塊。塊中最后一個表達式的值就是函數的返回值。舉例來說,下面的這個函數返回位于for循環之后的r的值。
def fac(n:Int) = {
var r=1
for(i <- 1 to n)
r=r*i;
r
}
在本例中我們并不需要用到return。我們也可以像Java或C++那樣使用retum,來立即從某個函數中退出,不過在Scala中這種做法并不常見。
遞歸函數
對于遞歸函數,我們必須指定返回類型。例如:
def fac(n:Int) : Int = if(n <= 0) 1 else n*fac(n-1)
如果沒有返回類型,Scala編譯器無法校驗n*fac(n - 1)的類型是Int。某些編程語言(如ML和Haskell)能夠推斷出遞歸函數的類型,用的是Hindley-Milner算法。不過,在面向對象的語言中這樣做并不總是行得通。如何擴展Hindley-Milner算法讓它能夠處理子類型仍然是個科研命題。
| 默認參數和帶名參數 |
指定默認參數
我們在調用某些函數時并不顯式地給出所有參數值,對于這些函數我們可以使用默認參數。例如:
def decorate (str : String, left : String="[", right : String="]")=
left+str+right
這個函數有兩個參數,left和right,帶有默認值"["和"]"。如果你調用decorate("Hello"),你會得到" [Hello]"。如果你不喜歡默認的值,可以給出你自己的版本:
decorate("Hello","<<<",">>>")
如果相對參數的數量,你給出的值不夠,默認參數會從后往前逐個應用進來。舉例來說:
decorate("Hello",">>>[")
則會使用right自帶的默認參數,得到:"<<<[Hello] "。
指定參數名
你也可以在提供參數值的時候指定參數名。例如:
decorate (left="<<<", str ="Hello", right=">>>")
結果是"<Hello>>>",由上可知帶名參數并不需要跟參數列表的順序完全一致。帶名參數可以讓函數更加可讀。它們對于那些有很多默認參數的函數來說也很有用。
混用參數名
當然,也可以混用未命名參數和帶名參數,只耍那些未命名的參數是排在前面的即可:
decorate ("Hello",right="]<<<")
上面代碼,將調用decorate ("Hello","[" ,"]<<<")
| 變長參數 |
有時候,實現一個可以接受可變長度參數列表的函數會更方便。以下示例顯示了它的語法:
def sum (args : Int*)={
var result=0
for (arg <- args)
result +=arg
result
}
那么,可以使用任意多的參數來調用該函數
val s=sum (1, 4, 9, 16, 25)
函數得到的是一個類型為Seq的參數,可以使用for循環來訪問每一個元素。
如果你已經有一個值的序列,則不能直接將它傳入上述函數。舉例來說,如下的寫法是不對的:
val s =sum(1 to 5)
如果sum函數被調用時傳人的是單個參數,那么該參數必須是單個整數,而不是一個整數區間。解決這個問題的辦法是告訴編譯器你希望這個參數被當做參數序列處
理。追加: _*,就像這樣:
val s=sum(l to 5._*) //將1 to 5當做參數序列處理
在遞歸定義當中我們會用到上述語法:
def recursiveSum (args: Int*): Int:{
if (args.length==0)
0
else
args.head+recursiveSum( args.tail : _* )
}
在這里,序列的head是它的首個元素,而tail是所有其他元素的序列,這又是一個Seq,我們用:_*來將它轉換成參數序列
| 過程 |
Scala對于不返回值的函數有特殊的表示法。如果函數體包含在花括號當中但沒有前面的=號,那么返回類型就是Unit。這樣的函數被稱做過程(procedure),過程不返回值,我們調用它僅僅是為了它的副作用。舉例來說,如下過程把一個字符串打印在一個框中,就像這樣:
---------
|Hello|
---------
由于過程不返回任何值,所以我們可以略去;號。
def box(s:String) { //注意前面沒有=
var border="-" * s.length+"--\n"
println(border+"|"+s+"|\n"+border)
}
有人不喜歡用這種簡明的寫法來定義過程,并建議大家總是顯式聲明Unit返回類型:
def box (s: String): Unit=(
}
| 懶值 |
當val被聲明為lazy時,它的初始化將被推遲,直到我們首次對它取值。例如:
lazy val words=scala.io.Source.fromFile("/usr/share/dict /words").mkString
這個調用將從一個文件讀取所有字符并拼接成一個字符串, 如果程序從不訪問words,那么文件也不會被打開。為了驗證這個行為,我們可以在REPL中試驗,但故意拼錯文件名。在初始化語句被執行的時候并不會報錯。不過,一旦你訪問words,就將會得到一個錯誤提示:文件未找到。
懶值對于開銷較大的初始化語句而言十分有用。它們還可以應對其他初始化問題,比如循環依賴。更重要的是,它們是開發懶數據結構的基礎。你可以把懶值當做是介于val和def的中間狀態。對比如下定義:
val words =scala.io.Source.fromFile ("/usr/share/dict/words") .mkString // 在words被定義時即被取值
lazy val words=scala.jo.Source.fromFile("/usr/share/dict/words").mkString // 在words被首次使用時取值
def words=scala.io.Source.fromFile("/usr/share/dict/words") .mkString // 在每一次words被使用時取值
需要注意的是:懶值并不是沒有額外開銷。我們每次訪問懶值,都會有一個方法被調用,而這個方法將會以線程安全的方式檢查該值是否已被初始化。
| 異常 |
Scala異常機制
Scala異常的工作機制和Java或C++ 一樣,當你拋出異常時,比如:
throw new IllegalArgumentException("x should not be neqativen")
當前的運算被中止,運行時系統查找可以接受IllegaIArgumentException的異常處理器,控制權將在離拋出點最近的處理器中恢復。如果沒有找到符合要求的異常處理器,則程序退出。
和Java一樣,拋出的對象必須是java.lang.Throwable的子類。不過,與Java不同的是,Scala沒有"受檢"異常,即你不需要聲明說函數或方法可能會拋出某種異常。在Java中, "受檢"異常在編譯期被檢查。如果你的方法可能會拋出IOException,你必須做出聲明。這就要求程序員必須去想那些異常應該在哪里被處理掉,這是個值得稱道的目標。不幸的是,它同時也催生出怪獸般的方法簽名,比如void doSometing() throws IOException,InterruptedException,ClassNotFoundException。許多Java程序員很反感這個特性,最終過早捕獲這些異常,或者使用超通用的異常類。Scala的設計者們決定不支持"受檢"異常,因為他們意識到徹底的編譯期檢查并不總是最好的。
throw表達式有特殊的類型Nothing。這在if/else表達式中很有用。如果一個分支的類型是Nothing,那么if/else表達式的類型就是另—個分支的類型。舉例來說,考慮如下代碼:
if (x > 0){
sqrt(x)
else
throw new IllegalArgumentException("x should not be negative")
第一個分支類型是Double,第二個分支類型是Nothing。因此,if/else表達式的類型是Double。
Scala異常捕捉
捕獲異常的語法采用的是模式匹配的語法:
try{
process (new URL( "http: //horstmann.com/fred-tiny. gif"))
}catch{
case _: MalformedURLException=>println("Bad URL: "+url)
case ex: IOException=>ex.printStacKTrace()
}
和Java或C++一樣,更通用的異常應該排在更具體的異常之后。而且,如果你不需要使用捕獲的異常對象,可以使用_來替代變量名。
try/finally釋放資源
try/finally語句讓你可以釋放資源,不論有沒有異常發生。例如:
var in=new URL("http://horstmann.com/fred.gif") .openStream))
try{
process{in)
}finally{
in. close()
}
finally語句不論process函數是否拋出異常都會執行,reader總會被關閉。這段代碼有些微妙,也提出了一些問題:
■ 如果URL構造器或openStream方法拋出異常怎么辦,這樣一來try代碼塊和finally語句都不會被執行。這沒什么不好,in從未被初始化,因此調用close方法沒
有意義
■ 為什么val in=new URL(...).openStream()不放在try代碼塊里,因為這樣做的話in的作用域不會延展到finally語句當中
■ 如果in.close()拋出異常怎么辦,這樣一來異常跳出當前語句,廢棄并替代掉所有先前拋出的異常。這跟Java一樣,并不是很完美,理想情況是老的異常應
該與新的異常一起保留
除此之外,try/catch和try/finally的目的是互補的。try/catch語句處理異常,而try/finally語句在異常沒有被處理時執行某種動作,通常是清理工作。我們可以把它們結合在一起成為單個try/catch/finally語句:
try { …} catch {…} finally { …}
?
如果,您認為閱讀這篇博客讓您有些收獲,不妨點擊一下右下角的【推薦】。
如果,您希望更容易地發現我的新博客,不妨點擊一下左下角的【關注我】。
如果,您對我的博客所講述的內容有興趣,請繼續關注我的后續博客,我是【Sunddenly】。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
總結
以上是生活随笔為你收集整理的Scala学习(二)--- 控制结构和函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HDU 3549 Flow Proble
- 下一篇: Bootstrap系列 -- 26. 下