2011年5月18日 星期三

5-15. Uniform access principle

Scala 嘗試將 data field 與 method 做進一步的一致化,也就 data field 與 method 區分不要再這麼清楚,以後可以視為同一個種類(可以把這二者想像是同一個東西)。一致化的好處是,sub class 可以很容易使用想要使用的方式,override parent 的 method 或是 val。

因為這個概念,衍生了以下這些我們之前討論的事情
1. data field 會產生對應的 method
2. method 與 data field 的 name space 是同一個
3. val 可以 override val
4. val 可以 override def

「Uniform access principle」就是希望 class 的使用者端,可以不理會 class 真正的宣告方式如何,可以使用相同的方式來 access 該 class 的資料。
舉例而言:有一個資料結構,有 length 的 property。length 可以是使用 data field 存起來,也可能是使用 method 來算出來,但 Scala 希望你都可以直接使用 obj.length 來 access 該 property。這就叫「uniform access principle」。

那要如何達到「uniform access principle」?

你可以使用我們之前所提到的「data member access」原則

我們要達到使用 obj.length 的方式,可以使用的方法
1. 使用 data member 宣告
class S {
  var length = 0
}
2. 使用 access methods
class S {
  def length = ...
  def length_=(n: Int) = {...}
}
請注意:access method 的 reader method 不可使用括號。

Uniform access principle 的概念將 data field 與 method 綁在一起,所以才會有一些混合 method 與 val data field 的用法,這是我們需要特別注意的地方。

5-14. Method override

如之前篇幅所提,Scala method 宣告與 Java 精神類似。但使用上有點不太一樣,我們一一說明

一. method override 需要清楚指明

Java 使用 @Override annotation 來指明要 override。使用 @Override 的好處是,你可以避免 override 了錯誤的  method,或是打錯字。建議你在使用 Java 時,需要善用 @Override。

Scala 承繼這個精神,並且發揚光大。在 Scala 中,若是你要 override method,需要特別指明,指明的方式,就是在 method 宣告時,使用 override 這個 keyword。
1. 若沒有指明 override,但你宣告一個與 parent class 相同的 method,compiler 會要求你確認是要 create 一個新的  method,還是要 override parent 的 method。
2. 若使用 override ,但祖先 class 並沒有宣告相同的 method,表示你的 override,是有問題的,compiler 會要求你修正正確。
範例:override
class S1 {
  def m1() = 10
}
class S2 extends S1 { // 繼承 S1
  override def m1() = 20 // 明確說明 override m1
}
範例:override 錯誤的 method
class S1 {
  def m1() = 10
}
class S2 extends S1 { // 繼承 S1
  override def m2() = 20 // 明確說明 override m2,但 S1 沒有 m2,所以 compile error
}

範例:沒有 override,但 method 與 parent class 相同
class S1 {
  def m1() = 10
}
class S2 extends S1 { // 繼承 S1
  def m1() = 20 // m1 在 parent class 出現過,所以此處會有 compile error
}

二. abstract method 可以不需宣告 override
前面有篇幅提過 abstract method,就是沒有 implement 的 method。
若你的 method 是 override abstract method,此時可以宣告 override,也可不宣告 override
範例:abstract method,可以不需宣告 override
abstract class S1 {
  def m1() //m1 是 abstract method,因為沒有 method body
}
class S2 extends S1 { // 繼承 S1  
  def m1() = 20 // m1 在 parent class 出現過,但因為是 abstract method,所以可以不宣告 override
}
範例:abstract method,也可以宣告 override
abstract class S1 {
  def m1() //m1 是 abstract method,因為沒有 method body
}
class S2 extends S1 { // 繼承 S1  
  override def m1() = 20 // m1 在 parent class 出現過,但因為是 abstract method,也可宣告 override
}

三. 「空括號 method」與「無括號 method」可以互相 override
雖然「空括號 method」與「無括號 method」,似乎不太一樣,但相同的地方仍然很多,所以 Scala 把他們視為一體,所以可以互相 override。

範例:「空括號 method」override「無括號 method」
class S1 {
  def m1 = 10 //m1 是 空括號 method
}
class S2 extends S1 { // 繼承 S1  
  override def m1() = 20 // S2.m1() override S1.m
}

