《Go語言聖經》學習筆記:6. 方法

6. 方法

6.1 方法的聲明

在函數聲明時,在其名字之前放上一個變量,即是一個方法。這個附加的參數會將該函數附加到這種類型上,即相當於爲這種類型定義了一個獨佔的方法。

示例:兩種Distance實現的效果是一樣的

type Point struct {
	X, Y float64
}

func Distance(p, q Point) float64 {
	return math.Hypot(p.X-q.X, p.Y-q.Y)
}

// 下面的p稱爲方法的接受器
func (p Point) Distance(q Point) float64  {
	return math.Hypot(p.X-q.X, p.Y-q.Y)
}

func main() {
	p := Point{1, 5}
	q := Point{5, 2}
	fmt.Println(Distance(p, q))
	fmt.Println(p.Distance(q))
}

在go語言中有一個好處就是:可以給同一個包內的任意命名類型定義方法,只要這個命名類型的底層類型。比如:數值、字符串、slice、map

示例,對一個切片定義方法

type Path []Point

func (p Path) Distance() float64 {
	sum := 0.0
	for i, _ := range p {
		if i>0 {
			sum += p[i].Distance(p[i-1])
		}
	}
	return sum
}

func main() {
	perim := Path{
		{1, 1},
		{5, 1},
		{5, 4},
		{1, 1},
	}
	fmt.Println(perim.Distance())    // 12
}

6.2 基於指針對象的方法

在函數中,如果我們要對一個對象進行修改,或者給函數傳參的參數對象內容過大,會選用傳遞指針的方式。因爲函數在傳遞的時候是進行值傳遞,也就是將一個參數裏邊的變量在另外一個內存地址重新拷貝一份,然後再在函數中使用,而指針變量的值是一個地址,所以本質上也是進行只拷貝,只不過拷貝了一個地址,編譯器可以通過這個地址去訪問參數對象。

同樣對於對象的方法,也可以實現基於指針對象的方法。

func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

使用的時候也是很簡單,就和上一小節的使用方法是一樣的。

但是細分起來有幾種情況:

  • 接收器的實際參數和其接收器的形式參數相同,比如兩者都是類型T或者都是類型*T
  • 接收器實參是類型T,但接收器形參是類型*T,這種情況下編譯器會隱式地爲我們取變量的地址
  • 接收器實參是類型*T,形參是類型T。編譯器會隱式地爲我們解引用,取到指針指向的實際變量

一句話總結就是無論什麼的變量是普通的類型還是指針類型,或者實現的方法的接收器是普通類型還是指針類型,都可以通過點.來訪問定義的方法

可以結合下面例子理解下:

type MyInt int

// 接受器i是一個指針類型
func (i *MyInt) pAdd(x int) MyInt {
	// 因爲x和i雖然底層變量是一樣的,但是其實是兩種不同的變量了,所以不能直接相加,詳見2.5類型
	return *i + MyInt(x)
}

// 接收器i只是一個普通類型
func (i MyInt) Add(x int) MyInt {
	return i + MyInt(x)
}

func main() {
	i := MyInt(20)
	pi := &i
	// 以下都正確
	fmt.Println(i.Add(1))		// 21
	fmt.Println(i.pAdd(2))		// 22
	fmt.Println(pi.Add(3))		// 23
	fmt.Println(pi.pAdd(4))		// 24
}

6.3 通過嵌入結構體來擴展類型

go語言中,嵌入結構體能夠達到像其他面向對象的編程語言一樣的繼承。

在下面代碼中結構體Point稱爲基類,通過匿名嵌入可以將Point嵌入到新的結構中,並且Point實現的方法能夠被ColoredPoint所繼承。關於嵌入結構體的其他一些細節可以看4.4節的結構體部分。

type Point struct{ X, Y float64 }

type ColoredPoint struct {
    Point
    Color color.RGBA
}

Point的方法繼承給了ColoredPoint,所以ColoredPoint類型能夠使用Distance函數

func main()  {
	red := color.RGBA{255, 0,0,1}
	green := color.RGBA{0, 255,0,1}
	cp1 := ColoredPoint{Point{1, 1}, red}
	cp2 := ColoredPoint{Point{4, 5}, green}

	fmt.Println(cp1.Distance(cp2.Point))
	fmt.Println(cp1.Distance(cp2)) // 錯誤
}

Distance函數是Point方法,參數也是Point類型,所以必須接受一個Point參數,否者會報錯。

當然,也可以爲ColoredPoint定製自己的Distance方法。

func (p *ColoredPoint) Distance(q *ColoredPoint) float64 {
	return p.Point.Distance(q.Point)
}

func main()  {
	red := color.RGBA{255, 0,0,1}
	green := color.RGBA{0, 255,0,1}
	cp1 := ColoredPoint{Point{1, 1}, red}
	cp2 := ColoredPoint{Point{4, 5}, green}

	fmt.Println(cp1.Distance(&cp2))				// 5
	fmt.Println(cp1.Point.Distance(cp2.Point))	// 5
	fmt.Println(cp1.Distance(cp2.Point))   		// 錯誤
}

上述代碼中,可以看到ColoredPoint擁有了自己的Distance方法,並且該方法可以接受*ColoredPoint的參數。但是原來的Point的Distance必須通過.Point.來訪問了。

6.4 方法值和方法表達式

定義一個新的變量,這個變量是一個函數的值,可以看作函數的別名,使用的時候可以當做函數一樣來使用。

方法值: 對一個已經聲明的類型對象A,使用另外一個變量B來代替其方法。其接受器依然是A,通常表示爲 B:=A.method

方法表達式: 當T是一個類型時,方法表達式可能會寫作T.f或者(*T).f,會返回一個函數"值",這種函數會將其第一個參數用作接收器,所以可以用通常(譯註:不寫選擇器)的方式來對其進行調用:通常表示爲 op:=T.f或者op:=(*T).f, 其中是(*T).f表示該方法的接收器是一個指針類型。

type Point struct {
	X, Y float64
}

// 下面的p稱爲方法的接受器
func (p *Point) Distance(q *Point) float64  {
	return math.Hypot(p.X-q.X, p.Y-q.Y)
}

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 *Point, q* Point) Point
	// 判斷要加還是減
	if add {
		// 方法表達式,會將第一個參數作爲接收器
		op = (*Point).Add
	} else {
		op = (*Point).Sub
	}
	for i := range path {
		path[i] = op(&path[i], &offset)
	}
}

func main()  {
	p1 := Point{1,1}
	p2 := Point{4, 5}
	p1Distance := p1.Distance   // 使用方法值
	// p1Distance是p1.Distance方法返回的一個值
	fmt.Println(p1Distance(&p2))

	perim := Path{
		{1, 1},
		{5, 1},
		{5, 4},
		{1, 1},
	}

	perim.TranslateBy(Point{1, 1}, true)
	fmt.Println(perim)
	perim.TranslateBy(Point{2, 2}, false)
	fmt.Println(perim)
}

6.5 封裝

一個對象的變量或者方法如果對調用方是不可見的話,一般就被定義爲“封裝”。Go使用命名時首字母大小寫來實現可見性。

封裝的好處:

  1. 因爲調用方不能直接修改對象的變量值,其只需要關注少量的語句並且只要弄懂少量變量的可能的值即可。
  2. 隱藏實現的細節,可以防止調用方依賴那些可能變化的具體實現,這樣使設計包的程序員在不破壞對外的api情況下能得到更大的自由。
  3. 是阻止了外部調用方對對象內部的值任意地進行修改。

本文主要參考:《Go語言聖經》


撩我?
搜索我的公衆號:Kyda
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章