Go對OO的選擇


Go摒棄了許多OO的概念,但是還是很好的繼承了OO的精髓——消息傳遞。我猜這個是學了Smalltalk的。通常我們說OO,我們會說這三大特性:對象,繼承,多態。

1,Go中的對象

對於GO來說他的類型系統也分為兩類:value type和reference type。value type就是內存中的內容,reference其實也是類似引用地址的指針。這樣Go中的對象很自然的分為兩類的:內容和引用內容的地址。Go的一個哲學就是讓“天下沒有隱晦的東西”。所以Go的對象在內存中的表示和其定義是一一對應的。有點“what you see is what you get”的感覺。

而在Go中,實現對象特征的是struct,其實這個好理解。當年C++之父Bjarne Stroustrup在從C語言搗騰C++的時候也是先擴展C中的struct,只不過,Bjarne Stroustrup太貪心,做的太多的。這個在學術上是有意義的,但在工程上並不買賬。

Go中一切直白,沒有隱藏的this指針。struct定義決定了struct內存的布局。但是Go添加了method,其實就是附加在struct上的一系列方法。我們可以通過struct的變量(相當我們OO中的實例)來調用這些方法。這些其實就是寫語法糖而已。讓原本C中的:

sayHello(Person *p, char *msg);

變成了類似於:

p := new(Person)

p.sayHello(msg string)

即使這小小的一變,在意義上變化大着了。這里的p是variable,Go稱這樣的variable為receiver。其實就是OO中的消息傳遞,而這個接受者就是p這個變量。

Go中有一點小的改進,調用語法上取消了對struct的變量和變量指針的區別。在上例中p := new(Person)中的p其實是new返回的指向Person變量的一個指針。但是我們仍然可以用點號來調用:p,sayHello()。

Go中的對象就這些,沒有別的了。

2,Go中的繼承

C++中的繼承是很隱晦的,子類的constructor會自動調用父類的constructor,等等,等等。我們所謂的繼承,其實是為了一種共享。這個共享,是通過嵌入來實現的。傳統的OO語言中把這個工作交給編譯器了,通過一些語法支持,編譯器會創建一個父類對象的實例,然后通過類似base的指針去引用父類對象。好處就是創建時相對會簡單,在工程上的缺點就是如果你不了解這些語法規則,那么你寫的代碼中有很多的坑——最可怕的是你並值知道有這些坑。

Go仍然是本着“all things are obvious”,組合就是組合,即把一個類型嵌入到另一個類型中。看個例子:

package main

import "fmt"

type Cellphone struct {
	brand string
	name string
	number string
	cost float32
}

func (c *Cellphone) showNumber() {
	fmt.Println("number:" + c.number)
}

type Person struct {
	Cellphone
	name string
	age int
}

func (p *Person)showPersonalInfo() {
	fmt.Printf("name:%s\tage:%d\t", p.name, p.age)
	p.showNumber();
}

func main() {
	p := Person{Cellphone{"moto", "Defy", "+86-13844448888", 2000}, "jerry", 31}
	
	p.showNumber()

	fmt.Println(p.brand) //output:moto

	fmt.Println(p.name)  //output:jerry

	p.showPersonalInfo()

}

直接將一個類型定義中引用需要嵌入的類型。Go做了一些語法上的支持:

1,可以直接調用嵌入類型的方法,比如p.showNumber()。或是直接引用嵌入類型的字段p.brand

2,可以嵌入多個類型。例子中沒有顯示。

3,每個類型只能嵌入一次。

4,當嵌入的類型和當前類型類型有重名的時候,當前類型覆蓋(重寫?)嵌入(父類?)類型的字段。這個模擬了子類overwrite父類父類的功能。比如例子中的p.name顯示的是Person這個struct的name。

5,當嵌入的類型超過一個,並且有字段有重名的情況。這時候我們可以明確地引用比如p.Cellphone.name。否則編譯時會有類似:xxx ambigous的錯誤。

Go的繼承也就這么多了,他的想法就是,讓顯性(obvious)的東西保持顯性。即使使用了語法糖,也絕不在后面做小動作。

