iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 20
0

今天終於要來談談介面,也就是 Interface 啦!之前當我們在談繼承以及抽象類別的時候,說到在 Golang 跟 Rust 並沒有這樣的概念,在這兩個語言則是透過實作 Interface 來實現多型 (在 Rust 實際上叫做 Trait,我們晚點會提到)。而在 Scala 雖然有繼承系統,不過也有類似 Java Interface 的東西,叫做 Trait,但和 Java 有點不同,我們在講 Scala 的時候也會提到。最後就是 Python 的部分並沒有 Interface,我們也會簡單說明下原因囉!


Python

  • 對於 Python 來說,因為有多重繼承的關係,所以並不需要像 Java 另外引入 Interface 來達到類似多重繼承的目的。此外,因為 Python 是 DuckTyping,所以有些需要 Interface 的場景在 Python 也不需要。這邊解釋下 DuckTyping,這是動態型別語言的一種風格,最常拿來解釋的一句話就是"當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子"。在 DuckTyping,一個 Object 有效的屬性和方法,不是繼承自特定的類型或是實現了某個 Interface,而是看當下其所有的屬性和方法的集合。DuckTyping 的函式接收的是任意類型的 Object,而在函式呼叫該 Object 的特定方法,因此我們關注的是,這個 Object 是否可以執行這個行為,而不是這個 Object 的類型。好處是很彈性,壞處是程式必須要到了執行期才可能發現問題。
  • 假如你需要一個像 Interface 這樣的東西來規範一些行為的話,可以運用我們之前在 Abstract Class 提到的,透過引入 metaclass ABCMeta,來讓自定義的 Class 有個可以繼承的 Abstract Class (裡面沒有方法有被實作)來做到行為規範。

Scala

  • Scala 和 Java 一樣都是 JVM 的語言,由於在 Java 因為沒有多重繼承 (避免鑽石問題),所以另外提供了 Interface,在 Java 的 Interface 只有函式簽名,並沒有包含實作 (Java 8 有 Default Method 了)。然後有時候有些實作,例如 "跑" 這個 Interface 對於獅子跟玩具獅子在 "跑" 的實作上應該是一樣的,這樣就會造成程式的重複。而 Scala 的 Trait 則是可以有 Interface 的效果,卻又讓你可以包含實作在裡頭。最簡單的形式如下。有一個 Trait Running 裡頭有一個方法 run()。而 class Lion 繼承了 Xxx,也實作了 trait Running (用 with)。注意的是,假使今天沒有繼承 Xxx,只有實作 Running 的話,要用 class Lion extends Running,但是這裡並不是繼承的關係,只是寫法上必須是這樣。假如沒有實作完全,就必須宣告成 abstract class 囉!
trait Running {
  def run(): Unit
}

class Lion extends Xxx with Running {
  def run() = println("Run with 4 legs") 
}
  • 假如 Trait 已經有了實作的方法,當我們使用 Trait 時,就不用自己再實作該方法。如果要覆蓋的話,就要用 override
trait Running {
  def run(): Unit = println("Run")
}

class Lion extends Xxx with Running {
  override def run() = println("Run with 4 legs") 
}
  • 而 Trait 也可以有尚未賦予值的變數,例如下面,可以看到 sayHello() 依賴 name 的值,所以當我們的 Class with SayHi 的時候就要定義 name 的值,不然也是會被認定是 Abstract class 囉。除此之外,Trait 也可以 Extend 某個 Class (例如 Class A ),來依賴該 Class 的某個 Method,所以間接限制了可以引入此 Trait 的 Class,例如繼承了 Class A 的 Class 就可以引入,但不相干的 Class 就不行了!
trait SayHi {
  val name: String
  def sayHello() = println("Hey, I'm " + name)
}
  • 當同時引入多個 Trait 的時候,假設有同名的函式且都 Extend 某個 Class,則 Scala 有一套規則來找出優先使用的函式,也就是最後引入 (最右邊) 的 Trait 的同名 Method 會被執行。而如果在 Trait 中使用 super 時要注意,因為 Trait 不是繼承那種層級關係,所以 super 在這其實是指最左邊,也就是最早引入的 Trait。也因為 Trait 的特性,可以拿來實現所謂的 Decorator Pattern,有興趣的可以參考這裡囉!

Golang

  • Interface 是具有一組方法的類型,像是上述的宣告方式,用的關鍵字是 type <Interface name> interface。然而 Interface 也可以不帶任何方法,又叫做 Empty Interface。如果我們今天有一個類型實作了一個 Interface 所有的方法,我們就說這個類型實作了這個 Interface,例如下面的 MyStruct 實作了 MyInterfaceA() int (func(s MyStruct) A() int),所以當 func f(i I) 需要 MyInterface 這個類型作為參數時,就能傳入 MyStruct 的 instance。
type MyInterface interface {
    A() int
}

type MyStruct struct {
    X int 
}

func(s MyStruct) A() int {
    return s.X
}

func f(i MyInterface) {
    i.A()
}

var i MyInterface = MyStruct{X: 1}
  • 可以發現在 Golang 這種不用顯性表明實作了哪個 Interface 的方式,而是只要實現該 Interface 的所有方法,就是 DuckTyping 的體現。Go 會自己檢查,並適時將類型轉換成該 Interface 進行使用。例如上述的程式碼的 var i MyInterface = MyStruct{X: 1}。如果要知道某個 Interface 的值,實際上是不是某個 Struct 的話,就可以用例如 value, ok := i.(MyStruct),這裡是看 i 這個 Interface 的變數是不是 MyStruct,如果是的話,value 就是該類型的值,而 oktrue,反之則是 false,這就是所謂的 Type assertion。當我們想要同時判斷很多類型時,還可以使用所謂 Type switch,可以參考這裡
  • 至於空的 Interface 是拿來幹嘛呢?因為任何類型都實作了空的 Interface (因為沒有方法),以下面程式來看,此 Function f 的參數表示可以丟入任何類型的值,Go 會自動幫我們進行轉換。然而 fAll 的參數是空 Interface 的 Slice,那是不是表示我們可以丟入任意類型的 Slice 呢?出乎意料,答案是不行的,可以參考官方解釋囉!
func f(p interface{}){   
    ...
}

func fAll(p []interface{}) {
    ...
}

  • 最後來談談的是實作的 Function 可以用 Value Receiver 也可以用 Pointer Receiver,但要注意的是,假使某個類型在實現一個 Interface 的時候,用了 Value Receiver 也用了 Pointer Receiver,那就只能使用其 Pointer 來作為該 Interface。這是因為 Golang 是 Call by value,當丟入的是 Pointer 是可以調用 Value receiver 的 Function,因為 Go 可以自動從此 Pointer 找到真正的 Value,但反過來則不能,因為沒辦法反查出原始 Value 的位置。
  • 關於更多 Interface 相關的知識可以參考這裡囉!

Rust


上一篇
[Day 18] 疊起來還是排起來
下一篇
[Day 20] 把東西給我排好
系列文
30 天把自己榨好榨滿的四週四語言大挑戰!30

尚未有邦友留言

立即登入留言