Scala Tutorial(8)Tuple 簡介 by brianhsu | CodeData
top

Scala Tutorial(8)Tuple 簡介

分享:

Scala Tutorial(7)加強版的 Interface — Trait << 前情

讀者可能會發現,雖然本系列的標題是 Scala Tutorial,但一至到目前為止,除了 if / else 外,似乎都沒有提到多數程式語言在介紹文章時會提及的流程控制的語法。

這是因為在 Scala 中,內建的控制結構與法嚴格來說只有以下四種:

  • if / else
  • while / do while 迴圈
  • Pattern Matching
  • for 迴圈

而在這四項當中,就有兩項(Pattern Matching 與 for 迴圈)實際上會應用到物件導向部份的概念和 Scala 標準函式庫中內建的資料型別,所以若沒有先介紹物件導向的部份,反而會讓這些看似基礎的流程控制難以理解,我們之後會介紹到的 for 迴圈和 Pattern Matching 就是一個相當好的例子。

所以在我們開始介紹 Scala 的這些流程控制結構之前,必須要先介紹在 Scala 中常常使用的兩個資料型別--Tuple 與 Option。

Tuple 簡介

Tuple 是一種固定長度,但每個元素可以是不同型別的容器。舉例來說,在我們小時候學習整數的除法的時候,當我們遇到 13 / 5 這個運算式的時候,我們會說他的答案是 2 餘 5,而這個商數是 2,餘數是 3 的關係,就可以用一個 Tuple 來表示,其中第一個元素的值是 2,第二個元素的值是 3。

而在 Scala 中,要表示這樣的 Tuple 關係,可以用 (2, 3) 這樣子的方式來表示,這同時也代表了這個 Tuple 的長度是 2,且每個元素的型別都是 Int。

不過我們提過,Tuple 裡的元素可以是不同的型態,因此我們也可以使用 ("Hello", 0.5, 300, 'C') 這樣的寫法,來表示一個長度是四,第一個元素是字串,第二個元素是 Dobule,第三個元素是 Int,第四個元素是字元的 Tuple。

