Golang中的面向對象繼承


Golang的面向對象機制與Smalltalk或者Java等傳統面向對象的編程語言不同。傳統面向對象編程語言的一個重要特性是繼承機制。因為繼承機制支持在關聯對象間進行代碼復用和數據共享。繼承機制曾在代碼復用和數據共享的設計模式占據主導地位,但是目前組合這一古老的技術重新煥發了活力。

本篇文章轉自Tim Henderson的 "Object Oriented Inheritance in Go", 原文地址是 http://hackthology.com/object-oriented-inheritance-in-go.html 。非常感謝李浩和駿奇對於這篇文章的翻譯。

在我們探討如何在Go中實現繼承機制之前(Golong中的繼承機制和其他語言(Java)的繼承機制有區別),我們先看一下Java中如何實現繼承機制。

繼承與組合

讓我們先看一下我最喜歡的話題之一:編譯器!編譯器由管道轉換構成,該管道讀取text文本並將其轉化為機器代碼、匯編語言、字節碼或者其他的編程語言。管道首先會使用語法分析器對目標變成語言進行語法分析。一般情況下文本會被分解為不同的組成部分,例如:關鍵詞、標識符、標點和數字等等。每個組成部分都會被相應的數據類型標記。例如下面這個Java數據類型:

public class Main {} 

這些組成部分(可以稱作標記)如下所示:

<public keyword>, "public"
<class keyword>, "class"
<idenitifier>, "Main"
<left-bracket>, "{"
<right-bracket>, "}"

這些標記可以划分為兩個部分:

  1. 標記類型
  2. 語義部分

這會導致我們進行如下的Java設計方式:

public enum TokenType { KEYWORD, IDENTIFIER, LBRACKET, RBRACKET, ... } public class Token { public TokenToken type; **Here, I think TokenToken should be "TokenType"** public String lexeme; } 

對於一些標記類型來說,例如數值常量,標記類型最好能夠將包含這些屬性信息。就數值常量來說,在他的標記類型里應該包括常量值這一屬性。實現這一設計的傳統方式是使用繼承機制產生Token子類。

public class IntegerConstant extends Token { public long value; } 

另外一種完成該設計的方式是利用組合方式產生IntegerConstant,IntegerConstant包含token的引用:

public class IntegerConstant { public Token type; public long value; } 

在這個例子中,繼承機制是一個比較恰當的選擇。理由是語法分析器需要返回一個通用類型。考慮一下語法分析器的接口設計:

public class Lexer { public Lexer(InputStream in) public boolean EOF() public Token peek() throws Error public Token next() throws Error } 

在繼承機制中,IntegerConstant 屬於Token類型,所以它可以在Lexer中調用。這不是唯一可用或者最好的設計,但是這種設計方式是有效的。讓我們看一下Go是如何完成這一目的的。

Inheritance and Composition in Go

Go中實現組合是一件十分容易的事情。簡單組合兩個結構體就能夠構造一個新的數據類型。

type TokenType uint16 const ( KEYWORD TokenType = iota IDENTIFIER LBRACKET RBRACKET INT ) type Token struct { Type TokenType Lexeme string } type IntegerConstant struct { Token *Token Value uint64 } 

這就是Go中實現代碼和數據共享的常用方式。然而如果你想實現繼承機制,我們該如何去做?

Why would you want to use inheritance in go

 

一個可選的方案是將Token設計成接口類型。這種方案在Java和Go都適用:

type Token interface { Type() TokenType Lexeme() string } type Match struct { toktype TokenType lexeme string } type IntegerConstant struct { token Token value uint64 } func (m *Match) Type() TokenType { return m.toktype } func (m *Match) Lexeme() string { return m.lexeme } func (i *IntegerConstant) Type() TokenType { return i.token.Type() } func (i *IntegerConstant) Lexeme() string { return i.token.Lexeme() } func (i *IntegerConstant) Value() uint64 { return i.value } 

這樣分析器就可以返回滿足Match 和 IntegerConstant類型的Token接口。

繼承機制的簡化版

 

上面的實現方案的一個問題是*IntegerConstant的方法調用中,出現了重復造輪子的問題。但是我們可以使用Go內建的嵌入機制來避免此類情況的出現。嵌入機制(匿名嵌入)允許類型之前共享代碼和數據。

