Go: 方法


方法

在面向對象編程的編程思想里,類、對象、方法是基礎。類比到Golang中

// 類
type Point struct {X, Y int}
// 對象
p := Point{1, 2}
// 方法 即綁定在struct上的函數
// ...

方法聲明

方法和函數類似,區別在於它在函數名前多了一個參數(接收器),用來將方法綁定在參數對應的類型上

package main

import (
	"fmt"
	"math"
)

type Point struct {
	X, Y float64
}

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

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

每個類型都有自己的命令空間,在同一個命名空間里不能有相同名稱的方法和成員

type Line struct {
	Start  Point
	End    Point
	// Length float64
    // 如果取消上面這行的注釋 編譯報錯:type Line has both field and method named Length
}

func (L Line) Length() float64 {
	return L.Start.Distance(L.End)
}

func main() {
    p := Point{1, 2}
	q := Point{4, 6}
	fmt.Println(p.Distance(q))  // 5
	line := Line{p, q}
	fmt.Println(line.Length())  // 5
}

不同類型的命名空間是獨立的,可以在不同類型中使用相同名字的方法

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])
		}
	}
	return sum
}

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

指針接收者的方法

函數調用實參變量是以復制一份的方式傳遞的,如果我們想在函數中進行更改會很麻煩;如果一個實參太大,我們希望避免復制整個實參,我們可以通過指針的方式傳遞變量地址。這也同樣使用與方法

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

func main() {
    p := Point{1, 2}
    p.ScaleBy(200)
    fmt.Printf("%+v", p) // {X:200 Y:400}
}

習慣上,如果Point上任何一個方法綁定指針接收者,那么所有的Point方法都應該使用指針接收者。方法的接收者只能是類型(Point)或者類型指針(Point)。

為了防止混淆,不允許本身是指針的類型進行方法聲明:

type p *int
func (p) f() {/*...*/}  // 編譯錯誤:非法的接收者類型

以下幾種寫法都是合法的:

// case1
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r)  // {2, 4}

// case2
p1 := Point{1, 2}
pptr := &p1
pptr.ScaleBy(2)
fmt.Println(p1)  // {2, 4}

// case3
p2 := Point{1, 2}
(&p2).ScaleBy(2)
fmt.Println(p2)  // {2, 4}

注意,不能對一個不能取地址的Point接收者參數調用*Point方法,因為無法獲得臨時變量的地址。

Piont{1,2}.ScaleBy(2)  // 編譯錯誤

反過來,指針類型(*Point)變量,它是可以調用Point類型的方法

type Point struct{}

func (p *Point) PtrFunc() {}
func (p Point) Func()     {}

func main() {
	p := Point{}
	ptr := &Point{}
	ptr.PtrFunc()
	ptr.Func()

	Point{}.Func()
	Point{}.PtrFunc() // 編譯錯誤:cannot call pointer method on Point literal

	p.Func()
	p.PtrFunc() // 編譯器做了隱式轉換
}

疑惑:如果所有類型T方法的接收者是T自己(而非*T),那么復制它的實例是安全的;調用方法的時候必須進行一次復制。但是任何方法的接收者是指針的情況下,應該避免復制T的實例,因為這么做可能會破壞原本的數據。

nil是一個合法的接收

方法的接收者可以是nil

// *IntList的類型nil代表空列表
type IntList struct {
	Value int
	Next  *IntList
}

func (list *IntList) Sum() int {
	if list == nil {
		return 0
	}
	return list.Value + list.Next.Sum()
}

func main() {
	a1 := IntList{1, nil}
	a2 := IntList{2, &a1}
	a3 := IntList{3, &a2}

	fmt.Println(a3.Sum())  // 6

}

當定義一個類型允許為nil作為接收者,應該在文檔中顯式地表明

通過結構體內嵌組成類型

在一個結構體A中嵌套另一個結構體B,則結構體A可以調用結構體B的方法

import (
	"fmt"
	"image/color"
	"math"
)

type Point struct{ X, Y float64 }

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

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

type ColoredPoint struct {
	Point
	Color color.RGBA
}

func main() {
	var cp ColoredPoint
	cp.X = 1
	fmt.Println(cp.Point.X)  // 1

	p := ColoredPoint{Point{1, 1}, color.RGBA{255, 0, 0, 255}}
	q := ColoredPoint{Point{5, 4}, color.RGBA{0, 0, 255, 255}}

	//fmt.Println(p.Distance(q)) // 編譯錯誤:cannot use q (type ColoredPoint) as type Point in argument to p.Point.Distance
	fmt.Println(p.Distance(q.Point))  // 5
	p.ScaleBy(2)
	q.ScaleBy(2)
	fmt.Println(p.Distance(q.Point))  // 10
}

ColoredPoint類型內嵌了Point類型,它可以調用PointDistanceScaleBy方法。也可以直接訪問Point的成員變量。

實際上,內嵌字段會告訴編譯生成額外的包裝方法來調用 Point聲明的方法:

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

func (p *ColoredPoint) ScaleBy(factor float64) {
    p.Point.ScaleBy(factor)
}

