Scala Tutorial(6)Scala 物件導向基礎之二 by brianhsu | CodeData
top

Scala Tutorial(6)Scala 物件導向基礎之二

分享:

Scala Tutorial(5)Scala 物件導向基礎之一 << 前情

我們之前看到了 Scala 在物件導向方面,提供了比 Java 更簡潔的語法來定義類別,並且提供了在程式語言層面所提供的 Singleton 物件,但 Scala 所提供的物件導向功能不只如此,而且在語義也許 Java 有一點點不同。如果想要了解 Scala 的特性,以便在寫 Scala 的程式碼時做出正確的設計決策的話,那麼知道 Scala 提供的這些語法是如何轉到 Java VM 上,將會很有幫助。

建構子中的參數詳解

在 Scala 當中可以在宣告類別的 primary constructor 時宣告該類別的成員變數,像下面的程式碼一樣:

class Person(name: String, age: Int)

我們說過上面這段程式碼會產生兩個成員變數,但實際上這是簡化過後的說法。Scala 的編譯器是相當聰明的,當他看到這段程式碼的時候,他會知道在 Person 這個類別當中 nameage 變數除了在建構子外都沒有被其他地方用到,既然如此,為何要浪費記憶體空間來存這些變數呢?所以這段程式碼產生出來的結果,這兩個變數都只會是建構子的參數,而不會是成員變數。

我們可以透過 scala -print <檔案名稱.scala> 來驗證:

[[syntax trees at end of                   cleanup]] // test.scala
package <empty> {
  class Person extends Object {
    def <init>(name: String, age: Int): Person = {
      Person.super.<init>();
      ()
    }
  }
}

同樣的,如果我們將程式碼新增一個 printAge 函式來印出 age 變數,並且在 Person 類別內印出 name 變數。在這個狀況下,我們知道 name 只有在物件被初始化的時候在建構子印出,所以沒必要設為成員變數,但 age 變數則需要。

class Person(name: String, age: Int) {
  def printAge {
    println("Age:" + age)
  }
  println("Name:" + name)
}

當我們針對上述的程式碼用 -print 參數進行檢查時,會發現與之前的版本不同,在 Person 這個類別當中出現了一個 private[this] val age 的變數,說明 age 已經被當成成員變數了。

但另一方面,如果我們在宣告 primary constructor 的參數列表的變數時加上了 val 或 var 前綴時,由於這些變數會變得可以透過外介存取,所以一定會成為成員變數,例如下面的範例程式碼:

class Person(val name: String, var age: Int)

但故事並沒有那麼簡單,雖然說在這個情況下,看起來像是我們在 Person 這個類別裡宣告了一個 public 的 val name 成員變數和 var name 成員變數(在 Scala 中沒有加任何修飾字的話就是 public):

clss Person {
  val name: String = "" // 實際的內容會在建構子中指定
  var age: Int = _
}

但實際上並不是這樣的,當我們在主建構子的參數列表的參數加了 val 變數時,除了宣告成員變數外,還會宣告成員變數的 Getter。同樣的,當我們加上 var 關鍵字時,除了成員變數、Getter 外,還會加上 Setter,所以實際上編譯出來的程式碼會接近下面這樣:

class Person {
  private val mName: String = ""
  private var mAge: Int = 0

  def name() = this.mName
  def age = this.mAge

  def age_= (x: Int): Unit = { this.mAge = x }
}

在這邊我們可以看到幾個之前在介紹定義 method 時沒有講到的細節:

  1. 在 Scala 中函式可以沒有參數列表,或參數列表是空的,在這種情況下我們可以像使用變數一樣的方式取得其返回值。
  2. 當使用 person.age = 10 這種語法指定值時,我們可以透過 age_= 這種函式的命名來覆寫其行為,例如在其中檢查傳入的值是否合法。

包山包海的 Case Class

看完了簡單的類別定義後,現在來看看 Scala 提供的超方便的 Case Class 吧!在 Scala 中要定義 Case Class 相當的簡單,只要在 class 關鍵字前加上 class 就行了,當加上了 case 的關鍵字時,Scala 會幫我們自動實作一些常用的功能。

首先第一個就是當我們使用 case class 時,在主建構子中參數列表中的參數都會預設是 val 變數,可以被外界存取。(當然也可以自己加上 private 等修飾字,或是加上 var 成為 var 變數)

case class Person(name: String, age: Int)

val person = new Person("Joe", 13)
println(person.age)

