Go 学习笔记(36)— 基于Go方法的面向对象(封装、继承、多态)
Go 面向?qū)ο缶幊痰娜筇匦?#xff1a;封裝、繼承和多態(tài)。
- 封裝:隱藏對象的屬性和實現(xiàn)細節(jié),僅對外提供公共訪問方式
- 繼承:使得子類具有父類的屬性和方法或者重新定義、追加屬性和方法等
- 多態(tài):不同對象中同種行為的不同實現(xiàn)方式
Go 語言的結(jié)構(gòu)體(struct)和其他語言的類(class)有同等的地位,但 Go 語言放棄了包括繼
承在內(nèi)的大量面向?qū)ο筇匦?#xff0c;只保留了組合(composition)這個最基礎(chǔ)的特性。
例如,我們要定義一個矩形類型
type Rect struct {x, y float64width, height float64
}
然后我們定義成員方法 Area() 來計算矩形的面積:
func (r *Rect) Area() float64 {return r.width * r.height
}
在定義了 Rect 類型后,該如何創(chuàng)建并初始化 Rect 類型的對象實例呢?這可以通過如下幾種方法實現(xiàn):
rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}
在 Go 語言中,未進行顯式初始化的變量都會被初始化為該類型的零值,例如 bool 類型的零值為 false,int 類型的零值為 0,string 類型的零值為空字符串。
在 Go 語言中沒有構(gòu)造函數(shù)的概念,對象的創(chuàng)建通常交由一個全局的創(chuàng)建函數(shù)來完成,以 NewXXX來命名,表示“構(gòu)造函數(shù)”:
func NewRect(x, y, width, height float64) *Rect {return &Rect{x, y, width, height}
}
詳細參見:https://blog.csdn.net/wohu1104/article/details/106202892 中的結(jié)構(gòu)體初始化章節(jié)。
1. 封裝
package mainimport "fmt"type data struct {val int
}func (p_data *data) set(num int) {p_data.val = num
}func (p_data *data) show() {fmt.Println(p_data.val)
}func main() {p := &data{4}p.set(10)p.show()
}
或者
package mainimport ("fmt"
)// 矩形結(jié)構(gòu)體
type Rectangle struct {Length intWidth int
}// 計算矩形面積
func (r *Rectangle) Area() int {return r.Length * r.Width
}func main() {r := Rectangle{4, 2}// 調(diào)用 Area() 方法,計算面積fmt.Println(r.Area())
}
2. 繼承
確切地說,Go 語言也提供了繼承,但是采用了組合的文法,所以我們將其稱為匿名組合,Go 語言的繼承方式采用的是匿名組合的方式。
package maintype Base struct {Name string
}func (base *Base) Foo() { ... }
func (base *Base) Bar() { ... }type Foo struct {Base...
}func (foo *Foo) Bar() {foo.Base.Bar()...
}
以上代碼定義了一個 Base 類(實現(xiàn)了 Foo() 和 Bar() 兩個成員方法),然后定義了一個 Foo 類,該類從Base 類“繼承”并改寫了 Bar() 方法(該方法實現(xiàn)時先調(diào)用了基類的 Bar() 方法)。
在“派生類” Foo 沒有改寫“基類” Base 的成員方法時,相應(yīng)的方法就被“繼承”,例如在上面的例子中,調(diào)用foo.Foo() 和調(diào)用 foo.Base.Foo() 效果一致。
此外,在 Go 語言中你還可以隨心所欲地修改內(nèi)存布局,如:
type Foo struct {... // 其他成員Base
}
這段代碼從語義上來說,和上面給的例子并無不同,但內(nèi)存布局發(fā)生了改變。“基類” Base 的數(shù)據(jù)放在了“派生類” Foo 的最后。
另外,在 Go 語言中,你還可以以指針方式從一個類型“派生”
type Foo struct {*Base...
}
這段 Go 代碼仍然有“派生”的效果,只是 Foo 創(chuàng)建實例的時候,需要外部提供一個 Base 類實例的指針。
另外,我們必須關(guān)注一下接口組合中的名字沖突問題,比如如下的組合:
type X struct {Name string
}type Y struct {XName string
}
組合的類型和被組合的類型都包含一個 Name 成員,會不會有問題呢?
答案是否定的。所有的 Y 類型的 Name 成員的訪問都只會訪問到最外層的那個 Name 變量,X.Name 變量相當(dāng)于被隱藏起來了。
那么下面這樣的場景呢:
type Logger struct {Level int
}type Y struct {*LoggerName string*log.Logger
}
顯然這里會有問題。因為之前已經(jīng)提到過,匿名組合類型相當(dāng)于以其類型名稱(去掉包名部分)作為成員變量的名字。按此規(guī)則,Y 類型中就相當(dāng)于存在兩個名為 Logger 的成員,雖然類型不同。
因此,我們預(yù)期會收到編譯錯誤。有意思的是,這個編譯錯誤并不是一定會發(fā)生的。假如這兩個 Logger在定義后再也沒有被用過,那么編譯器將直接忽略掉這個沖突問題,直至開發(fā)者開始使用其中的某個 Logger。
Woman結(jié)構(gòu)體中包含匿名字段 Person,那么 Person 中的屬性也就屬于 Woman 對象。
package mainimport "fmt"type Person struct {name string
}type Man struct {Personsex string
}func main() {man := Man{Person{"wohu"}, "男"}fmt.Println(man.name) // wohufmt.Println(man.sex) // 男
}
package mainimport "fmt"type parent struct {val int
}type child struct {parentnum int
}func main() {c := child{parent{1}, 2}fmt.Println(c.num)fmt.Println(c.val)
}
3. 多態(tài)
在面向?qū)ο笾?#xff0c;多態(tài)的特征為:不同對象中同種行為的不同實現(xiàn)方式。在 Go 語言中可以使用接口實現(xiàn)這一特征。
package mainimport ("fmt"
)// 正方形
type Square struct {side float32
}// 長方形
type Rectangle struct {length, width float32
}// 接口 Shaper
type Shaper interface {Area() float32
}// 計算正方形的面積
func (sq *Square) Area() float32 {return sq.side * sq.side
}// 計算長方形的面積
func (r *Rectangle) Area() float32 {return r.length * r.width
}func main() {
// 創(chuàng)建并初始化 Rectangle 和 Square 的實例,由于這兩個實例都實現(xiàn)了接口中的方法,
//所以這兩個實例,都可以賦值給接口 Shaper r := &Rectangle{10, 2}q := &Square{10}// 創(chuàng)建一個 Shaper 類型的數(shù)組shapes := []Shaper{r, q}// 迭代數(shù)組上的每一個元素并調(diào)用 Area() 方法for n, _ := range shapes {fmt.Println("矩形數(shù)據(jù): ", shapes[n])fmt.Println("它的面積是: ", shapes[n].Area())}
}/*
矩形數(shù)據(jù): &{10 2}
它的面積是: 20
圖形數(shù)據(jù): &{10}
它的面積是: 100
*/
package mainimport "fmt"type act interface {write()
}type xiaoming struct {
}type xiaobai struct {
}func (xm *xiaoming) write() {fmt.Println("xiaoming write")
}func (xf *xiaobai) write() {fmt.Println("xiaobai write")
}func main() {
/*
> 接口特點:
> + 接口只有方法聲明、沒有實現(xiàn),沒有數(shù)據(jù)字段
> + 接口可以匿名嵌入其它接口,或者嵌入到結(jié)構(gòu)中> 接口是用來定義行為的類型,這些被定義的行為不由接口直接實現(xiàn),
> 而是由用戶定義的類型實現(xiàn),**一個實現(xiàn)了這些方法的具體類型是這個接口類型的實例。****如果用戶定義的類型實現(xiàn)了某個接口類型聲明的一組方法,
那么這個用戶定義的類型的值就可以賦給這個接口類型的值。
這個賦值會把用戶定義的類型存入接口類型的值。**
*/var w actxm := xiaoming{}xb := xiaobai{}w = &xmw.write()w = &xbw.write()
}
輸出結(jié)果:
xiaoming write
xiaobai write
或者以下代碼,將結(jié)構(gòu)體初始化封裝為函數(shù)。
// 創(chuàng)建初始化函數(shù),初始化結(jié)構(gòu)體對象,返回為接口對象
func NewXiaoming(xm xiaoming) act {return &xm
}// 創(chuàng)建初始化函數(shù),初始化結(jié)構(gòu)體對象,返回為接口對象
func NewXiaobai(xb xiaobai) act {return &xb
}func main() {m := NewXiaoming(xiaoming{})m.write()b := NewXiaobai(xiaobai{})b.write()
}
Go 中的接口可以說是方法特征的集合表達。要實現(xiàn)其接口,只需要實現(xiàn)接口中的所有方法即可。
package mainimport ("fmt"
)type animal interface {run()breath()
}type dog struct {legs intnose string
}type fish struct {fin stringgill string
}func (d dog) run() {fmt.Printf("Dog runs with %d legs\n", d.legs)
}func (d dog) breath() {fmt.Printf("Dog breath with %s\n", d.nose)
}func (f fish) run() {fmt.Printf("Fish runs with %s\n", f.fin)
}func (f fish) breath() {fmt.Printf("Fisn breath with %s\n", f.gill)
}func behavior(an animal) {an.run()an.breath()
}func main() {d := dog{legs: 4,nose: "nose",}f := fish{fin: "fin",gill: "gill",}behavior(d)behavior(f)
}
輸出結(jié)果:
Dog runs with 4 legs
Dog breath with nose
Fish runs with fin
Fisn breath with gill
程序首先定義了一個 animal 接口,它有 run()、breath() 兩個方法。接著定義了兩種類型,分別是 dog 和 fish,顯然這兩種類型的動物都擁有 animal 動物類的兩個方法,因而各自實現(xiàn)了它們。最后一個帶有 animal 參數(shù)的 behavior 函數(shù)的出現(xiàn)很好地詮釋了面向?qū)ο笾卸鄳B(tài)的構(gòu)造形式。
總結(jié)
以上是生活随笔為你收集整理的Go 学习笔记(36)— 基于Go方法的面向对象(封装、继承、多态)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Go 中 time.Parse 报错:y
- 下一篇: Go 学习笔记(37)— 标准命令(go