範例:「無括號 method」override「空括號 method」
class S1 {
  def m1() = 10 //m1 是 空括號 method
}
class S2 extends S1 { // 繼承 S1  
  override def m1 = 20 // S2.m1 override S1.m()
}

在 Scala 中的 val field,與其說是一個 data field,更像是一個常數的 method,把它當成 method,有時反而更加適當。def 也可以像 method 一樣 override,甚至與 def 之間可以 override。

四. val 與 val 間就像 def 一樣,override 的規定要求相同
範例:val override val
class S1 {
  val m1 = 10 //m1 是 val
}
class S2 extends S1 { // 繼承 S1  
  override val m1 = 20 // val override val
}
範例:同名的 val
class S1 {
  val m1 = 10 //m1 是 val
}
class S2 extends S1 { // 繼承 S1  
  val m1 = 20 // 與 parent 同名的 val,需要宣告override
}

五. val 可以 override def
def 定義一個 method,val 定義一個常數。Scala 中強調不可變資料的重要性,所以 val 可以 override def,但反向是不允許的。
範例:val override def
class S1 {
  def m1() = 10 //m1 是 空括號 method
}
class S2 extends S1 { // 繼承 S1  
  override val m1 = 20 // val override def
}
class Test {
  def test {
    val s2 = new S2
    s2.m1 // 此時會得到 20
    s2.m1() // 糟糕,這裡會出錯,因為 S2 並沒有 m1()
    val s1: S1 = s2
    s1.m1() //可以使用了
  }
}
注意:上例第 11 行的 s2.m1() 是錯誤的,因為 S2 沒有宣告 m1(),只有 val m1,此 val m1 是 override S1.m1()。
但這很奇怪啊?S2 的 val m1 可以 override S1.m1(),但卻不能使用該 m1() method?
這是因為我們需要回歸到「uniform access principle」(下節會提到)的本意,是讓沒有括號的 method 與 val 混用,但此時 s2.m1() 是希望直接使用 method,這就不符合「uniform access principle」原先的想法,所以被禁止了。
你若是還是希望使用 m1() method 的方式,此時需要先將 s2 cast 成一個 S1 的 instance,這時就可以使用了,方式就如同上例的第 12, 13 行一樣。

範例:def override val
class S1 {
  val m1 = 10 //m1 是 val
}
class S2 extends S1 { // 繼承 S1  
  override def m1 = 20 // def override val,這是錯誤的
}

上例打算使用 method 來 override val,當然不會允許,原因是 val 被視為常數,所以一定是不能被 override 的。

5-13. 「無括號 method」與「空括號 method」

Scala 的 method 宣告,雖然語法與 Java 不同,但精神 Java 的類似。
class S {
  def m1() = {...}
}

但 Scala 的 method 在處理無參數的 method 時,與 Java 有滿大的不同。

在 Scala 中,若 method 沒有參數,method 宣告時可以不包含小括號。但需要注意的是,沒有小括號與有小括號的 method 二者是不一樣的。

無括號的 method 稱為「無括號 method」,而有括號但無參數的 method,稱為「空括號 method」。

呼叫時,無括號的 method 需使用無括號的呼叫方式。而有括號的 method,可以使用有括號的呼叫方式,但由於小括號省略原則,也可以使用無括號的呼叫方式。

範例:「空括號 method」
可以使用有小括號的呼叫方式,或是無小括號的呼叫方式
class S {
  def m() = {....}
}
class Test {
  def test() = {
    val s = new S
    s.m() //使用有小括號的呼叫方式
    s.m //小括號省略原則,所以這是合法的
  }
}

範例:「無括號 method」
只可使用無小括號的呼叫方式
class S {
  def m = { //這裡是沒有小括號的 method
    ...
  }
}
class Test {
  def test() = {
    val s = new S
    s.m // 這是正常的 method 呼叫,並非使用小括號省略原則
    s.m() //由於 m 其實是無括號之 method,所以這種呼叫方式是錯誤的
  }
}
上例很清楚看到,無括號 method 不能使用有括號的呼叫方式。

雖然「無括號 method」與「空括號 method」,兩者被視為兩個不同的 method,但同名的這兩種 method,仍不能同時存在。
範例:同名的「無括號 method」與「空括號 method」,不可一起存在。
class S {
  def m() = .... // 空括號的 method
  def m = ... // 無括號 method
  }
}
上例,有兩個 method m,會有 compile error。

