在面對物件程式設計(OOP),我們常把某個物件實現的具體行為的函式稱作方法。例如 C++中A類的某個函式實現了某種行為,我們就叫做 A 的方法。在 golang 中如果要定義一個方法,只需要在函式宣告時,在函式名前加上某個變數,即該變數實現了某個方法。
方法宣告
type Point struct{
X, Y float64
}
//按照傳統方法,我們可能會按照下面的方式來寫
func Distance(p, q Point) float64 {
return math.Hypot(q.X - p.X, q.Y - p.Y)
}
//但在 go 語言中則是這樣
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
複製程式碼
引數 p 我們一般稱為方法接收器(receiver), 按照早期 OOP 的說法, 呼叫一個方法稱為"向一個物件傳送訊息", 這裡的 p 類似C++中的 this 指標, python 中的 self.
方法呼叫是這樣的:
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(p.Distance(q))
複製程式碼
p.Distance
這種表示式叫做選擇器, 因為編譯器會自動選擇適合 p 這個物件的 Distance 方法來執行.可以理解為 C++中每個類都有對應自己的方法, 儘管名字相同,但都有自己所屬的名稱空間.
比如我們可以再定義一個同樣的 Distance 方法:
type Path []Point
func (path Path)Distance() float64 {
sum := 0.0
for i := range path {
if i > 0 {
sum += path[i-1].Distance(path[i]) //此處呼叫了上面 Point 的 Distance 方法
}
}
}
複製程式碼
基於指標物件的方法
我們知道, go 語言中函式呼叫的引數值都是值拷貝, 如果需要更改入參的值, 則需要用指標來處理.同樣, 宣告方法時,也可以將接收器定義為指標型別. 例如:
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
複製程式碼
但是需要注意的是, 一般約定如果某個類(比如 Point)有一個指標作為接收器的方法, 那麼所有的方法都必須定義為指標接收器. 這裡只是為了展示兩種方法.
另外,如果型別本身是一個指標, 則不允許其出現在接收器中, 例如:
type P *int //P是一個 int 型的指標
func (P) f() {...} //此處會編譯出錯
複製程式碼
在 go 語言中呼叫指標的方法,有多種寫法:
r := &Point{1,2}
p := Point{1,2}
pptr := &p
r.ScaleBy(2)
pptr.ScaleBy(2)
(&p).ScaleBy(2)
複製程式碼
幸運的是, 我們不需要牢記這麼多寫法, go 語言中編譯器會幫忙做型別轉換, 無論接收器是指標型別還是非指標型別,都可以呼叫.
總結一下共三種方式:
//1. 接收器實參與接收器形參相同
//此處為形參為 Point, 如果寫 Point{1,2}.ScaleBy(2)則會報錯
//原因是Point{1,2}是一個臨時變數, 無法獲取記憶體地址
Point{1,2}.Distance(q)
//pptr為一個指標,實參也是指標
pptr.ScaleBy(2)
//2. 接收器形參為型別 T, 實參為型別*T
//此處p為型別 T, 呼叫的*T 的方法, 編譯器隱式轉換了
p.ScaleBy(2)
//3. 接收器形參為*T, 實參為型別 T
//同樣編譯器做隱式轉換
pptr.Distance(q)
複製程式碼
含有匿名結構體的方法
在上一篇文章中提到過匿名結構體, 如果某個結構體A中含有匿名結構體B, 則在呼叫 B 結構體的方法func()
時,可以直接使用A.func()
.例如:
type Point struct { X, Y float64}
type ColoredPoint struct {
Point
Color color.RGBA
}
//Point 實現了 Distance 方法, 則
red := color.RGBA{255,0,0,255}
blue := color.RGBA{0,0,255,255}
var p = ColoredPoint{Point{1,1}, red}
var q = ColoredPoint{Point{5,4}, blue}
fmt.Println(p.Distance(q.Point))//"5"
p.ScaleBy(2)
複製程式碼
注意:一個 ColoredPoint 並不是一個 Point, 可以看成它“has a” Point, 並且有從 Point 類引入的 Distance 和 ScaleBy 方法。
也可以在結構體中宣告一個匿名指標,用來共享結構記憶體並動態的改變物件之間的關係。下例:
type ColoredPoint struct {
*Point
Color color.RGBA
}
p:= ColoredPoint{&Point{1,1}, red}
q:= ColoredPoint{&Point{5,4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point // p、q現在共享記憶體
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point)
複製程式碼
方法值和方法表示式
之前的例子p.Distance
叫做選擇器,對於 Point.Distance
則稱為方法表示式,它們返回的“值”成為方法值。
大多數情況,人們習慣於使用選擇器來呼叫一個方法。使用方法表示式呼叫方法的話,函式的第一個引數會被用作接收器。例如:
p := Point{1,2}
q := Point{4,6}
distance := Point.Distance //method expression
fmt.Println(distance(p,q)) //"5"
fmt.Printf("%T\n", distance) //func(Point, Point) float64
複製程式碼
方法表示式的好處在於當根據一個變數來決定呼叫同一個型別的哪個函式時,就可以來使用方法表示式。比如說:
type Point struct{X, Y float64}
func (p Point) Add(q Point) Point {return Point{p.X + q.X, p.Y + q.Y}}
func (p Point) Sub(q Point) Point {return Point{p.X - q.X, p.Y - q.Y}}
type Path []Point
func (path Path)TranslateBy(offset Point, add bool) {
var op func(p, q Point) Point
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
//call either path[i].Add(offset) or path[i].Sub(offset).
path[i] = op(path[i], offset)
}
}
複製程式碼
?的例子,變數 op 代表加法或者減法,二者都是屬於 Point 型別,使用方法表示式就會變的很簡潔。
Period.?
更多文章歡迎關注公眾號:程式設計師 Morgan。
聚焦程式人生,關注自我管理,不給自己人生設限!