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

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

分享:

Scala Tutorial(4)實戰 Higher-Order Function << 前情

除了 Functional Programming 的編程典範外,Scala 也支援了物件導向的功能,其語法與 Java 類似,但修正並簡化了 Java 中的一些較為繁雜的地方。由於 Scala 的標準函式庫是以物件導向為基礎,並以 Functional Programming 為輔的形式來呈現,因此在進入到如何使用 Scala 函式庫中提供的各式功能前,我們有必要了解 Scala 在物件導向方面與 Java 有所出入的地方。

更簡潔的 Class

在定義類別方面,Scala 的語法與 Java 類似,但有一些不同--首先,在 Scala 中若 class 與 method 沒有給定存取範圍的修飾字(如 private / protected)等,則其預設為 public 範圍,這樣的設計可以讓我們的程式碼更為簡潔。

此外,和 Java 不同,Scala 的建構子並不是同名函式,而是由兩個部份所組成--該類別的建構子參數列表,以及該類別的內容。

舉例而言,在下面的 ClassA 中,它的建構子是直接寫在類別的定義中的,這和 Java 相當不一樣。

class ClassA {
  val x = 10

  println("Init classA first print")

  def hello() {
    println("hello from ClassA")
  }

  println("Init classA second print")
}

val objA = new ClassA
objA.hello()

在這個例子中,我們可以看到整個 ClassA 裡扣掉定義成員變數與成員函式後的程式碼會依序被執行,但這邊要強調的是這並不是好的 Scala 程式碼風格,在 Scala 的程式碼中,我們通常會盡量讓類別中只有成員函式和變數的定義,若有其他需要額外處理的邏輯,通常會使用 Factory Pattern 來達成。

Init classA first print
Init classA second print
hello from ClassA

除了整個類別就是建構子外,Scala 另一個與 Java 相當不同的地方,就在於其 new 一個物件時的建構子,是與類別的成員變數緊密結合的,當我們把參數列表宣告在類別名稱的後面時,參數列表中的變數會自動成為該類別的成員變數。

class Person(userID: Int, name: String)

val person = new Person(102, "John Smith")

// 編譯錯誤:建構子中預設的成員變數是 private
// println(person.name)

舉例而言,上程式碼中的第一行是完全合法的 Scala 類別宣告(在 Scala 中若一個類別不需要定義成員時,可以完全省略大括號),其對應到以下的 Java 程式碼:

public Class Person {
    private final int userID;
    private final String name;

    public Person(int userID, String name) {
        this.userID = userID;
        this.name = name;
    }
}

我們可以看到,Scala 的定義方式相較 Java 簡單了許多,我們只需要告訴 Scala 這個類別的建構子需要哪些參數即可,而不必自己宣告成員變數和重覆撰寫我們已經有的資訊。當然,在有些時候我們可能會希望這些成員參數可以被外界所存取,這個時候我們可以在建構子中的變數前面加上 val 或 var 前綴,分別代表其是 public val 或 public var 變數。

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

val person = new Person(102, "John Smith")
println(person.userID)  // OK
// person.userID = 103  // 編譯錯誤,userID 是 val 所以不能被重新指定
println(person.name)    // OK,印出 John Smith
person.name = "David"   // OK,name 是 var 變數,所以可以被重新指定
println(person.name)    // OK,印出 David

在 Scala 中定義類別還有一個需要注意的地方,就是雖然 Scala 的類別也可以有一個以上的建構子函式,稱為 auxiliary constructor,但這些建構子有一個嚴苛的條件--在該建構子的第一行一定要呼叫到定義在類別名稱後的建構子,或者其他已經定義過的建構子。

我們可以試著定義看看一個 auxiliary constructor,讓使用者只需要傳入 userID 就可以生成物件,在 Scala 中 auxiliary constructor 的名稱固定是 this 而不會跟著類別名稱而改變。

class Person(val userID: Int, var name: String) {
  def this(userID: Int) = {
    this(userID, "Who am I")
    println("In auxiliary constructor")
  }
  println("In primary constructor")
}

val person = new Person(1)
println(person.userID)      // 1
println(person.name)        // Who am I

上面的這段程式碼可以正常執行,而且會先印出 In primary constructor 這才印出 In auxiliary constructor,換句話說,auxiliary constructor 的內容實際上是在物件建立之後才能被執行。