「何時使用空括號,何時使用無括號?」

在 Scala 中,「無括號 method」很常見,通常用在於直接 return 回一個 data field 的值。所以,無括號 method 通常不會有冗長的 code 以及 side effect。
相反的,空括號的 method,則與一般的 method 一樣,該 method 可以處理很多事,或是有 side effect。其與其他 method 的差異點,只在於它剛好沒有參數而已。

無論是「無括號 method」或「空括號 method」,由於小括號原則,我們都可以使用無括號的方式來呼叫。

「那何時使用有括號呼叫,何時使用無括號呼叫?」

與上述決定「何時宣告無括號 method」的通則一樣。當呼叫到一個複雜的 method,Scala 建議你使用空括號的呼叫。若你是呼叫一個無 side effect 的 method,這時建議你使用無括號的呼叫方式。

總之,不論是宣告,還是呼叫,無括號的 method,使用在無 side effect 時。而空括號的 method,使用在複雜或有 side effect 的情況下。

5-12. 小括號省略原則

為支援快速開發的目標,在 Scala 中,不像 Java 那麼重視小括號。
在 Java 中,小括號需要存在的地方,我們不可隨意省略。
但在 Scala 中,我們有很多地方都可以省略括號,尤其是與 class 相關,有許多地方都可以省略小括號,。

小括號最常使用在 method 宣告與 method call 時,在我們進一步討論 method 時,我們先說明一下「小括號省略原則」,以下是可以省略的用法
  1. create instance 時,若 constructor 不需有參數,小括號可以省略。這已在上篇討過。
  2. 在 method 宣告時,若該 method 沒有參數,可以宣告無小括號的 method。
  3. 在 method 呼叫時,若是不需要有參數,可以省略點(dot) 與小括號。
  4. 在 method 呼叫時,若是只有 1 個參數,可以省略點(dot) 與小括號。
不要小看小括號省略原則,我們後來將會看到小括號省略將會如何影響 Scala 的語法,說小括號省略影響 Scala 的設計理念其實也不為過。我們將於以下章節繼續討論。

5-11. Data member 與 Access methods

關於 data member,Scala 編譯出來的 bytecode,遵循 OO 傳統的 encapsulation 觀念,會將所有 data member 使用 private modifier 隱藏起來,並產生 access method 作為真正要 access 這些 data member 的方式。

在 Scala 宣告 data_member,會產生以下的東西:
1. 產生一個 private data member,若是常數,則會加上 final modifier
2. 產生 access method
    若是常數,只會有一個 reader 的 access method
    若是變數,會有 reader 與 writer 的 access methods
    access method 的 visibility 將會與該變數的 visibility 有關。

讓我們依序說明 data member 常數與 data member 變數。

範例:宣告一個 data member 常數
class S {
  val val_s: String = "abc"
}
使用 javap 查看,得到


可以瞭解,在 Scala 中的一個 data_member 常數  val_s,會產生
1. 一個 Java 的 data member:final private val_s
2. 一個 Java 的 access method:public val_s()

說明:
由於是 data member 常數,所以會變成 final 的 Java data member
由於 Scala 中的 member,default 是 public,所以 access method 的 visibility 是 public

讓我們將 data member 換成 private,看會產生何種變化
範例:宣告一個 private 的 data member 常數
class S {
  private val val_s: String = "abc"
}



我們可以看到,與上一個例子相同,只是 method val_s() 變成 private,可見得 access method 的 visibility 與 Scala data member 的 visibilty 相同。

接下來我們看 data member 變數,會產生什麼?
class S {
  private var var_s: String = "abc"
}




在 Scala 中的一個 data_member 變數  var_s,會產生
1. 一個 Java 的 data member:private var_s
2. 一個 Java 的 reader access method:public var_s()
3. 一個 Java 的 writer access method:public var_s_$eq()

說明:
由於是 data member 變數,所以該 Java data member 沒有加上 final
由於 Scala 中的 member,default 是 public,所以 access methods 是 public

由上面的例子,很清楚我們發現兩個事實
1.Scala 的 data member 常數,會編譯成 final private data member,以及 reader methods。
2.Scala 的 data member 變數,會編譯成 private data member,以及 reader / writer 兩個 methods。