另一個就是自動產生有意義的 toString 函式,當我們使用一般的方式宣告類別時,toString 只會印出類別的名稱。但如果使用 case class 來宣告的話,則會印出該類別的主建構子中的參數列表宣告的成員變數的內容。

scala> class PersonA(name: String, age: Int)
defined class PersonA

scala> val joe = new PersonA("Joe", 13)
joe: PersonA = PersonA@4be408f0

scala> case class Person(name: String, age: Int)
defined class Person

scala> val david = new Person("David", 15)
david: Person = Person(David,15)

此外,它也會自動建立一個對應名稱的 Singleton 物件,並且實作我們之前提到的 apply 方法,其參數列表就是主建構子的參數列表,所以當我們在建立物件時,除了使用 new 關鍵字外,也可以省略 new 關鍵字,使用 apply 方法來建立:

scala> val joe = Person("Joe", 13)
joe: Person = Person(Joe,13)

scala> val david = Person.apply("David", 13)
david: Person = Person(David,13)

另一個相當重要的功能是他會自動實作 equals() 和 hashCode() 方法,所以我們可以放心的將其放入各種 Java 或 Scala 的容器中,也可以直接使用 == 運算子來比較兩個不同的物件是否相等。(在 Scala 中的 == 其語義與 Java 中的 equals 方法相同,檢查的是物件的內容是否相同,而不是兩者是同一個物件)

舉例而言,我們可以用 new 關鍵字建立兩個不同的物件,但含有相同的內容,這個時候用 hashCode 或 == 來檢查時,會發現兩者的內容是相同的:

scala> val david1 = new Person ("David", 13)
david1: Person = Person(David,13)

scala> val david2 = new Person ("David", 13)
david2: Person = Person(David,13)

scala> david1.hashCode
res8: Int = 254294983

scala> david2.hashCode
res9: Int = 254294983

// 檢查兩個變數指到的物件的內容是否相同,語意與 Java 中的 equals 函式相同
scala> david1 == david2 
res11: Boolean = true

// 檢查兩個變數是否 reference 到同一個物件,語意和 Java 運作在 reference 變數上的 == 同同
scala> david1 eq david2 
res12: Boolean = false

在這邊要特別提到的,是 case class 的 hashCode 函式是依據主建構子中的成員變數的內容決定的,所以如果我們將某個成員參數設成 var 變數的話,hashCode 很有可能會因為物件的狀態被改變而更動,如果在是在 hashCode 的一致性很重要的情境下,請務必記得這一點,並且避免將主建構子中的參數設成 var 變數。

scala> case class Person(name: String, var age: Int)
defined class Person

scala> val person = new Person("Joe", 13)
person: Person = Person(Joe,13)

scala> person.hashCode
res0: Int = -87682959

scala> person.age = 15
person.age: Int = 15

scala> person.hashCode
res1: Int = 710749424

除了上述提到的功能外,Case Class 還提供了一個叫做 copy 的函式,這個函式可以很方便地幫我們產生另一個物件,而其內容是建構在原本的物件上,但可以透過參數微調。

case class Person(name: String, age: Int)

val user1 = new Person("Joe", 13)
val user2 = user1.copy(age = 14)
val user3 = user1.copy(name = "Billy", age = 30)

println(user1)  // user1 的物件狀態不會被改變
println(user2)  // user2 是新建的物件,name 一樣但 age 被變動
println(user3)  // 當然要把所有參數全部砍掉重練也是可以的

最後,case class 還實作了 unapply 這個函式,不過這個函式通常只有在使用 Scala 的 Pattern Matching 功能時會被 Scala 呼叫,我們將會在介紹 Pattern Matching 時回頭來介紹,在這邊就暫且按下不表。

小結

在這一篇中,我們看到了 Scala 中的類別的主建構子的參數是如何運作的,以及 Scala 中所提供的 case class 如何進一步幫我簡化一些常見的工作。

但另一方面,我們一直都沒有提到與 Java 中的 Interface 相對應的東西。這是因為在 Scala 中提供的對應的 Trait 相較 Java 的 Interface 來說更為強大,舉例而言,在 Scala 中的 Trait 中的函式是可以有實作的,我們將會在下一篇中來詳細看看 Trait 要如何達成 Java 中 Interface 提供的功能,以及他有什麼其他神奇的地方。

後續 >> Scala Tutorial(7)加強版的 Interface — Trait

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

相關文章

留言

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

熱門論壇文章

熱門技術文章