type IntegerConstant struct { Token value uint64 } func (i *IntegerConstant) Value() uint64 { return i.value } 

IntegerConstant中匿名嵌入了Token類型,使得IntegerConstant"繼承"了Token的字段和方法。很酷的方法!我們可以這樣寫代碼:

t := IntegerConstant{&Match{KEYWORD, "wizard"}, 2} fmt.Println(t.Type(), t.Lexeme(), t.Value()) x := Token(t) fmt.Println(x.Type(), x.Lexeme()) 

(可以在這里試一下 :https://play.golang.org/p/PJW7VShpE0)

我們沒有編寫Type()和Value()方法的代碼,但是*IntegerConstant也實現了Token接口,非常棒。

結構體的"繼承"機制

 

Go中有三種方式完成”繼承“機制,您已經看到了第一種實現方式:在結構體的第一個字段匿名嵌入接口類型。你還可以利用結構體實現其他兩種”繼承“機制:

1. 匿名嵌入結構體實例
type IntegerConstant struct { Match value uint64 } 
2. 匿名嵌入結構體實例指針
type IntegerConstant struct { *Match value uint64 } 

在所有的例子中,與正常嵌入類型不同的是我們使用匿名嵌入。然而,這個字段還是有字段名稱的,名稱是嵌入類型名稱。在IntegerConstant 的Match字段中,字段名稱是Match,無論嵌入類型是實例還是指針。

在以上的方案中,你不能嵌入與嵌入類型相同的方法名。例如結構體Bar匿名嵌入結構體Foo后,就不能擁有名稱為Foo的方法,同樣也不能實現type Fooer interface { Foo() }接口類型。

共享代碼、共享數據或者兩者兼得

相比於Java,Go在繼承和聚合之間的界限是很模糊的。Go中沒有extends關鍵詞。在語法的層次上,繼承看上去與聚合沒有什么區別。Go中聚合跟繼承唯一的不同在於,繼承自其他結構體的struct類型可以直接訪問父類結構體的字段和方法。

type Pet struct { name string } type Dog struct { Pet Breed string } func (p *Pet) Speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *Pet) Name() string { return p.name } func (d *Dog) Speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.Speak(), d.Breed) } func main() { d := Dog{Pet: Pet{name: "spot"}, Breed: "pointer"} fmt.Println(d.Name()) fmt.Println(d.Speak()) } 

(可以試一下 https://play.golang.org/p/Pmkd27Nqqy)

輸出:

spot
my name is spot and I am a pointer

嵌入式繼承機制的的局限

相比於Java, Go的繼承機制的作用是非常有限的。有很多的設計方案可以在Java輕松實現,但是Go卻不可能完成同樣的工作。讓我們看一下:

Overriding Methods

 

上面的Pet例子中,Dog類型重載了Speak()方法。然而如果Pet有另外一個方法Play()被調用,但是Dog沒有實現Play()的時候,Dog類型的Speak()方法則不會被調用。

package main import ( "fmt" ) type Pet struct { name string } type Dog struct { Pet Breed string } func (p *Pet) Play() { fmt.Println(p.Speak()) } func (p *Pet) Speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *Pet) Name() string { return p.name } func (d *Dog) Speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.Speak(), d.Breed) } func main() { d := Dog{Pet: Pet{name: "spot"}, Breed: "pointer"} fmt.Println(d.Name()) fmt.Println(d.Speak()) d.Play() } 

(試一下 https://play.golang.org/p/id-aDKW8L6)

輸出:

spot
my name is spot and I am a pointer
my name is spot

但是Java中就會像我們預想的那樣工作:

public class Main { public static void main(String[] args) { Dog d = new Dog("spot", "pointer"); System.out.println(d.Name()); System.out.println(d.Speak()); d.Play(); } } class Pet { public String name; public Pet(String name) { this.name = name; } public void Play() { System.out.println(Speak()); } public String Speak() { return String.format("my name is %s", name); } public String Name() { return name; } } class Dog extends Pet { public String breed; public Dog(String name, String breed) { super(name); this.breed = breed; } public String Speak() { return String.format("my name is %s and I am a %s", name, breed); } } 

輸出:

$ javac Main.java && java Main
spot
my name is spot and I am a pointer
my name is spot and I am a pointer

這個明顯的區別是因為Go從根本上阻止了抽象方法的使用。讓我們看看下面這個例子:

package main import ( "fmt" ) type Pet struct { speaker func() string name string } type Dog struct { Pet Breed string } func NewPet(name string) *Pet { p := &Pet{ name: name, } p.speaker = p.speak return p } func (p *Pet) Play() { fmt.Println(p.Speak()) } func (p *Pet) Speak() string { return p.speaker() } func (p *Pet) speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *Pet) Name() string { return p.name } func NewDog(name, breed string) *Dog { d := &Dog{ Pet: Pet{name: name}, Breed: breed, } d.speaker = d.speak return d } func (d *Dog) speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.speak(), d.Breed) } func main() { d := NewDog("spot", "pointer") fmt.Println(d.Name()) fmt.Println(d.Speak()) d.Play() } 