這樣的說明看似複雜,但其實如果從物件導向的角度去思考,就會發現 Scala 其實只是幫我們省略了一些東西而已,當我們寫 (2, 3) 這樣的敘述式時,其實 Scala 會執行的是 new Tuple2[Int, Int](2, 3),而當我們寫 ("Hello", 0.5, 300, 'C') 的時候,實際上 Scala 幫我們補成了 new Tuple4[String, Double, Int, Char]("Hello, 0.5, 300, 'C')

透過下面的程式碼範例,我們可以看到,其實兩種寫法是相等的,在 Scala 裡面,Tuple 不過就是一般的物件而已,只是 Scala 讓我們可以省略明確使用 new 關鍵字和指定類別名稱的步驟而已。

scala> val quadUsingTuple = ("Hello", 0.5, 300, 'C')
quadUsingTuple: (String, Double, Int, Char) = (Hello,0.5,300,C)

scala> val quadUsingNew = new Tuple4[String, Double, Int, Char]("Hello", 0.5, 300, 'C')
quadUsingNew: (String, Double, Int, Char) = (Hello,0.5,300,C)

scala> val isSameTuple = (quadUsingTuple == quadUsingNew)
isSameTuple: Boolean = true

scala> val usingExplictClassName: Tuple4[String, Double, Int, Char] = ("Hello", 0.5, 300, 'C')
usingExplictClassName: (String, Double, Int, Char) = (Hello,0.5,300,C)

此外,若 Tuple 只有兩個元素,除了用上述的語法外,我們可以使用 A -> B 這種語法來建立 Tuple:

scala> val tupleUsingParenthesis = (1, 3)
tupleUsingParenthesis: (Int, Int) = (1,3)

scala> val tupleUsingArrow = 1 -> 3
tupleUsingArrow: (Int, Int) = (1,3)

scala> tupleUsingParenthesis == tupleUsingArrow
res0: Boolean = true

那我們要怎麼存取 Tuple 裡面的值呢?很簡單,由於在 Scala 中 Tuple 也是物件,而 Tuple 物件的每一個元素都對應到一個成員變數:第一個元素的名稱叫 _1、第二個元素的名稱叫 _2……依此類推,因此我們可以用下面的方式來存取 Tuple 中的資料。

不過這裡要特別注意的,是 Scala 中的 Tuple 和字串一樣,是 immutable 的物件,當產生之後就不能再改變物件的狀態,因此我們無法改變 Tuple 內成員變數的值,否則會出現 reassignment to val 的編譯錯誤。

scala> val tuple = (2, 3)
tuple: (Int, Int) = (2,3)

scala> val quotient = tuple._1
quotient: Int = 2

scala> val remainder = tuple._2
remainder: Int = 3

scala> val quadruple = ("Hello", 0.5, 300, 'C')
quadruple: (String, Double, Int, Char) = (Hello,0.5,300,C)

scala> val myString = quadruple._1
myString: String = Hello

scala> val myChar = quadruple._4
myChar: Char = C

scala> tuple._1 = 10
<console>:8: error: reassignment to val
   tuple._1 = 10
            ^

雖然我們可以利用這種方式來存取 Tuple 中的資料,但這樣的作法對於寫程式的人來說不是非常友善--若要取出 Tuple 中四個元素,就需要撰寫四行程式碼,而且 Tuple 中使用位置編號做為成員變數名稱的方式,也讓我們很難去分別 Tuple 中每個元素代表什麼意義。

但很幸運地,Scala 提供了另一種取出 Tuple 中元素的方法,我們可以透過在等號的左邊放上 val 或 var 關鍵字(同樣地,用 val 的話賦值後的變數不能被更動,若用 var 則可以),以及相對應的 Tuple 型式的變數來取出 Tuple 的值,所以上述的程式碼可以改成下面這樣:

scala> val (quotient, remainder) = (2, 3)
quotient: Int = 2
remainder: Int = 3

scala> val (myString, myDobule, myInt, myChar) = ("Hello", 0.5, 300, 'C')
myString: String = Hello
myDobule: Double = 0.5
myInt: Int = 300
myChar: Char = C

scala> println(myString)
Hello

scala> println(quotient)
2

scala> // 使用 val 的話,該變數不能被重新賦值
scala> quotient = 3
<console>:8: error: reassignment to val
       quotient = 3
                ^

scala> var (q1, r1) = (2, 3)
q1: Int = 2
r1: Int = 3

scala> // 使用 var 關鍵字的變數則可以被重新賦值
scala> q1 = 10
q1: Int = 10

scala> // 但因為 q1 是另一個全新的變數,所以不會影響到其他東西
scala> quotient
res2: Int = 2

如果我們只需要 Tuple 中的某些欄位時,我們可以用底線 _ 來告訴 Scala 我們不需要那些欄位的值,所以不必把他指派給任何變數:

scala> val (_, remainder) = (2, 3)
remainder: Int = 3

scala> val (_, myDobule, myInt, _) = ("Hello", 0.5, 300, 'C')
myDobule: Double = 0.5
myInt: Int = 300

在這裡要提的是,雖然這樣的語法看起來看其他語言中的 Parallel Assignment 很像,但實際上這是之後我們會提到的 Pattern Matching 的其中一種使用方式,而且這樣的作法會先產出一個 Tuple 的物件後,依序取出 Tuple 中的每個元素後再指派給相對應的變數,所以雖然看似使用上相當便利,但實際上與單純的用兩行程式碼來賦值給變數相比來說,其 overhead 是相當大的,若是相當追求效能的程式的話,就不適合使用這種方式來對變數賦值。

另外,由於這實際上是 Pattern Matching 而不是 Parallel Assignment,所以也不能使用這個方法來賦值給已經存在的變數:

// test.scala
var x = 10
var y = 20
var (x, y) = (3, 5)

若我們執行上述的程式碼,那麼 Scala 編譯器會抱怨說我們已經定義過 x 和 y 兩個變數了:

brianhsu@BrianHsuDell ~ $ scala test.scala 
/home/brianhsu/test.scala:3: error: x  is already defined as variable x
var (x, y) = (3, 5)
     ^
/home/brianhsu/test.scala:3: error: y  is already defined as variable y
var (x, y) = (3, 5)
        ^
two errors found

在這裡要特別注意的是,若把這段程式碼貼到 Scala REPL 中會發現他可以正常執行,使得我們好像可以賦值給已經存在的變數,但千萬不要被騙了,會有這樣的情況,是因我們每次在 Scala 中輸入每一行程式碼的時候,都會產生新的 scope(換句話說,REPL 自動幫我們加上大話號 {} 了),所以上面的程式碼在 REPL 中執行時實際上是:

// 若把上述的程式貼到 REPL 中,實際上會變成下面這樣:
{
  var x = 10

  {
    var y = 20

    {
      var (x, y) = (3, 5)
    }

  }

}

這個時候,因為 Scala 內層的 block 的變數會把外層同名的變數 shadow 掉,所以看起來好像 x 和 y 重新被賦值了,但實際上這個程式碼裡總共有以下四個變數,只是我們無法存取到 x 和 y 而已。

  • val x = 10
  • val y = 20
  • val x’ = 3
  • val y’ = 5

使用 Tuple 做為函數回傳值的型別

前面講了那麼多,那麼究竟 Tuple 可以用在什麼地方呢?簡單的來說,他很適合用在當我們的函式需要傳回一組簡單的狀態的時候。例如之前提到的除法,若我們希望寫一個 Java 的函式,可以傳回整數除法的商和餘數的話,我們會需要自己定義一個類別,其中有兩個欄位分別代表商和餘數,然後再使用這個類別做為函數的回傳值:

class DivideAnswer {
  public final int quotient;
  public final int remainder;

  public DivideAnswer(int quotient, int remainder) {
    this.quotient = quotient;
    this.remainder = remainder;
  }

  public static DivideAnswer divide(int dividend, int divisor) {
    return new DivideAnswer(dividend / divisor, dividend % divisor);
  } 
}

但是有了 Tuple 之後,我們就可以直接回傳 Tuple,而不必刻意去定義這樣的類別,使得程式碼更簡潔清楚:

def divide(dividend: Int, divisor: Int) = {
  ((dividend / divisor), (dividend % divisor))
}

同樣的,Tuple 也很適合用在表示 Key 和 Value 這種成對的關係,事實上,若我們要在 Scala 中將特定的 Key / Value 加入 Map 這樣的資料結構時,傳入的就是一個 Tuple 物件,而非像 Java 中 HashMap 的 put 方法一樣,把 key 和 value 視為不同的參數。

scala> val zipCode = Map[Int, String]()
zipCode: scala.collection.immutable.Map[Int,String] = Map()

scala> // 還記得嗎?加號也是 method
scala> zipCode + ((221, "汐止"))
res0: scala.collection.immutable.Map[Int,String] = Map(221 -> 汐止)

scala> // 所以上面的程式實際上等於:
scala> zipCode.+( new Tuple2(221, "汐止"))
res1: scala.collection.immutable.Map[Int,String] = Map(221 -> 汐止)

scala> // 若 Tuple 只有兩個元素,我們也可以使用 -> 符號來建立:
scala> // 這樣的好處是 Tuple 的括號不會和呼叫函式的括號混淆
scala> zipCode + (115 -> "南港")
res2: scala.collection.immutable.Map[Int,String] = Map(115 -> 南港)

我到底該使用 Tuple 還是 case class?

可能有讀者已經注意到了,雖然我們說 Tuple 的好處是不用另外定義類別,但我們之前也提到過,在 Scala 中定義類別是很簡單的事,多數的時候都只要一行程式碼就可以了,所以上述整數除法的函式也可以改成下面這樣,同樣也多不了多少程式碼:

case class DivideAnswer(quotient: Int, remainder: Int)
def divide(dividend: Int, divisor: Int) = {
  DivideAnswer((dividend / divisor), (dividend % divisor))
}

那麼這兩種做法有什麼不同,我們又應該用哪一種呢?

答案是--實際上這兩種作法是相同的,若我們翻開 Scala 的 API 說明文件,就會看見實際上 Tuple2 這個資料型別,也不過就是 Scala 函式庫已經幫我們先定義好的 case class,唯一的差別是 Tuple 中成員函式的名稱是固定且不具意義的編號,而使用 case class 的話,我們可以替每個成員變數取較有意義的名稱,以提高程式碼的可讀性和可維護性。

也因為這樣的原因,所以通常來說會優先使用自行定義的 case class,但若有已經約定俗成的表達方式,例如當我們提到 (1024, 768) 是螢幕的解析度的資料時,前者必定是螢幕的寬而後者是高時,就不妨直接使用 Tuple 來表示。

小結

這次我們介紹了如何在 Scala 中使用 Tuple 來表示一組簡單的資料型別,以及 Scala 是如何實現 Tuple 這樣的資料結構和他的用處。雖然在本篇中還未提到,但其實 Tuple 在 Scala 的控制結構中常常被使用到。

另一個 Scala 常常與控制結構一起出現的資料叫做 Option[T],他可以用來取代使用 null 來表示函式可能沒有回傳值的方法,我們下一篇將會介紹這個資料型別,以及他比起使用 null 來說有什麼好處。

後續 >> Scala Tutorial(9)Option[T] 簡介

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

熱門論壇文章

熱門技術文章