匿名字段可以是指向命名類型的指針,字段和方法間接地來自於所指向的對象。這可以讓我們共享通用的結構以及使對象之間的關系更加動態、多樣化。
我們將ColoredPoint的匿名字段改成指針類型,在對比一下和上面非指針類型的區別:

import (
	"fmt"
	"image/color"
	"math"
)

type Point struct{ X, Y float64 }

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

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

type ColoredPoint struct {
	*Point
	Color color.RGBA
}

func main() {
	var cp ColoredPoint
	cp.Point = &Point{}  // 匿名指針類型的默認值是nil,必須對其進行初始化
	cp.Point.X = 1  // 如果沒有上面的那一行,執行報錯:panic: runtime error: invalid memory address or nil pointer dereference
	fmt.Println(cp.Point.X) // 1

	p := ColoredPoint{&Point{1, 1}, color.RGBA{255, 0, 0, 255}}  // 初始化是Point傳地址
	q := ColoredPoint{&Point{5, 4}, color.RGBA{0, 0, 255, 255}}

	//fmt.Println(p.Distance(q)) // 編譯錯誤:cannot use q (type ColoredPoint) as type Point in argument to p.Point.Distance
	fmt.Println(p.Distance(*q.Point)) // 5  實參傳遞時,要轉化為值
	p.ScaleBy(2)
	q.ScaleBy(2)
	fmt.Println(p.Distance(*q.Point)) // 10
}

結構體類型也可以由多個匿名字段

type ColoredPoint struct {
    Point
    color.RGBA
}

p := ColoredPoint{Point{1, 1}, color.RGBA{255, 0, 0, 255}}

當調用p.ScaleBy方法時,它會先查找ColoredPoint有沒有聲明這個方法,如果沒有,再從其內嵌對象Pointcolor.RGBA上查找,再從Pointcolor.RGBA的內嵌對象上查找。當同一個查找級別中有同名方式時,編譯器報錯;

type A struct {}
func (a A) Func() {}
type B struct {}
func (b B) Func() {}
type C struct {
    A
    B
}

func main() {
    c := C{}
    c.Func()  // 編譯錯誤:ambiguous selector c.Func
}

方法只能在命名的類型(比如Point)和指向他們指針(*Point)中聲明,但內嵌幫助我們能夠在未命名的結構體類型中聲明方法。

方法變量與表達式

我們可以將方法賦予一個方法變量,方法變量是一個函數,本質上會綁定到接收者上,可以理解為方法的引用,方法變量只要傳遞實參就可以調用成功。

a := Point{1, 2}
b := Point{4, 6}
distanceFromA := a.Distance  // 方法變量
fmt.Println(distanceFromA(b))  // 5
origin := Point{0, 0}
fmt.Println(distanceFromA(origin)) // 2.23606797749979

scaleA := a.ScaleBy  // 方法變量
scaleA(2)
fmt.Println(a)  // {2, 4}

方法表達式與方法變量相似,區別是方法變量是由將類型聲明的變量的方法賦予的,而方法表達式是有類型的方法賦予的,有點繞,看一下例子:

a := Point{1, 2}
b := Point{4, 6}
distanceFromA := a.Distance  // 方法變量 由a的方法賦予
distance := Point.Distance // 方法表達式 由Point類型的方法賦予

方法的接收者會替換成函數的第一個參數

fmt.Println(distanceFromA(b))  // 5 方法變量
fmt.Println(distance(a, b))  // 5 方法表達式 
fmt.Printf("%T\n", distance) // func(Point, Point) float64

// scale := Point.ScaleBy // 編譯報錯:nvalid method expression Point.ScaleBy (needs pointer receiver: (*Point).ScaleBy
scale := (*Point).ScaleBy
scale(&a, 2)
fmt.Println(a)
fmt.Printf("%T\n", scale)  // func(*Point, float64)

封裝

控制變量和方法不能通過對象訪問(私有),即為封裝。Go語言中通過控制命名的大小寫來實現,首字母大寫的標識符可以被導出,小寫的就不可以。因此,可以通過結構體來是實現封裝,向調用者隱藏重要的數據和實現細節,防止非法更改。

type IntSet struct {
    words []uint64
}

type IntSet2 []uint64

對比兩個類型,IntSet將實際存儲數據的slice封裝成了一個不可訪問字段,IntSet2也將數據存儲在slice,但它是可以被訪問的,我們可以同*s在其他包中訪問、更改。

思考:結構體里的字段一定都要封裝起來,不讓使用者看到嗎?

封裝的優點:

  • Go語言封裝的單元是包而不是類型,包內的函數和方法對結構體的字段是可見的
  • 實現細節可以對包的使用方屏蔽,方便設計者靈活改變
  • 防止使用者非法更改結構體內的變量

封裝的缺點:

  • 需要設計者編寫很多的方法來實現對字段的讀取和更新,因為調用者無法自助。

封裝並不總會需要的,要結合實際的適用場景區別對待。


免責聲明!

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



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