該 data member 的 reader method,就是與 data member 同名的 method。
該 data member 的 writer method,就是 data member 名稱,後面再加上「_$eq」的 method。

Scala 中,有以下的概念「data member 使用原則」
  1. Scala 中,只要有 reader method,就可以把它當成 read 的 data member 來處理。
    • 所以只要有一個 method 叫 x(),我們可以把它視為有一個可讀的 x data member。
  2. Scala 中,只要有 reader / writer method,就可以把它當成 write 的 data member 來處理。
    • 所以只要有兩個 method 叫 x 與 x_$eq(),我們可以把它視為有一個可寫的 x data member,所以我們可以直接使用 obj.x = ... 的格式。
    • 但是,若只有一個 writer method,則無法把它當成 data member 來使用
請注意:「name_$eq()」這種格式的 method 名稱,是在 JVM 層次所看到的,若你想要在 Scala 中產生這種格式的 method,你可以宣告一個 method 叫「name_=()」。

範例:有 reader method x(),當成 read 的 data member 來看待
class S {
  def x() = 100 // S 有 x() method,可以將它視為 read only 的 x
}
class Test {
  def m1() = {
    val s = new S
    val n = s.x // 將 x 視為 S 的 data member 來處理
  }
}
請注意,上例使用 s.x 是合法,雖然 S 裡面沒有一個 data member x。

其實,Scala 中更有一個關於 method call 的小括號省略原則。小括號省略原則讓你可以省略小括號,所以可以使用 s.x,這裡的 「data member 使用原則」也讓你可以使用 s.x,兩者是相通的。

