List 與 Tuple


scala.List 是有序的物件群集(Collection),與 陣列(或Java的List)不同的是,在Scala中,List是不可變動的(Immutable),這表示你不可以改變物件的內容或狀態,就如同 字串 一樣。

Scala鼓勵你使用或設計不可變動的物件,因為不可變動物件不會有複雜的狀態變化,你不用擔心物件在傳遞的過程中,不小心被改變了狀態,在多執行緒環境下,也不用擔心共用存取的競速(Race condition)問題。

在Scala中要建立List,必須使用工廠方法:
val list = List(10, 20, 30)
for(i <- 0 until list.length) {
println(list(i))
}

上例中,建立了一個List,內含元素10、20、30,要取得元素同樣是指定索引,索引可使用()的方式來指定,就如同陣列索引的指定方式,List是不可變動的,所以你無法使用list(0) = 20這樣的方式來改變List的元素內容。

List是抽象類別,你沒辦法直接用來建立實例,如果你要建立一個List物件,沒有任何的元素的話,可以使用List()或者是寫下Nil(Nil是List[Nothing]的實例)。List的==被定義為可以比較兩個List的元素是否相同,以下是個範例:
val list1 = List(10, 20, 30)
val list2 = List(10, 20, 30)
val list3 = List(20, 30, 40)
println(list1 == list2) // 顯示 true
println(list1 == list3) // 顯示 false
println(List() == Nil) // 顯示 true

如果要在List中附加元素,可以使用::方法,不過要注意,::是將元素附加至List的前端,由於List是不可變動的,所以傳回的是新建立的List物件,例如:
val list1 = List(10, 20, 30)
(5 :: list1).foreach(println) // 逐行顯示 5、10、20、30

val list2 = 1 :: 2:: 3:: 4 :: Nil
list2.foreach(println) // 逐行顯示 1、2、3、4

上 例中,還示範了如何從空List逐步附加元素。Scala沒有提供將元素附加至List後端的方法(其實本來是有+方法,不過已經被標示為 deprecation,也就是廢棄不建議使用),將元素附加至List前端很快速,只需要常數時間(Constant time),但將元素附加至List後端,則會隨著List的長度而增加線性時間。

操作順序與關聯 中有提過,如果運算方法最後一個字元是:,其實該方法是屬於右邊物件,所以5 :: list1,其實是list1.::(5),例如:
val list1 = List(10, 20, 30)
list1.::(5).foreach(println) // 逐行顯示 5、10、20、30

如果你想串接兩個List,則可以使用:::方法,例如:
val list1 = List(10, 20, 30)
val list2 = List(40, 50, 60)
(list1 ::: list2).foreach(println) // 逐行顯示 10、20、30、40、50、60
list2.:::(list1).foreach(println) // 逐行顯示 10、20、30、40、50、60

:::同樣是以:結尾,所以是由右邊物件呼叫的方法,在上面的範例也有示範過了。如果你要將一個List附加至另一個List之後,其實也可以使用++方法,例如:
val list1 = List(10, 20, 30)
val list2 = List(40, 50, 60)
(list1 ++ list2).foreach(println) // 逐行顯示 10、20、30、40、50、60

使用++似乎與使用:::的作用相同,不過有所不同的是,++是由左邊物件呼叫的方法,而++右邊的物件可以是 scala.Iterable 物件,而不用得是List,例如,你可以將陣列的元素內容附加至List之後:
val list = List(10, 20, 30)
val arr = Array(40, 50, 60)
(list ++ arr).foreach(println) // 逐行顯示 10、20、30、40、50、60

如果你忽略:結尾的方法其實是由右邊物件呼叫,那麼有可能會發生像以下的錯誤:
val list = List(10, 20, 30)
val arr = Array(40, 50, 60)
val result = (list ::: arr) // 這是錯的

執行時會顯示的錯誤訊息如下,指出了:::並非陣列可呼叫的方法:
...: error: value ::: is not a member of Array[Int]
val result = (list ::: arr)
                             ^

你可以使用-從List中刪去指定的元素,注意所有符合的元素都會被刪掉:
val list = List(10, 10, 20, 20, 30, 30)
(list - 20).foreach(println) // 逐行顯示 10、10、30、30

使用--的話,則可以刪去另一個List物件中所擁有的元素,例如:
val list1 = List(10, 20, 30, 40, 50)
val list2 = List(20, 30)
(list1 -- list2).foreach(println) // 逐行顯示 10、40、50

List中的元素必須是相同的類型,如果你想要在某個群集物件中放置不同型態的物件,而且又要群集物件記得每個元素的型態,則可以使用Tuple(List中如果放置不同型態的物件,則List會全部視為 scala.Any,這是Scala所有類別的最頂層父類別,每個物件的型態資訊基本上就失去了,你必須自己記得放了什麼)。Tuple同樣是不可變動的。一個例子如下所示:
val tuple = (10, "Justin", true)
println(tuple._1) // 顯示 10
println(tuple._2) // 顯示 Justin
println(tuple._3) // 顯示 true

注意到,存取元素時是使用_n的方式,而n是從1開始(這是來自其它有靜態型別tuple特性的語言傳統,如Haskell),由於Tuple中每個元素的型態資訊不同,因此無法僅靠像List或陣列的索引存取方式來記得型態資訊,因而給予每個元素個別的_n名稱。

在建立Tuple時,可以直接在()中指定元素,不過要注意的是,如果你的Tuple中只有一個元素,則必須使用Tuple()來定義,例如:
val tuple = Tuple(1)

上例中,Tuple其實是 scala.Predef 中定義的名稱,如果你沒有使用上例的方式,而是直接用(1),則會被視為定義一個整數,而非Tuple。Tuple的實際型態是根據它所包括的元素個數來決定(包括參數化類型資訊),例如:
val tuple1 = (10, "Justin")
val tuple2 = (10, "Justin", true)
println(tuple1.getClass) // 顯示 class scala.Tuple2
println(tuple2.getClass) // 顯示 class scala.Tuple3

上例中,tuple1的參數化類型為Tuple2[Int, String],tuple2則為Tuple3[Int, String, Boolean]。Tuple最大的用途之一,就是可以同時指定多個變數,例如:
val (id, name) = (123, "Justin")
println(id) // 顯示 123
println(name) // 顯示 Justin

上例中,實際上宣告了兩個變數id與name,並被分別指定Tuple物件中對應的值。當你要從函式傳回兩個以上的物件時,這個特性就很有用:
def info(data: String) = {
// 作一些事情
val id = 123 // 取得 id 了
val name = "Justin" // 取得名稱了
(id, name)
}

val (id, name) = info("一些輸入資料")
println(id) // 顯示 123
println(name) // 顯示 Justin