Scala學習筆記2
===控制結構
scala和其他程式語言有一個根本性差異:在scala中,幾乎所有構造出來的語法結構都有值。這個特性使得程式結構更加精簡。scala內建的控制結構很少,僅有if、while、for、try、match和函式呼叫等而已。如此之少的理由是,scala從語法層面上支援函式字面量。
if表示式:
scala的if/else語法結構與java等一樣,但是在scala中if/else表示式有值,這個值就是跟在if/esle後邊的表示式的值。如下:
val s = if(x > 0) 1 else -1
同時注意:scala的每個表示式都有一個型別,比如上述if/esle表示式的型別是Int。如果是混合型別表示式,則表示式的型別是兩個分支型別的公共超型別。String和Int的超型別就是Any。如果一個if語句沒有else部分,則當if條件不滿足時,表示式結果為Unit。如:
if(x > 0) 1
就相當於:
if(x > 0) 1 else ()
while迴圈:
scala擁有與java和c++中一樣的while和do-while迴圈,while、do-while結果型別是Unit。
for表示式:
scala中沒有類似於for(; ;)的for迴圈,你可以使用如下形式的for迴圈語句:
for(i <- 表示式)
該for表示式語法對於陣列和所有集合類均有效。具體介紹如下:
列舉:for(i <-1 to 10),其中“i <- 表示式”語法稱之為發生器
過濾:也叫守衛,在for表示式的發生器中使用過濾器可以通過新增if子句實現,如:for(i <- 1 to 10 if i!=5),如果要新增多個過濾器,即多個if子句的話,要用分號隔開,如:for(i <- 1 to 10 if i!=5; if i!=6)。
巢狀列舉:如果使用多個“<-”子句,你就得到了巢狀的“迴圈”,如:for(i<- 1 to 5; j <- 1 to i)。
流間變數繫結:你可以在for發生器以及過濾器等中使用變數儲存計算結果,以便在迴圈體中使用,從而避免多次計算以得到該結果。流間變數繫結和普通變數定義相似,它被當作val,但是無需宣告val關鍵字。
製造新集合:for(…) yield變數/迴圈體,最終將產生一個集合物件,集合物件的型別與它第一個發生器的型別是相容的。
實際上:for表示式具有等價於組合應用map、flatMap、filter和foreach這幾種高階函式的表達能力。實際上,所有的能夠yield(產生)結果的for表示式都會被編譯器轉譯為高階方法map、flatMap及filter的組合呼叫;所有的不帶yield的for迴圈都會被轉譯為僅對高階函式filter和foreach的呼叫。正是由於這幾個高階函式支援了for表示式,所以如果一個數據型別要支援for表示式,它就要定義這幾個高階函式。有些時候,你可以使用for表示式代替map、flatMap、filter和foreach的顯式組合應用,或許這樣會更清晰明瞭呢。
scala中沒有break和continue語句。如果需要類似的功能時,我們可以:
1)使用Boolean型別的控制變數
2)使用巢狀函式,你可以從函式當中return
3)...
塊表示式與賦值:
在scala中,{}塊包含一系列表示式,其結果也是一個表示式,塊中最後一個表示式的值就是其值。
在scala中,賦值語句本身的值是Unit型別的。因此如下語句的值為“()”:
{r = r * n; n -= 1}
正是由於上述原因,scala中不能多重賦值,而java和c++卻可以多重賦值。因此,在scala中,如下語句中的x值為“()”:
x = y = 1
match表示式與模式匹配:
scala中沒有switch,但有更強大的match。它們的主要區別在於:
① 任何型別的常量/變數,都可以作為比較用的樣本;
② 在每個case語句最後,不需要break,break是隱含的;
③ 更重要的是match表示式也有值;
④ 如果沒有匹配的模式,則MatchError異常會被丟擲。
match表示式的形式為:選擇器 match{備選項 }。一個模式匹配包含了一系列備選項,每個都開始於關鍵字case。每個備選項都包含了一個模式以及一到多個表示式,它們將在模式匹配過程中被計算。箭頭符號“=>”隔開了模式和表示式。按照程式碼先後順序,一旦一個模式被匹配,則執行“=>”後邊的表示式((這些)表示式的值就作為match表示式的值),後續case語句不再執行。示例如下:
a match {
case1 => "match 1"
case_ => "match _"
}
match模式的種類如下:
① 通配模式:可以匹配任意物件,一般作為預設情況,放在備選項最後,如:
case _ =>
② 變數模式:類似於萬用字元,可以匹配任意物件,不同的是匹配的物件會被繫結在變數上,之後就可以使用這個變數操作物件。所謂變數就是在模式中臨時生成的變數,不是外部變數,外部變數在模式匹配時被當作常量使用,見常量模式。注意:同一個模式變數只能在模式中出現一次。
③ 常量模式:僅匹配自身,任何字面量都可以作為常量,外部變數在模式匹配時也被當作常量使用,如:
case "false" =>"false"
case true=> "truth"
case Nil=> "empty list"
對於一個符號名,是變數還是常量呢?scala使用了一個簡單的文字規則對此加以區分:用小寫字母開始的簡單名被當作是模式變數,所有其他的引用被認為是常量。如果常量是小寫命名的外部變數,那麼它就得特殊處理一下了:如果它是物件的欄位,則可以加上“this.”或“obj.”字首;或者更通用的是使用字面量識別符號解決問題,也即用反引號“`”包圍之。
④ 抽取器模式:抽取器機制基於可以從物件中抽取值的unapply或unapplySeq方法,其中,unapply用於抽取固定數量的東東,unapplySeq用於抽取可變數量的東東,它們都被稱為抽取方法,抽取器正是通過隱式呼叫抽取方法抽取出對應東東的。抽取器中也可以包含可選的apply方法,它也被稱作注入方法,注入方法使你的物件可以當作構造器來用,而抽取方法使你的物件可以當作模式來用,物件本身被稱作抽取器,與是否具有apply方法無關。樣本類會自動生成伴生物件並新增一定的句法以作為抽取器,實際上,你也可以自己定義一個任意其他名字的單例物件作為抽取器使用,以這樣的方式定義的抽取器物件與樣本類型別是無關聯的。你可以對陣列、列表、元組進行模式匹配,這正是基於抽取器模式的。
⑤ 型別模式:你可以把型別模式當作型別測試和型別轉換的簡易替代,示例如下:
case s: String => s.length
⑥ 變數繫結:除了獨立的變數模式之外,你還可以把任何其他模式繫結到變數。只要簡單地寫上變數名、一個@符號,以及這個模式。
模式守衛:模式守衛接在模式之後,開始於if,相當於一個判斷語句。守衛可以是任意的引用模式中變數的布林表示式。如果存在模式守衛,只有在守衛返回true的時候匹配才算成功。
Option型別:scala為可選值定義了一個名為Option的標準型別,一個Option例項的值要麼是Some型別的例項,要麼是None物件。分離可選值最通常的辦法是通過模式匹配,如下:
case Some(s) => s
case None => “?”
模式無處不在:在scala中,模式可以出現在很多地方,而不單單在match表示式裡。比如:
① 模式使用在變數定義中,如下:
val myTuple = (123, “abc”)
val (number, string) = myTuple
② 模式匹配花括號中的樣本序列(即備選項)可以用在能夠出現函式字面量的任何地方,實質上,樣本序列就是更普遍的函式字面量,函式字面量只有一個入口點和引數列表,樣本序列可以有多個入口點,每個都有自己的引數列表,每個樣本都是函式的一個入口點,引數被模式所特化。如下:
val withDefault: Option[Int] => String = {
case Some(x) => "is int"
case None=> "?"
}
③ for表示式裡也可以使用模式。示例如下:
for((number, string) <- myTuple) println(number +string)
模式匹配中的中綴標註:帶有兩個引數的方法可以作為中綴操作符使用,使用中綴操作符時實際上是其中一個運算元在呼叫操作符對應的方法,而另一個運算元作為方法的引數。但對於模式來說規則有些不同:如果被當作模式,那麼類似於p op q這樣的中綴標註等價於op(p,q),也就是說中綴標註符op被用做抽取器模式。
===函式
函式定義:
定義函式時,除了遞迴函式之外,你可以省略返回值型別宣告,scala會根據=號後邊的表示式的型別推斷返回值型別,同時=號後邊表示式的值就是函式的返回值,你無需使用return語句(scala推薦你使用表示式值代替return返回值,當然根據你的需要,也可以顯式使用return返回值)。示例如下:
def abs(x: Double) = if(x >= 0) x else -x
def fac(n: Int) = {
var r = 1
for(i <- 1 to n)r = r * i
r
}
對於遞迴函式必須指定返回值型別,如下:
def fac(n: Int) : Int = if(n <= 0 ) 1else n * fac(n-1)
但你要知道的是:宣告函式返回型別,總是有好處的,它可以使你的函式介面清晰。因此建議不要省略函式返回型別宣告。
函式體定義時有“=”時,如果函式僅計算單個結果表示式,則可以省略花括號。如果表示式很短,甚至可以把它放在def的同一行裡。
去掉了函式體定義時的“=”的函式一般稱之為“過程”,過程函式的結果型別一定是Unit。因此,有時定義函式時忘記加等號,結果常常是出乎你的意料的。
沒有返回值的函式的預設返回值是Unit。
函式呼叫:
scala中,方法呼叫的空括號可以省略。慣例是如果方法帶有副作用就加上括號,如果沒有副作用就去掉括號。如果在函式定義時,省略了空括號,那麼在呼叫時,就不能加空括號。另外,函式作為操作符使用時的呼叫形式參見相應部分。
函式引數:
一般情況下,scala編譯器是無法推斷函式的引數型別的,因此你需要在引數列表中宣告引數的型別。對於函式字面量來說,根據其使用環境的不同,scala有時可以推斷出其引數型別。
scala裡函式引數的一個重要特徵是它們都是val(這是無需宣告的,在引數列表裡你不能顯式地宣告引數變數為val),不是var,所以你不能在函式裡面給引數變數重新賦值,這將遭到編譯器的強烈反對。
重複引數:
在scala中,你可以指明函式的最後一個引數是重複的,從而允許客戶向函式傳入可變長度引數列表。要想標註一個重複引數,可在引數的型別之後放一個星號“*”。例如:
def echo(args: String*) = for(arg <-args) println(arg)
這樣的話,echo就可以被零至多個String引數呼叫。在函式內部,重複引數的型別是宣告引數型別的陣列。因此,echo函式裡被宣告為型別“String*”的args的型別實際上是Array[String]。然而,如果你有一個合適型別的陣列,並嘗試把它當作重複引數傳入,會出現編譯錯誤。要實現這個做法,你需要在陣列名後新增一個冒號和一個_*符號,以告訴編譯器把陣列中的每個元素當作引數,而不是將整個陣列當作單一的引數傳遞給echo函式,如下:
echo(arr: _*)
預設引數與命名引數:
函式的預設引數與java以及c++中相似,都是從左向右結合。另外,你也可以在呼叫時指定引數名。示例如下:
def fun(str: String, left: String = “[”,right: String = “]”) = left + str + right
fun(“hello”)
fun(“hello”, “<<<”)
fun(“hello”, left =“<<<”)
函式與操作符:
從技術層面上來說,scala沒有操作符過載,因為它根本沒有傳統意義上的操作符。諸如“+”、“-”、“*”、“/”這樣的操作符,其實呼叫的是方法。方法被當作操作符使用時,根據使用方式的不同,可以分為:中綴標註(操作符)、字首標註、字尾標註。
中綴標註:中綴操作符左右分別有一個運算元。方法若只有一個引數(實際上是兩個引數,因為有一個隱式的this),呼叫的時候就可以省略點及括號。實際上,如果方法有多個顯式引數,也可以這樣做,只不過你需要把引數用小括號全部括起來。如果方法被當作中綴操作符來使用(也即省略了點及括號),那麼左運算元是方法的呼叫者,除非方法名以冒號“:”結尾(此時,方法被右運算元呼叫)。另外,scala的中綴標註不僅可以在操作符中存在,也可以在模式匹配、型別宣告中存在,參見相應部分。
字首標註:字首操作符只有右邊一個運算元。但是對應的方法名應該在操作符字元上加上字首“unary_”。識別符號中能作為字首操作符用的只有+、-、!和~。
字尾標註:字尾操作符只有左邊一個運算元。任何不帶顯式引數的方法都可以作為字尾操作符。
在scala中,函式的定義方式除了作為物件成員函式的方法之外,還有內嵌在函式中的函式,函式字面量和函式值。
巢狀定義的函式:
巢狀定義的函式也叫本地函式,本地函式僅在包含它的程式碼塊中可見。
函式字面量:
在scala中,你不僅可以定義和呼叫函式,還可以把它們寫成匿名的字面量,也即函式字面量,並把它們作為值傳遞。函式字面量被編譯進類,並在執行期間例項化為函式值(任何函式值都是某個擴充套件了scala包的若干FunctionN特質之一的類的例項,如Function0是沒有引數的函式,Function1是有一個引數的函式等等。每一個FunctionN特質有一個apply方法用來呼叫函式)。因此函式字面量和值的區別在於函式字面量存在於原始碼中,而函式值作為物件存在於執行期。這個區別很像類(原始碼)和物件(執行期)之間的關係。
以下是對給定數執行加一操作的函式字面量:
(x: Int) => x + 1
其中,=>指出這個函式把左邊的東西轉變為右邊的東西。在=>右邊,你也可以使用{}來包含程式碼塊。
函式值是物件,因此你可以將其存入變數中,這些變數也是函式,你可以使用通常的括號函式呼叫寫法呼叫它們。如:
val fun = (x: Int) => x + 1
val a = fun(5)
有時,scala編譯器可以推斷出函式字面量的引數型別,因此你可以省略引數型別,然後你也可以省略引數外邊的括號。如:
(x) => x + 1
x => x + 1
如果想讓函式字面量更簡潔,可以把萬用字元“_”當作單個引數的佔位符。如果遇見編譯器無法識別引數型別時,在“_”之後加上引數型別宣告即可。如:
List(1,2,3,4,5).filter(_ > 3)
val fun = (_: Int) + (_: Int)
部分應用函式:
你還可以使用單個“_”替換整個引數列表。例如可以寫成:
List(1,2,3,4,5).foreach(println(_))
或者更好的方法是你還可以寫成:
List(1,2,3,4,5).foreach(println _)
以這種方式使用下劃線時,你就正在寫一個部分應用函式。部分應用函式是一種表示式,你不需要提供函式需要的所有引數,代之以僅提供部分,或不提供所需引數。如下先定義一個函式,然後建立一個部分應用函式,並保存於變數,然後該變數就可以作為函式使用:
def sum(a: Int, b: Int, c: Int) = a + b + c
val a = sum _
println(a(1,2,3))
實際發生的事情是這樣的:名為a的變數指向一個函式值物件,這個函式值是由scala編譯器依照部分應用函式表示式sum _,自動產生的類的一個例項。編譯器產生的類有一個apply方法帶有3個引數(之所以帶3個引數是因為sum _表示式缺少的引數數量為3),然後scala編譯器把表示式a(1,2,3)翻譯成對函式值的apply方法的呼叫。你可以使用這種方式把成員函式和本地函式轉換為函式值,進而在函式中使用它們。不過,你還可以通過提供某些但不是全部需要的引數表達一個部分應用函式。如下,此變數在使用的時候,可以僅提供一個引數:
val b = sum(1, _: Int, 3)
如果你正在寫一個省略所有引數的部分應用函式表示式,如println _或sum _,而且在程式碼的那個地方正需要一個函式,你就可以省略掉下劃線(不是需要函式的地方,你這樣寫,編譯器可能會把它當作一個函式呼叫,因為在scala中,呼叫無副作用的函式時,預設不加括號)。如下程式碼就是:
List(1,2,3,4,5).foreach(println)
閉包:
閉包是可以包含自由(未繫結到特定物件)變數的程式碼塊;這些變數不是在這個程式碼塊內或者任何全域性上下文中定義的,而是在定義程式碼塊的環境中定義(區域性變數)。比如說,在函式字面量中使用定義在其外的區域性變數,這就形成了一個閉包。如下程式碼foreach中就建立了一個閉包:
var sum = 0
List(1,2,3,4,5).foreach(x=> sum += x)
在scala中,閉包捕獲了變數本身,而不是變數的值。變數的變化在閉包中是可見的,反過來,若閉包改變對應變數的值,在外部也是可見的。
尾遞迴:
遞迴呼叫這個動作在最後的遞迴函式叫做尾遞迴。scala編譯器可以對尾遞迴做出重要優化,當其檢測到尾遞迴就用新值更新函式引數,然後把它替換成一個回到函式開頭的跳轉。
你可以使用開關“-g:notailcalls”關掉編譯器的尾遞迴優化。
別高興太早,scala裡尾遞迴優化的侷限性很大,因為jvm指令集使實現更加先進的尾遞迴形式變得困難。尾遞迴優化限定了函式必須在最後一個操作呼叫本身,而不是轉到某個“函式值”或什麼其他的中間函式的情況。
在scala中,你不要刻意迴避使用遞迴,相反,你應該儘量避免使用while和var配合實現的迴圈。
高階函式:
帶有其他函式作為引數的函式稱為高階函式。
柯里化:
柯里化(Currying)是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。如下就是一個柯里化之後的函式:
def curriedSum(x: Int)(y: Int) = x + y
這裡發生的事情是當你呼叫curriedSum時,實際上接連呼叫了兩個傳統函式。第一個呼叫的函式帶單個名為x的引數,並返回第二個函式的函式值;這個被返回的函式帶一個引數y,並返回最終計算結果。你可以使用部分應用函式表示式方式,來獲取第一個呼叫返回的函式,也即第二個函式,如下:
val onePlus = curriedSum(3)_
高階函式和柯里化配合使用可以提供靈活的抽象控制,更進一步,當函式只有一個引數時,在呼叫時,你可以使用花括號代替小括號,scala支援這種機制,其目的是讓客戶程式設計師寫出包圍在花括號內的函式字面量,從而讓函式呼叫感覺更像抽象控制,不過需要注意的是:花括號也就是塊表示式,因此你可以在其中填寫多個表示式,但是最後一個表示式的值作為該塊表示式的值並最終成為了函式引數。如果函式有兩個以上的引數,那麼你可以使用柯里化的方式來實現函式。
傳名引數:
對於如下程式碼,myAssert帶有一個函式引數,該引數變數的型別為不帶函式引數的函式型別:
myAssert(predicate: () => Boolean) = {
if(!predicate())
throw new AssertionError
}
在使用時,我們需要使用如下的語法:
myAssert(() => 5 > 3)
這樣很麻煩,我們可以使用如下稱之為“傳名引數”的語法簡化之:
myAssert(predicate: => Boolean) = {
if(!predicate)
throw new AssertionError
}
以上程式碼在定義引數型別時是以“=>”開頭而不是“() =>”,並在呼叫函式(通過函式型別的變數)時,不帶“()”。現在你就可以這樣使用了:
myAssert(5 > 3)
其中,“predicate: =>Boolean”說明predicate是函式型別,在使用時傳入的是函式字面量。注意與“predicate: Boolean”的不同,後者predicate是Boolean型別的(表示式)。
偏函式:
偏函式和部分應用函式是無關的。偏函式是隻對函式定義域的一個子集進行定義的函式。scala中用scala.PartialFunction[-T,+S]來表示。偏函式主要用於這樣一種場景:對某些值現在還無法給出具體的操作(即需求還不明朗),也有可能存在幾種處理方式(視乎具體的需求),我們可以先對需求明確的部分進行定義,以後可以再對定義域進行修改。PartialFunction中可以使用的方法如下:
isDefinedAt:判斷定義域是否包含指定的輸入。
orElse:補充對其他域的定義。
compose:組合其他函式形成一個新的函式,假設有兩個函式f和g,那麼表示式f _ compose g _則會形成一個f(g(x))形式的新函式。你可以使用該方法對定義域進行一定的偏移。
andThen:將兩個相關的偏函式串接起來,呼叫順序是先呼叫第一個函式,然後呼叫第二個,假設有兩個函式f和g,那麼表示式f _ andThen g _則會形成一個g(f(x))形式的新函式,剛好與compose相反。