3,Go中的多態

短答案:Go中沒有多態。面向對象中的多態其本質是方法的分發。通過類似於virtual table的東西記錄下分發的規則。有復寫的時候方法分發給當前實例(子類)方法,沒有復寫的時候分發給父類相應的方法。Go中不允許不同類型的賦值,所以從根本上不支持傳統OO中的多態。

Go中通過interface來實現(契約式)方法的分發。interface定義了方法的簽名,任意類型只要實現了該方法,我們就認為這個類型實現了這個接口。這個不需要在定義類型的時候指定該類型所要實現的接口的方式被稱之為“非入侵式的接口(non-invasive interface)”。

Go雖然不容許不同類型間的賦值,但是接口是個例外(接口在Go的內部也是一種特別的type)。只要一個類型實現了該接口,我們就可以將該類型的變量賦值給該接口的變量:

//Interfaces
type Shaper interface {
	Area() float32
}

//Square
type Square struct {
	border float32
}

//we can assign the variable of Square to variable of Shaper(interface)
var ishaper Shaper = &Square{20.0}

這樣Shaper的變量就可以引用任意一個實現了該接口類型的變量了。在底層接口變量其實是一個帶着兩個數據的指針:一塊數據是該receiver(即引用的實際變量),另一塊是該接口實現的方法表指針。借助於這點Go多態也就完善了。Go中稱這個位type assertion。其實也就是OO中的類型推斷。我們可以通過類似這樣的語法去看變量是否實現了接口,或者是從接口中得到具體的類型:

ishaper = s  //assign a variable to interface variable
fmt.Println(ishaper.Area())

//tye assertion, get value from interface
if v, ok := ishaper.(*Square); ok {
	fmt.Println(v)
}

//type assertion to test value implement a interface
//type assertion is only applied on interface, so we cast the s to empty interface
if v, ok := interface{}(s).(Shaper); ok {  
	fmt.Println(v.Area())
}

好了,現在不難寫出來體現多態的方法了,記住本質就是方法分發(method dispatch):

func PrintArea(s Shaper) {
	fmt.Printf("The area is %v\n", s.Area())
}

如果你想實現可以接受任意參數的函數怎么辦?Go中的空接口可以實現這個功能,空接口其實就是沒有任何方法的接口:

type Any interface{

}

任何類型都實現了空接口,也就是說Any接口的變量可以引用任意一個其他類型的變量,在利用type assertion,我們可以寫出通用的方法:

func PrintArea(s interface{}) {
	if v, ok := s.(Shaper); ok {
		fmt.Printf("The area is %v\n", v.Area())	
	}
}

完整示例:

package main

import "fmt"

//Interfaces
type Shaper interface {
	Area() float32
}

//Square
type Square struct {
	border float32
}

func (s *Square)Area() float32 {
	return s.border * s.border
}

//Rectangle
type Rectangle struct {
	width float32
	height float32
}

func (r *Rectangle)Area() float32 {
	return r.width * r.height
}

//Polymorphism
func PrintArea(s interface{}) {
	if v, ok := s.(Shaper); ok {
		fmt.Printf("The area is %v\n", v.Area())	
	}
}

func main() {
	s := new(Square)
	s.border = 2.0

	var ishaper Shaper = s  //assign a variable to interface variable
	fmt.Println(ishaper.Area())

	//tye assertion, get value from interface
	if v, ok := ishaper.(*Square); ok {
		fmt.Println(v)
	}

	//type assertion to test value implement a interface
	//type assertion is only applied on interface, so we cast the s to empty interface
	if v, ok := interface{}(s).(Shaper); ok {  
		fmt.Println(v.Area())
	}

	//Type switches
	switch ishaper.(type) {
	case *Square:
		fmt.Println("Square type")
	}

	r := &Rectangle{10.0, 15.0}
	//a interface variable is a pointer in essence, so can pass the variable r(a pointer)
	PrintArea(s)    
	PrintArea(r)
}

Go的多態也講完了。


免責聲明!

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



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