範例:有 reader method x 與 writer method x_$eq(),當成 write 的 data member 來看待
class S {
  var x1 = 0
  def x = x1
  def x_=(n: Int) = x1 = n // S 有 x_=() method,可以將它視為 write 的 x data member
}
class Test {
  def m1() = {
    val s = new S
    s.x = 10 // 將 x 視為 S 的 data member 來處理
    println(s.x) // 將 x 視為 S 的 data member 來處理,這時候將會印出 10
}
請注意:需要有 x 與 x_$ 兩個 method 才會被視為 data field
注意:x_$=() method 會編譯成 x_$eq() method。

上例中,「s.x = 10」可以看成是 syntax sugar,會被 compile 改成呼叫「s.x_(10)」
其實,我們應該更精確的講,Scala 中沒有所謂的 data field 的 assignment,所有的 data field assignment,都會被改變成 call obj.name_=(value)。

範例:有 writer method x_$eq(),但沒有 reader method x,此時不可當成 data member 來看待
class S {
  var x1 = 0
  def x_=(n: Int) = x1 = n // S 有 x_=() method,但沒有 def x method
}
class Test {
  def m1() = {
    val s = new S
    s.x = 10 // 這裡會出錯,有 writer 但 沒有 reader 仍不行
}
範例:有 writer method x_$eq(),有 reader method x(),此時不可當成 data member 來看待,因為 reader method 需為 method x
class S {
  var x1 = 0
  def x() = x1
  def x_=(n: Int) = x1 = n // S 有 x_=() method,但沒有 def x method
}
class Test {
  def m1() = {
    val s = new S
    s.x = 10 // 這裡會出錯,有 writer x_=() 但 沒有 reader x 仍不行
}

請注意,空括號的 method x() 與無括號的 method x,在 Scala 中是視為不同的,若想要當成 data assignment 的 method,所需要的是無括號的 method x()。

將上例改為如下例
class S {
  var x1 = 0
  def x = x1 // 無括號的 method
  def x_=(n: Int) = x1 = n // S 有 x_=() method,但沒有 def x method
}
class Test {
  def m1() = {
    val s = new S
    s.x = 10 // 這裡就會成功了
}

這裡我們需要重複之前的一句話,我們應該更精確的講,Scala 中沒有所謂的 data field 的 assignment,所有的 data field assignment,都會被改變成 call obj.name_=(value),但前提是 name 需要有兩個 method 一個是 name_=(value) method ,另一個就是 name 這個無括號的 method。

另外,當我們使用 obj.name 來 access 該 obj 的 name data field 時,其實會被轉變成呼叫 obj.name 這個 method 或是 obj.name() 這個 method (這兩個 method 沒辦法不會同時存在)。

綜合上述兩點,我們可以看出 Scala 對於 data field 的操作其實與 Java 完全不同,Java 會直接操作到該 data field,但 Scala 會將所有的 data field 都轉換成 method call,所以外部程式其實是無法直接碰觸到該 data field 的,這樣保留了一點點安全性。
Scala 沒有像一些語言為保留這樣的安全性而直接取消掉 data field 的操作方式,保留這樣的 data field 的使用方式給 programmer 帶來相當大的方便性,Scala 將 data field 的操作直接在語言直接轉會成 method call,我們可以看成這是 syntax sugar,但需要時時提醒自己,其實這只是呼叫到相對應的 method。

5-10. Data member 與 method 的 name space

在之前我們談過,Scala 的 class 宣告格式為:
class S {
  var var_n: Int = 10
  val val_s: String = "hello"
  def m(n: Int) = n + 10
}
上例中,我們宣告
1. 使用 var 宣告一個 data member 變數:var_n
2. 使用 val 宣告一個 data member 常數:val_s
3. 使用 def 宣告一個 method:m

在 Java 中,data member 與 method 的 name space 是不同的,所以 data member 與 method 可以同名。

但 Scala 中,data member 與 method 共用 name space,因此 Scala 中你不可以宣告 data member 與 method 同名。
其實,說清楚一點是,Scala 會為 data member 產生 access method。而所產生的 access method 會與 data member 同名。所以此時若再宣告同名的 method,將會造成兩個同名的 method,產生 compile 錯誤。

範例:Java 中,method 可以與 data member 同名。
class J {
  int n1 = 10;
  int n1() {
    return 10;
  }
}

範例:Scala 中,method 不可以與 data member 同名。
class S {
  var n1 = 10 // compiler 會編譯本 statement 成為一個 bytecode 的 data member,以及至少一個 access method n1()
  def n1() = 10 // compiler 會認為此 statement 重複宣告 n1()
}

2011年5月7日 星期六

5-9. Access control

OO 語言有一個重要的特色,就是資料封裝(data encapsulation),所謂資料封裝,就是希望外面使用這個 class 的呼叫者,不需知道該 class 是如何 implementation,只需知道怎麼應用即可。

資料封裝的達成方式,通常就是透過 access control來達成。
所謂「access control」就是在定義 class 時,你希望該 class 有多少資訊可以給外界使用。
access control 有時又會稱呼為「visibility」。

Java 的 access control 有 4 種
1. private:只有自己可以用
2. package:只有自己與同 package 的可以用,這是 default
3. protected:只有自己、同 package、以及 subclass 可以用
4. public:全世界都可用

Java 使用 modifier 來定義 method 與 data field 的 access control。Java access modifier 有 public、protected、private。並沒有 package 這種 modifier,但 default 是 package access。
Java 的 access control 相信大家都熟,所以不仔細介紹。

Scala 的 access control,大部分的概念與 Java 相同,也同樣使用 access modifier 來定義 access control,但有些地方不一樣。以下依序說明:

1. Scala 標榜快速開發,所以 default 是 public。只有那些你想要保護的 method 或 data field,你才需要使用 access modifier 加以保護。

2. Scala 的 access modifier 只有 private 與 protected,沒有 public。也沒有所謂的「package access」這種模式,因為沒有標明 access modifier,就表示是 public。但並不表示 Scala 無法做到「只讓相同 package 的 class 來 access」,Scala 使用「protection scope」來達到,protection scope 將詳述於下。

3.Private member:outer class 不能 access inner class 的 private field。
Java 把 inner class 當成與自己是同一個體,所以 inner class 與 outer class 彼此可以互相 access 對方的 private field。
舉例說明
class J {
  private int n_outer_private; //宣告一個 private data field
  class Inner {
    private int n_inner_private;
    void p() {
      int n = n_outer_private; //可以使用 outer private field
    }
  }
  void p_outer() {
    Inner inner = new Inner();
    int n = inner.n_inner_private; //可以使用 inner private field
  }
}
但 Scala 對待 private 的方式,比 Java 更加嚴謹。Scala 為支援 functional 與 concurrency 的因素,決定將 改變 Java 那種把 outer class 與 inner class 視為同一個體的觀念。

在 Scala 中,inner class 視為 implement 的手段,所以 inner class 會被保護起來。outer class 無法 access inner class 的 private field。但 inner class 仍然視為 outer class 的一員,仍舊可以 access outer 的 private field。
範例:outer class 不可 access inner class 的 private。但 inner class 可以 access outer class 的 private。
class S {
  private var n_outer_private = 10
  class Inner {
    private var n_inner_private = 10
    def inner_print = println(n_outer_private) // 這裡是 inner class 去 access outer class 的 private,這理是正確的
  }
  def outer_print = {
    val inner = new Inner
    val n = inner.n_inner_private // 這裡是 outer class 去 access inner 的 private,這裡會有 compile 錯誤
  }
}

4.Protected field 只有自己與 subclass 可以 access,移除 package 可以 access 的觀念。
在 Java,protected field 可以讓相同 package 的其他 class 來 access。但這個許可,在 Scala 被移除,所以 Scala 的 protected 只有自己與 subclass 可以 access。
同樣的,若希望 package 的其他 class 可以 access 被保護起來的 field,可以使用「protection scope」的方式來做到。

範例
class S1 {
  protected def p_protected() = println("S1")
}
class S11 extends S1 {
  def p_S11 = p_protected() // 使用 super class 的 protected,這是合法的
}
class S2 {
  val s1 = new S1
  def p_S2 = s1.p_protected() // 使用同一 package 的 protected,在 Scala 這是不合法的。但在 Java 這是合法的
}

5. Protection Scope:private 或是 protected 保護的 field,可以進一步訂定保護的範圍。
所謂保護的範圍,指的是我們可以擴充或縮小可以 access 的範圍。
比如 private 的 access 範圍,原來指的是自己的 class,但我們可以使用「protection scope」加以擴充或縮小。

格式為:
private[X] field 或
protected[X] field

private[X] field 代表設定該 field 為 private,但可以放大或縮小到 X 的範圍。
protected[X] field 代表設定該 field 為 protected,但可以放大或縮小到 X 的範圍。

這個[X]稱為 access qualifier,X 可以縮小為 this,或是放大到 package。所以我們可以用這個方式來定義,package private 或 package protected。

範例:限定範圍到 package
package p1 {
  package p2 {
    private[p1] class C1 // class C1 為 private,且可以 access 的範圍擴充到 package p1
    private[p2] class C2 // class C2 為 private,且可以 access 的範圍擴充到 package p1.p2
  }
}
範例:限定範圍到 outer class
class Outer {
  class Inner {
    protected[Outer] def p1 = println("p1") // 限定的範圍擴充到 Outer
  }
}
範例:限定範圍到 class
class Outer {
  class Inner {
    protected[Outer] def p1 = println("p1") // 限定的範圍擴充到 Outer
  }
}
範例:限定範圍到自己這個 instance。
這是 Java 沒有的限制方式,表示本 field,只有自己 instance 可以 access,就算相同的 class 的 instance 也不允許 access。此種限制方式稱為「object-private」,是最嚴格的限制
class S {
  private[this] val n_object_private = 10 // 這個是 object private
  def p = {
    val s = new S // create 另一個 S instance
    val n = s.n_object_private // 錯誤,因為在本 method 底下,不允許 access 別的 instance 的 object private field
  }
}

6. Companion object 與 class 的 access right。
Companion object 與 class 這兩個東西,在 Java 世界中其實是一個 class,為了簡化方便,Scala 將一個 class 創造出兩個 class 出來。
既然原先在 Java 中本來是一個 class,所以二者本來就是一體,所以有兩個結論

a. Companion object 與 companion class 彼此可以互相 access private 的部份。

b. Companion object 與 companion class 的 access right 是一致的。也就是,若有一個 class X 允許讓 class C access,則 object C 同樣也 access X。

7. Package 沒有 access control 的機制,也就是 package 一定都是 public 的。

8. class 宣告也可以加上 private 或 protected 以做進一步的控制。
protected 的 class 表示該 class 只能給同一個 package 或 sub_package 的其他 class access。
private 的 class 表示該 class 只能給同一個 package 的其他 class access。
protected[this] 的 class 表示該 class 只能給包起來的 package 的其他 class access。
private[this] 的 class 表示該 class 只能給包起來的 package 的其他 class access。
範例
package p {
  private class C { // 宣告 package p 裡面有一個 private class C
  }
}