免費com域名注冊網站上海seo推廣整站
文章目錄
- 方法
- 方法聲明
- 基于指針對象的方法
- nil 也是合法的接收器類型
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 封裝
方法
今天我們來復習 Golang 語法中的“方法”,方法屬于某個「類型」,它屬于 Go 的語法糖,本質上是一個函數,使得這個類型可以像在調用它的“類方法”一樣來調用這個函數。
方法聲明
在聲明函數時,在函數名前面放上一個變量,這個函數就成為了變量所對應類型的方法,這個方法將成為該類型的獨占方法。一個方法聲明的例子如下:
type Point struct{ X, Y float64 }func Distance(p, q Point) float64 {return math.Hypot(q.X-p.X, q.Y-p.Y)
}func (p Point) Distance(q Point) float64 {return math.Hypot(q.X-p.X, q.Y-p.Y)
}
上述方法定義時,Point
類型的p
是方法的接收器(receiver),在 C++ 當中我們使用this
作為接收器,而在 Python 中我們使用self
作為接收器。在 Golang 當中,我們可以自定義接收器的名稱,《Go 語言圣經》當中給出的建議是使用類型的第一個字母。
調用方法的例子如下:
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call
可以看到,函數Distance
與Point
類型的Distance
方法不會產生沖突。
在 Go 當中,美中類型都有其方法的命名空間,我們在使用 Distance 這個名字的時候,不同的 Distance 調用指向了不同類型里的 Distance 方法。下例是一個更復雜的例子,它定義了一個Path
類型,它本質是[]Point
,我們進一步為它也定義一個Distance
方法:
// A Path is a journey connecting the points with straight lines.
type Path []Point
// Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {sum := 0.0for i := range path {if i > 0 {sum += path[i-1].Distance(path[i])}}return sum
}
兩個Distance
有不同的類型,但兩個方法之間沒有任何關系。調用新方法來計算三角形周長:
perim := Path{{1, 1},{5, 1},{5, 4},{1, 1},
}
fmt.Println(perim.Distance()) // "12"
基于指針對象的方法
在最開始我們提到,「方法」是 Golang 的語法糖,其底層本質上仍然是一個函數,方法的接收器將作為這個函數的第一個參數。因此,如果我們想要在方法當中修改接收器的值,或是這個方法的接收器占用的內存非常大,我們不希望在調用方法時進行值拷貝,此時就可以使用指針對象作為接收器:
func (p *Point) ScaleBy(factor float64) {p.X *= factorp.Y *= factor
}
方法的名字是(*Point).ScaleBy
,此處的括號是必須的,否則會被理解為*(Point.ScaleBy)
(函數值的指針)。
在真實開發(fā)環(huán)境中,一般約定如果Point
這個類型有「以指針作為接收器」的方法,那么Point
的所有方法必須有一個指針接收器,即使某些方法不需要指針接收器。此外,為了避免歧義,如果一個類型本身是一個指針的話(比如type P *int
),那么它不允許出現(xiàn)在接收器中。
想要調用指針類型方法(*Point).ScaleBy
,只需要提供一個Point
類型的指針并調用該方法即可。比如:
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
以上的寫法較為笨拙,因為需要我們顯式地將值轉為指針再調用指針接收器方法。 Go 存在一種語法糖,也就是如果一個值類型直接調用它的以指針作為接收器的方法,那么 Go 的編譯器會隱式地幫我們用這個值的指針來調用指針方法。比如:
p := Point{1, 2}
p.ScaleBy(3)
上述代碼中,編譯器隱式地幫我們用&p
調用ScaleBy
。這種簡寫方法僅適用于變量,因為變量的地址是確定的,如果Point
是某個 struct 的成員,或者是 slice 當中的元素,由于我們無法取到 struct 成員的地址,且 slice 底層的數組可能會修改從而導致地址改變,因此對于這類值,我們不能調用它們的以指針作為接收器的方法。臨時變量的內存地址也無法取到,因此也不能直接對臨時變量調用指針接收器方法:
Point{1, 2}.ScaleBy(2) // ? 不能對臨時變量調用指針接收器方法
此外,對于一個指針類型,如果它具有以值作為接收器的方法,那么這個指針也可以直接調用值接收器方法,Go 編譯器會隱式地幫我們解指針引用。
總結一下,在每一個合法的方法調用表達式中,存在以下三種情況,都是可以正常運行的:
第一種情況是方法調用者的類型與其方法接收器的類型匹配,即二者都是值T
或指針*T
:
Point{1, 2}.Distance(q) // Distance 是以值為接收器的方法
pptr.ScaleBy(2) // pptr 是 Point 的指針, ScaleBy 是以指針為接收器的方法
第二種是:如果接收器的實參,即方法的調用者類型是值T
,但接收器的形參類型是*T
,這種情況下編譯器會隱式地幫助我們取實參的地址:
p.ScaleBy(2) // implicit (&p)
第三種是:如果接收器的實參是指針*T
,形參是T
,編譯器會隱式地幫助我們解引用,取到指針實際指向的變量值:
pptr.Distance(q) // implicit (*pptr)
nil 也是合法的接收器類型
就像函數允許 nil 值的指針作為參數一樣,方法本質上也是函數,因此該類型的指針接收器方法可以通過 nil 指針來調用。
下例是一個鏈表求和的例子,該例通過調用鏈表類型的 Sum 方法來對鏈表進行求和,由于 nil 指針也可以調用對應類型的方法,因此當鏈表到尾時,nil 仍然可以繼續(xù)調用 Sum 方法,只不過這次調用會在方法的邏輯中判斷出當前指針為 nil,返回 0:
// An IntList is a linked list of integers.
// A nil *IntList represents the empty list.
type IntList struct {Value intTail *IntList
}
// Sum returns the sum of the list elements.
func (list *IntList) Sum() int {if list == nil {return 0}return list.Value + list.Tail.Sum()
}
通過嵌入結構體來擴展類型
下例定義了一個 ColoredPoint 類型,它將 Point 類型作為嵌入加入到了結構體的定義當中:
type Point struct { X, Y int64 }
type ColoredPoint struct {PointColor color.RGBA
}
基于結構體內嵌的方式,我們可以直接認為嵌入的字段就是 ColoredPoint 自己的字段,在使用時完全不需要指出 Point,ColoredPoint 本身就可以直接訪問 X 和 Y 成員:
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X)
cp.Point.Y = 2
fmt.Println(cp.Y)
對于 Point 中的方法,我們也有類似的用法,可以把 ColoredPoint 類型當作接收器來調用 Point 里的方法,即使 ColoredPoint 沒有聲明這些方法:
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)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
Point 類的方法也被引入了 ColoredPoint,故內嵌可以使我們定義字段特別多的復雜類型,可以先將字段按小類型分組,然后定義小類型的方法,之后再把它們組合起來。
需要注意的是,Point 嵌入在了 ColoredPoint 當中,這種關系不是繼承,也不是子類與父類之間的關系。ColoredPoint “has” a Point,所以在調用 Distance 方法時,方法傳入的實參必須顯式地選擇 ColoredPoint 當中的 Point 對象,否則編譯器會報錯:compile error: cannot use q (ColoredPoint) as Point
。
ColoredPoint 不是 Point,但基于內嵌,它擁有一個 Point,并且從擁有的 Point 中引入了 Distance 和 ScaleBy 方法。從具體的實現(xiàn)角度來說,內嵌字段會指導編譯器隱式地額外生成方法來對已有的方法進行封裝,等價于:
func (p ColoredPoint) Distance(q Point) float64 {return p.Point.Distance(q)
}func (p *ColoredPoint) ScaleBy(factor float64) {p.Point.ScaleBy(factor)
}
因此,即使我們通過 ColoredPoint 對象調用內嵌的 Point 的方法,在 Point 的方法中我們也無法訪問 ColoredPoint 的成員。
在類型中內嵌的匿名字段也可能是一個命名類型的指針,這種情況下字段和方法會間接地引入到當前的類型中。添加這一層間接關系讓我們可以共享通用的結構并動態(tài)地改變對象之間的關系。下例的 ColoredPoint 聲明內嵌了一個*Point
指針:
type ColoredPoint struct {*PointColor 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 // Now, p and q share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // {2, 2}, {2, 2}
一個 struct 可以定義多個匿名字段,例如:
type ColoredPoint struct {Pointcolor.RGBA
}
這種類型的值會擁有 Point 和 RGBA 類型的所有方法,以及直接定義在 ColoredPoint 中的方法。當編譯器解析一個選擇器到方法時,比如p.ScaleBy
,它會首先去找直接定義在這個類型當中的ScaleBy
方法,然后找 ColoredPoint 內嵌字段引入的方法,然后去 Point 和 RGBA 的內嵌字段繼續(xù)找引入的方法,一直遞歸向下尋找直到找到為止。如果選擇器有二義性的話,編譯器會報錯,比如你在同一級里有兩個同名的方法。
下例展示了一個基于 Go 實現(xiàn)的非常簡單的 Cache 的 Demo:
var cache = struct {sync.Mutexmapping map[string]string
}{mapping: make(map[string]string),
}func Lookup(key string) string {cache.Lock()v := cache.mapping[key]cache.Unlock()return v
}
該例中,sync.Mutex
字段被嵌入到了 struct 當中,故其 Lock 和 Unlock 方法也被引入到了 cache 對應的匿名結構類型,使得我們可以非常方便地進行加鎖和解鎖操作。
方法值和方法表達式
我們之前使用過的p.Distance
(注意,不帶括號,此時是方法的值)叫做“選擇器”,選擇器會返回一個方法“值”,即一個將方法(Point.Distance
)綁定到特定接收器變量的函數。這個函數調用時不需要指定接收器,因為已經在p.Distance
中指定p
為接收器了,此時只需要傳入參數即可:
p := Point{1, 2}
q := Point{4, 6}distanceFromP := p.Distance // p.Distance 獲取方法值, 綁定到 distanceFromP 上
// ?? 此時已經選擇 p 為接收器了
fmt.Println(distanceFromP(q)) // "5"
當T
是一個類型時,方法表達式可能會寫作T.f
或(*T).f
,此時返回的是一個函數的“值”,這種函數會將第一個傳入的參數作為接收器,例如:
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance
fmt.Println(distance(p, q))
這一點不難理解,因為我在本篇開頭已經提到,Golang 的 Method 實際上是一種語法糖,它本質上是一個以方法調用者為第一個實參的函數。因此,類型的方法值就是函數本身,即:
distance := Point.Distance
// ?? distance 本身是一個有兩個形參的函數, 這兩個形參的類型都是 Point
封裝
“一個對象的變量或方法對調用方不可見”被定義為“封裝”,詳細來說也可以稱為“信息隱藏”。封裝是面向對象的特性之一。
Go 只有一種可見性手段,那就是大寫首字母的標識符會從定義它們的包中被導出,小寫字母則不會導出。這種限制包內成員可見性的方式同樣適用于 struct 或一個類型的方法。基于上述原因,如果我們想對一個對象進行封裝,那么它必須是一個 struct。
下例定義了一個 IntSet 類型,盡管它只有一個字段,但是由于我們想要對它進行封裝,所以必須把這個單獨的字段定義在 struct 當中:
type IntSet struct {words []uint64
} // words 是非導出的, 用戶無法直接訪問// ?? 如果我們直接定義為
type IntSet []uint64 // 該方法會使得其他包的用戶直接改變 IntSet 底層的 []uint64
這種基于名字的手段使得在 Golang 語言層面最小的封裝單元是 package,而不是其他語言一樣的類型。一個 struct 類型的字段對同一個包內的所有代碼都有可見性,無論你的代碼是寫在一個函數還是一個方法里。
封裝提供了三個優(yōu)點:
- 調用方不能直接修改對象的變量值,而修改只能通過包的發(fā)布人員對外提供的接口來完成;
- 隱藏了實現(xiàn)的細節(jié),防止調用方以來那些可能變化的具體實現(xiàn),這使得設計包的程序員可以在不破壞對外 API 的情況下獲得更多開發(fā)上的自由;
- 阻止外部調用方對對象內部的值任意地進行修改。
Go 的編程風格不禁止導出字段,一旦導出,就無法保證在 API 兼容的前提下去除對已經導出字段的導出。