所以如果我們把 println 移到 auxiliary constructor 的第一行的話,這個時候因為物件還沒被建立,Scala 會出現編譯錯誤,告知 auxiliary constructor 一定要先呼叫其他的建構子(以 this 為名稱的函式)。

class Person(val userID: Int, var name: String) {
  def this(userID: Int) = {
    println("In auxiliary constructor")
    this(userID, "Who am I")
  }
}

val person = new Person(1)
println(person.userID)      // 1
println(person.name)        // Who am I

也由於有這樣的限制,我們上面才會提到在 Scala 中若產生物件前需要有較為複雜的邏輯處理時,通常我們會使用 Factory Pattern,由工廠物件來負責初始化建立物件時需要的環境。

內建的 Singleton 物件

另一個 Scala 與 Java 在物件導向方面非常不一樣的地方,就是 Scala 中是沒有 static 變數與函式的,任何在類別中定義的函式與變數,都是 instance method 和 instance variable,在 Scala 中不存在 static 這樣的概念。

另一方面,Scala 使用 Singleton 物件來模擬 static 的特性,而所謂的 Singleton 物件,就是指在整個程式的執行時期,都只會有一份的物件。

在 Java 中若我們要達到這樣的效果,並須自己撰寫 class 並且使用 Factory Pattern 來確保使用者不能建立一個以上的該類別物件,但是在 Scala 中則直接提供了定義 Singleton 物件的方法,那就是使用 object 關鍵字。

object PersonData {
  var counter: Int = 0
  def printStatus() {
    println("counter:" + counter)
  }
}

PersonData.counter = 10
PersonData.printStatus()

當我們使用 object 關鍵字時,定義出來的這個 PersonData 就是 Singleton 物件,Scala 會在我們第一次存取到 PersonData 的成員函式或變數時產生這個物件,而且整個執行期只會有一份。

此外,object 關鍵字定義出來的物件名稱,是可以和用 class 關鍵字定義出來的類別名稱相同,這稱為 Companion Object,在這種情況下,相互可以存取彼此的 private 成員,而且看起來就會像是在使用 static 變數或函式一樣。

class Person(val userID: Int, var name: String) {
  Person.counter += 1
}

object Person {
  var counter: Int = 0
  def printStatus() {
    println("counter:" + counter)
  }
}

val person = new Person(1)
val person2 = new Person(2)
Person.printStatus()        // 印出 2

特殊的 apply 成員函式

最後值得順帶一提的,是在 class 或 object 中定義名為 apply 的函式,在呼叫的時候可以使用特別的語法--也就是不需要加上 .apply() 來呼叫。

class MyClass {
  def apply(str: String) {
    println("str:" + str)
  }
}

val myClass = new MyClass
myClass("Hello World")      // 印出 str:Hello World

我們可以看到,當在類別裡定義了 apply 函式時,我們可以直接將 new 出來的物件當作函式使用,而其呼叫的內容也就是我們所定義的 apply 函式。

同樣的,這個方法也可以應用在用 object 定義的 Singleton 物件上,而我們也可以用這個方法來解決上述提到的建構子的問題:

class Person(userID: Int, name: String)

class Person {
  def apply(userID: Int) = {
    if (userID > 0) {
      new Person(userID, "Buddy")
    } else {
      throw new Exception("userID must > 0")
    }
  }
}

val person1 = Person(1)
val person2 = Person(2)

在這個例子中,我們就可以利用這個特性,使用 Factory Method 在建立 Person 物件前先檢查 userID 的合法性,但另一方面又不會影響到 Client 端程式碼的可讀性。

現在讀者們應該知道在上一篇當中,當我們使用 List(10, 12, 31, 45) 這樣的語法來建立 List 的時候,到底發生了什麼事了吧?沒錯,我們實際上就只是呼叫到 List 這個 Singleton 裡定義的 apply 函式,然後由這個函式幫我們建立好 List 物件而已,而不是使用到了麼 List 專有的初始化語法。

小結

在這一篇中,我們看到了 Scala 中基本的物件導向功能,也可以發現 Scala 針對 Java 許多為人垢病的地方進行了改良,例如直接使用建構子來宣告成員變數,或是直接提供了 Singleton 物件等等。

但 Scala 的物件導向功能不僅如此而已,它還提供了相當重要的兩個功能--Case Class 與 Trait,我們下一篇就會來看看這兩樣東西到底可以幫我們解決什麼樣的問題。

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

留言

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

熱門論壇文章

熱門技術文章