(試一下 https://play.golang.org/p/9iIb2px7jH)

輸出:

spot
my name is spot and I am a pointer
my name is spot and I am a pointer

現在跟我們預想的一樣了,但是跟Java相比略顯冗長和晦澀。你必須手工重載方法簽名。而且,代碼在結構體未正確初始化的情況下會崩潰,例如當調用Speak()時,speaker()卻沒有完成初始化工作的時候。

Subtyping

 

在Java中,Dog繼承自Pet,那么Dog類型就是Pet子類。這意味着在任何需要調用Pet類型的場景都可以使用Dog類型替換。這種關系稱作多態性,但Go的結構體類型不存在這種機制。

讓我們看下面的例子:

package main import ( "fmt" ) type Pet struct { speaker func() string name string } type Dog struct { Pet Breed string } func NewPet(name string) *Pet { p := &Pet{ name: name, } p.speaker = p.speak return p } func (p *Pet) Play() { fmt.Println(p.Speak()) } func (p *Pet) Speak() string { return p.speaker() } func (p *Pet) speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *Pet) Name() string { return p.name } func NewDog(name, breed string) *Dog { d := &Dog{ Pet: Pet{name: name}, Breed: breed, } d.speaker = d.speak return d } func (d *Dog) speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.speak(), d.Breed) } func Play(p *Pet) { p.Play() } func main() { d := NewDog("spot", "pointer") fmt.Println(d.Name()) fmt.Println(d.Speak()) Play(d) } 

(試一下 https://play.golang.org/p/e1Ujx0VhwK)

輸出:

prog.go:62: cannot use d (type *Dog) as type *Pet in argument to Play

然而,接口類型中存在子類化的多態機制!

package main import ( "fmt" ) type Pet interface { Name() string Speak() string Play() } type pet struct { speaker func() string name string } type Dog interface { Pet Breed() string } type dog struct { pet breed string } func NewPet(name string) *pet { p := &pet{ name: name, } p.speaker = p.speak return p } func (p *pet) Play() { fmt.Println(p.Speak()) } func (p *pet) Speak() string { return p.speaker() } func (p *pet) speak() string { return fmt.Sprintf("my name is %v", p.name) } func (p *pet) Name() string { return p.name } func NewDog(name, breed string) *dog { d := &dog{ pet: pet{name: name}, breed: breed, } d.speaker = d.speak return d } func (d *dog) speak() string { return fmt.Sprintf("%v and I am a %v", d.pet.speak(), d.breed) } func Play(p Pet) { p.Play() } func main() { d := NewDog("spot", "pointer") fmt.Println(d.Name()) fmt.Println(d.Speak()) Play(d) } 

(試一下 https://play.golang.org/p/WMH-cr4AJf)

輸出:

spot
my name is spot and I am a pointer
my name is spot and I am a pointer

所以接口類型可以用來實現子類化的機制。但是如果你想正確的實現方法重載,需要了解以上的技巧。

Conclusion

事實上,雖然這不是Go的主打特性,但是Go語言在結構體嵌入結構體或者接口方面的能力確實為實際工作增加了很大的靈活性。Go的這些特性為我們解決實際問題提供了新的解決方案。但是相較於Java等語言,由於Go缺少子類化和方法重載支持還有存在一些局限性。Go含有一項Java沒有的特性--接口嵌入。關於接口嵌入的細節請參考Golang的官方文檔的Embedding部分。

非常感謝echlebek, Alexander Staubo, spriggan3和breerly對這篇文章提供的支持!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM