Scala學習(二)--- 控制結構和函數

控制結構和函數java

摘要c++

本篇主要學習在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】。

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索