讀《快學Scala 》一書的摘要
Scala 運行于JVM之上,擁有海量類庫和工具,兼顧函數式編程和面向對象。
在Scala中, 解釋器就是我們喜歡的REPL,變量或者函數的類型總是寫在變量或函數的后面(與java相反),數值類型的轉換通過方法而不是強制類型轉換,僅當同一行代碼存在多條語句時才需要用分號隔開。
scala 允許自定義操作符,注意有分寸地使用,在使用scala.開頭的包時,可以省去scala前綴。scala沒有靜態方法,類似的特性可以用單例對象,一個類對應的companion object就跟Java中的靜態方法一樣,使用companion object的apply方法是scala中構建對象的常用方法。
控制
在scala中,判斷語句跟其他語言類似,但{}塊包含一系列表達式,其結果也是一個表達式,塊中最后一個表達式的值就是塊的值。
在for 循環的變量之前并沒有val或var的指定。該變量的類型是集合的元素類型,循環變量的作用域一直持續到循環結束。scala并沒有提供break或continue語句來退出循環,替代方法如下:
- 使用Boolean的控制變量
- 使用嵌套函數——在函數中return
- 使用Breaks對象中的break方法,控制權的轉移是通過拋出和捕獲異常完成的,出于性能考慮,盡量避免這一機制。
for有豐富的形態,可以提供多個生成器,并帶有if開頭的表達式,還可以使用任意多的循環變量定義。
函數
scala中的方法對對象進行操作,而函數不是,C++中也有函數,不過在Java中智能用靜態方法模擬,scala的函數不需要return,對應遞歸函數必須指明返回類型,可以混用未命名參數和帶名參數,只要那些未命名的參數是排在前面的即可。
同樣,變長參數列表很方便,通過 :_* 將值序列轉換成參數序列。當變長參數類型為Object的Java方法時,需要手工對基本類型進行轉換。
scala對不返回值的函數有特殊的表示法,如果函數體包含在花括號當中但沒有前面的=號,那么返回類型就是Unit,這樣的函數也叫過程函數。
當val 被聲明為lazy時,它的初始化將被延遲加載,直到對它初次取值。每次訪問延遲加載的變量,都會有一個方法被調用,以線程安全的方式檢查該值是否已被初始化。
異常
scala異常的工作機制和Java或C++一樣,但沒有"受檢"異常,捕獲異常采用模式匹配的語法,不需要使用捕獲的異常對象,可以用_來替代變量名。try/catch和try/finally是互補的。
數組和映射
Scala中的Array是定長數組,ArrayBuffer是變長數組,對應于Java中的ArrayList,C++中的Vector,可以用相同的代碼處理這兩種數據結構,用 for (i<-區間 )來遍歷,
用for(...) yield
創建一個類型與原始集合相同的新集合,還可以通過if 在進行條件過濾。Scala中的內建函數sum,sorted,max,min,quicksork提供了常用算法。由于Scala數組是用java數組實現的,可以在java和scala之間傳遞,只需引入scala.collection.JavaConversions里的隱式轉換方法。
scala中,映射是對偶的集合,可以看做將鍵映射到值的函數,區別在于函數一般用于計算,而映射只做查詢。用=可以直接增加映射,也可用+=添加多個關系,用for((k,v)<-映射) 來遍歷映射,使用scala.collection.JavaConversions.mapAsScalaMap將Java中的map轉換為scala中的映射。
scala中,元組是不同類型的值的聚集,()構成元組,用方法1,2...訪問其組元,而通常使用模式匹配來獲取元組的組元。使用元組的原因之一是把多個值綁在一起,以便它們能夠被一起處理,通常用zip方法開完成,使用toMap方法將對偶的集合轉換成映射。
類與對象
在scala中,類并不聲明為public,源文件可以包含多個類,所有這些類都具有共有可見性。對每個字段都提供了getter和setter方法,分別叫做 字段名 和 字段名_,可重新自定義。
注意:
1)如果字段私有,則getter和setter也是私有的
2)如果字段val,則只有getter方法
3)如果不需任何getter和setter,可將字段聲明為private[this]
將scala字段標注為@BeanProperty時,會產生Java屬性的定義方法getxxx和setxxx。
scala中的類有一個主構造器primary constructor,可有任意多個輔助構造器 auxiliary constructor。輔助構造器與其他語言的區別在于: 1)名稱為this 2)必須以一個先前已定義的其他輔助構造器或主構造器的調用開始。而主構造器的參數直接放置在類名之后,會執行類定義中的所有語句,無參主構造器僅僅執行類體中的所有語句而已。如果將主構造器定義為私有的,則必須通過輔助構造器來構造對象了。在內嵌類中,可以通過外部類.this來訪問外部類的this 引用。
對象本質上擁有類的所有特質,只不能提供構造器參數。所有使用單例對象的地方,scala中都可以用對象來實現。
1)作為存放工具函數或常量的地方
2)高效共享單個不可變實例
3)需要用單個實例來協調某個服務時
對Java中既有實例方法又有靜態方法的類,scala通過類及與類同名的伴生對象來實現。類和它的伴生對象可以相互訪問私有特性,必須存在于同一源文件中。
每個scala程序都必須從一個對象的main方法開始,也可以擴展App特質。scala中沒有枚舉類型,但標準類庫提供了一個Enumeration助手類,用于產生枚舉。
包
Scala中的包與java包或c++命名空間的目的相同,但可以在同一文件中為多個包貢獻內容。
盡量使用完整包名,避免使用scala,java,com,org等來命名嵌套的包。串聯式包語句可以限定可見的包。
包可以包含類,對象和屬性,但不能包含函數和變量的定義,在實現上,包對象被編譯成帶有靜態方法和字段的JvM類。通過修飾符同樣可以達到public,private或protected的效果。
在scala中, 任何地方都可以聲明引入包,這一點和python很相似。通過選取器可以引入包中的指定成員,還可以對指定成員重命名或者隱藏java.lang,scala,predef 總是被默認引入的。
繼承
scala擴展類的方式同樣是使用extends關鍵字,重寫一個非抽象方法必須使用override修飾符,用isInstanceOf方法判斷某個對象是否屬于某個特定的類,只有主構造器可以調用超類的構造器。
字段重寫時的限制:
- def 只能重寫另一個def
- 只能重寫另一個val或不帶參數的def
- var只能重寫另一個抽象的var
構造順序問題的根本原因——java允許在超類的構造方法中調用子類的方法。因為在子類中正確的擴展相等性判斷非常困難,所以將equals方法定義成final。除非萬不得已,不要使用wait,notify和synchronized。
和java的接口不同,scala特質可以給出這些特質的缺省實現。讓特質擁有具體行為存在一個弊端,當特質改變時,所有混入了該特質的類必須要重新編譯。scala不支持多繼承,可以用with關鍵字來添加額外的特質。當做富接口使用的特質將具體方法和抽象方法結合在了一起,特質中的字段同樣既可以是具體的,又可以是抽象的。
混入特質的對象在構造時的執行順序:
- 首先調用超類的構造器
- 特質構造器在超類構造器之后,類構造器之前執行
- 特質由左到右構造
- 每個特質中,父特質先被構造,
- 如果多個特質有一個父特質,若已被構造則不會再次構造
- 所有特質構造完畢,子類被構造。
缺少構造器參數是特質與類之間唯一的技術差別。
文件訪問
scala.io.source對象的getlines方法可以讀取文件的所有行,可以把source對象當成迭代器讀取文件中的每個字符,java.util.Scanner來處理同事包含文本和數字的文件。
從URL中讀取時,需要事先知道編碼格式,scala中沒有提供讀取二進制文件的方法,需要使用Java類庫,同樣沒有內建的對寫入文件的支持,可使用java.io.PrintWriter,訪問目錄也要用java的方法,例如java.nio.file.Files類中的walkFileTree。
scala集合類都是可序列化的。
scala.sys.process包提供了用于與shell程序交互的工具,包含了一個從字符串到processbuilder對象的隱式轉換,!操作符就是執行這個processbuidler的對象。scala.util.matching.Regex 利用正則表達式對字符串進行分析。
操作符與解析器
變量、函數、類等的名稱統稱為標識符,反引號中可以包含幾乎任何字符序列。
在scala中,除了
- 以冒號:結尾的操作符
- 賦值操作符
所有操作符都是左結合的。
unapply方法接受一個對象,然后從中取值,通常是當初用來構造該對象的值。要取任意長度的值的序列,一般用unapplySeq命名方法。
Scala解析器庫是scala語言總內嵌領域特定語言(DSL)的高級示例。為了使用Scala解析庫,需提供一個擴展自Parsers特質的類并定義那些由基本操作組合起來的解析操作,包括:
- 匹配一個詞法單元
- 在兩個操作之間做選擇(|)
- 依次執行兩個操作(~)
- 重復一個操作(rep)
- 可選擇地執行一個操作(opt)
組合子返回的是樣例類的實例而不是對偶,這樣更方便模式匹配。要生成解析樹,需用^^操作符,給出產生樹節點的函數,避免左遞歸和回溯。
StandardTokenParsers類提供了一個產出這些詞法單元的解析器。
集合
Scala中所有集合都是iterable的,seq是有先后次序的序列(如數組和列表),Set是沒有先后次序的序列,map是一種鍵值對偶。Scala優先采用不可變集合,::操作符從給定的頭和尾創建一個新的列表。如果要把列表中的某個節點變成列表中的最后一個節點,不能將next引用設為nil,而應該設為LinkedList.empty.
已排序的集使用紅黑樹實現的,scala2.9沒有可變的已排序集,要用到java.util.TreeSet
Scala 關于添加和移除的操作符:
- 向后
(:+)
或向前(+:)
追加元素到序列中 - 添加
(+)
元素到無先后次序的集合中 - 用
-
移除元素 - 用
++
和--
批量添加和移除元素 - 對于列表,優先使用
::
和:::
- 改值操作有
+=
,++=
,-=
和--=
對于集合,推薦++,&和--,盡量不用++:,+=:和++=: 操作方式。
初始值和操作符是兩個分開定義的柯里化參數,這樣scala就能用初始值類型來推斷操作符的類型定義。任何while循環都可以用折疊來替代,對于那些完整構造需要很大開銷的集合而言,迭代器作用大,而流將緩存訪問過的行,允許你重新訪問他們。
對于數組,緩存,哈希表,平衡樹而言,基于par方法的并行實現很高效。
模式匹配
與switch語句不同,scala模式匹配沒有break的問題。如果case中的判斷不能匹配,則捕獲所有的模式來嘗試匹配。變量模式可能與常量表達式沖突,變量必須以小寫字母開頭。如果有一個小寫字母開頭的常量,則需要把它抱在反引號中。
在類型匹配的時候,必須給出一個變量名,否則會拿對象本身來進行匹配。由于匹配發生在運行時,Jvm中泛型的類型信息是被擦掉的,所有不能用類型來匹配特定的Map類型。正則表達式是適合使用提取器的場景。
樣例類是一種特殊的類,經過優化以被用于模式匹配,其實例使用(),樣例對象不使用圓括號。中置表示法可用于任何返回對偶的unapply方法。樣例類的特點:
- 模式匹配的代碼更精簡
- 構造時不需new
- 可以免費得到toString,equals,hashCode 和copy方法
讓所有樣例類都擴展某個密封的類或特質是個好做法。被包在花括號內的一組case語句是一個偏函數,偏函數表達式必須位于編譯器可以推斷返回類型的上下文中。
注解
注解可以在程序的各個條目中添加信息,是插入到代碼中以便有工具可以對他們進行處理的標簽。可以對是scala類使用java注解,也可以使用scala特有的注解。
在scala中,可為類,方法,字段,局部變量和參數添加注解。Java注解的參數類型只能是:
- 數值型變量
- 字符串
- 類變量
- java枚舉
- 其他注解
- 上述類型的數組。
如果要實現一個新的Java注解,則需要用Java來編寫該注解類。scala用@clonable和@remote來標記可被克隆的和遠程的對象。@varargs注解可以從Java調用Scala的帶有變長參數的方法。
Scala類庫中的有些注解可以控制編譯器的優化,@tailrec 用于消除遞歸,@switch 注解可以檢查scala的match語句是否真的被編譯成了跳轉表,用@inline來建議編譯器做內聯,@editable給那些可以在生產代碼中移除的方法打上標記,對被省略的方法的調用,編譯器會替換成Unit對象,@uncheckVariance會取消與型變相關的錯誤提示。
xml處理
Scala提供了對xml(當然也就支持html了)的內建支持,可以用scala.xml.Elem的值表示一個XML元素。Node類是所有xml節點類型的父類,Elem類描述xml元素。要處理某個元素的屬性鍵和值,可以用attributes屬性,然后用()來訪問定鍵的值 ,使用循環或asAttrMap方法遍歷所有屬性。
內嵌的字符串會被轉成Atom[String]節點,所以可在xml中包含scala代碼,被內嵌的scala代碼還可以繼續包含XML片段,被引用的字符串當中的花括號不會被解析和求值。
NodeSeq提供了類似xpath中/,//的操作符,scala中用,\ 替代,可以在模式匹配中使用xml的關鍵字。由于scala中xml節點和節點序列是不可變的,若要修改一個節點,需創建拷貝,給出修改,在拷貝未修改的部分。RuleTransformer類的transform方法遍歷給定節點的所有后代,應用所有規則,最后返回經過變換的樹。
Scala中的ContructingParser是個解析器,用于加載xml,可以保留注釋,CDATA和空白,用doc.dtd可以訪問到DTD。
保存XML時,沒有內容的元素不會被寫成自結束的標簽。Scala中每個元素都有一個scope屬性,類型為NamespaceBinding,該類的Uri屬性輸出命名空間的URI。
高級函數和高級類型
在scala中,函數是頭等公民,可以用變量存儲函數,可以使用匿名函數,和帶參數的函數。如果需要一個序列的值,一般從一個簡單序列轉化得出。函數可以在變量不再作用域內時被調用,這樣的函數叫閉包。
柯里化是指將原來接受兩個參數變成接受一個參數的函數的過程。不需要用return語句來返回函數值,函數的返回值就是函數體的值。
scala中,用方括號來定義類型參數,從調用該方法的實際參數來推斷出類型。視圖界定 T<%V要求必須存在一個從T到V的隱式轉換,Manifest對象是構造器的隱式參數,可用于上下文界定,類型變化的方向和子類型方向是相反的。
函數在參數上是逆變的,在返回值上的協變的,對象是不能泛型化的。
在內部,編譯器將所有嵌套的類型表達式a.b.c.T都翻譯成類型投影a.b.c.type#T。對應復雜類型,可用type關鍵字創建一個簡單的別名,type同樣被用于那些在子類中被具體化的抽象類型。
結構類型指的是一組關于抽象方法,字段和類型的規格說明,可用安全而方便的反射調用。
在scala中,通過特質和自身類型達到一個簡單的依賴注入效果。如果類型是在類實例化時給出,則使用泛型,如果類型是在子類中給出,則使用抽象類型。
List這樣的泛型類型有時稱為類型構造器。Container特質是scala集合類庫中使用的構建器機制的的簡化版。
Actor
actor提供了并發程序中與傳統的基于鎖的結構不同的另一種選擇,通過盡可能避免鎖和共享狀態,actor更容易地設計出正確、沒有死鎖或爭用狀況的程序。Scala提供了actor的簡單實現,akka(http://akka.io)提供了高級actor類庫。
每個actor都要擴展Actor類并重寫Act方法,actor是處理異步消息的對象,消息可以是任何對象,通過!操作符發送消息,例如:
actorX !“happy new year”
一個好的方式是使用樣例類作為消息,這樣,actor可以使用模式匹配了。發送的消息存放在mailbox,receive方法從mailbox中取下一條消息并處理,如果在receive方法被調用時并沒消息,則該調用會阻塞,直到有消息抵達。actor可以安全地修改它自己的數據。
向其他actor發送消息的方法:
- 使用全局的actor
- actor可以構造成帶有指向一個或更多actor的引用
- actor可接收帶有指向另一個actor的引用的消息
- actor可以返回消息給發送方
actor可以發送一個消息并等待回復,用!?操作符即可,盡量避免同步消息。
actor的act方法在actor的start方法被調用時開始執行。接下來進入某個循環,終止條件如下:
- act方法返回
- act方法由于異常被終止
- actor調用exit方法
通過link方法可以將不同的actor鏈接在一起。
actor的設計原則如下:
- 避免使用共享狀態
- 不要調用actor的方法
- 保持每個actor簡單
- 上下文數據包含在消息中
- 最小化給發送方回復
- 最少阻塞調用
- 使用react
- 建立失敗區