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