《Go 語言程序設計》讀書筆記 (三) 方法

方法

方法聲明

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

package geometry

import "math"

type Point struct{ X, Y float64 }

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

// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

上面的代碼裏那個附加的參數p,叫作方法的接收器(receiver)。在Go語言中,咱們並不會像其它語言那樣用this或者self做爲接收器;咱們能夠任意的選擇接收器的名字。建議是可使用其類型的第一個字母,好比這裏使用了Point的首字母p。express

在方法調用過程當中,接收器參數通常會在方法名以前出現。這和方法聲明是同樣的,都是接收器參數在方法名字以前。下面是例子:編程

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,可是卻沒有發生衝突。第一個Distance的調用實際上用的是包級別的函數geometry.Distance,而第二個則是使用剛剛聲明的Point,調用的是Point類下聲明的Point.Distance方法。這種p.Distance的表達式叫作選擇器,由於他會選擇合適的對應p這個對象的Distance方法來執行。api

由於每種類型都有其方法的命名空間,咱們在用Distance這個名字的時候,不一樣的Distance調用指向了不一樣類型裏的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.0
    for i := range path {
        if i > 0 {
            sum += path[i-1].Distance(path[i])
        }
    }
    return sum
}

Path是一個命名的slice類型,而不是Point那樣的struct類型,然而咱們依然能夠爲它定義方法。兩個Distance方法有不一樣的類型。他們兩個方法之間沒有任何關係,儘管Path的Distance方法會在內部調用Point.Distance方法來計算每一個鏈接鄰接點的線段的長度。app

Go和不少其它的面向對象的語言不太同樣。在Go語言裏,咱們能夠爲一些簡單的數值、字符串、slice、map來定義一些附加行爲很方便。方法能夠被聲明到任意類型,只要不是一個指針或者一個interface(接收者不能是一個指針類型,可是它能夠是任何其餘容許類型的指針)。函數

對於一個給定的類型,其內部的方法都必須有惟一的方法名,可是不一樣的類型卻能夠有一樣的方法名,好比咱們這裏Point和Path就都有Distance這個名字的方法;因此咱們沒有必要非在方法名以前加類型名來消除歧義,好比PathDistance。在上面兩個對Distance名字的方法的調用中,編譯器會根據方法的名字以及接收器來決定具體調用的是哪個函數。this

指針對象的方法

當調用一個函數時,會對其每個參數值進行拷貝,若是一個函數須要更新一個變量,或者函數的其中一個參數實在太大咱們但願可以避免進行這種默認的拷貝,這種狀況下咱們就須要用到指針了。對應到咱們這裏用來更新接收器的對象的方法,當這個接受者變量自己比較大時,咱們就能夠用其指針而不是對象來聲明方法,以下:編碼

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

這個方法的名字是(*Point).ScaleBy。這裏的括號是必須的;沒有括號的話這個表達式可能會被理解爲*(Point.ScaleBy)設計

  • 在現實的程序裏,通常會約定若是Point這個類有一個指針做爲接收器的方法,那麼全部Point的方法都必須有一個指針接收器,即便是那些並不須要這個指針接收器的函數。咱們在這裏打破了這個約定只是爲了展現一下兩種方法的異同而已。
  • 無論你的method的receiver是指針類型仍是非指針類型,都是能夠經過指針/非指針類型進行調用的,編譯器會幫你作類型轉換。

    p := Point{1, 2}
    pptr := &p
    p.ScaleBy(2) // implicit (&p)
    pptr.Distance(q) // implicit (*pptr)
  • 在聲明一個method的receiver是指針仍是非指針類型時,你須要考慮兩方面的內部,第一方面是這個對象自己是否是特別大,若是聲明爲非指針變量時,調用會產生一次拷貝;第二方面是若是你用指針類型做爲receiver,那麼你必定要注意,這種指針類型指向的始終是一塊內存地址,就算你對其進行了拷貝(指針調用時也是值拷貝,只不過指針的值是一個內存地址,因此在函數裏的指針與調用方的指針變量是兩個不一樣的指針可是指向了相同的內存地址)。

Nil也是一個合法的接收器類型

  • 就像一些函數容許nil指針做爲參數同樣,方法理論上也能夠用nil指針做爲其接收器,尤爲當nil對於對象來講是合法的零值時,好比map或者slice。在下面的簡單int鏈表的例子裏,nil表明的是空鏈表:

    // An IntList is a linked list of integers.
    // A nil *IntList represents the empty list.
    type IntList struct {
        Value int
        Tail  *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()
    }

    當你定義一個容許nil做爲接收器的方法的類型時,在類型前面的註釋中指出nil變量表明的意義是頗有必要的,就像咱們上面例子裏作的這樣。

經過嵌入結構體來擴展類型

  • 下面的ColoredPoint類型

    import "image/color"
    
    type Point struct{ X, Y float64 }
    
    type ColoredPoint struct {
        Point
        Color color.RGBA
    }

    內嵌可使咱們在定義ColoredPoint時獲得一種句法上的簡寫形式,並使其包含Point類型所具備的一切字段和方法。

    var cp ColoredPoint
    cp.X = 1
    fmt.Println(cp.Point.X) // "1"
    cp.Point.Y = 2
    fmt.Println(cp.Y) // "2"
    
    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"

    經過內嵌結構體可使咱們定義字段特別多的複雜類型,咱們能夠將字段先按小類型分組,而後定義小類型的方法,以後再把它們組合起來。

  • 內嵌字段會指導編譯器去生成額外的包裝方法來委託已經聲明好的方法,和下面的形式是等價的:

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

    當Point.Distance被第一個包裝方法調用時,它的接收器值是p.Point,而不是p,固然了,在Point類的方法裏,你是訪問不到ColoredPoint的任何字段的。

  • 方法只能在命名類型(像Point)或者指向類型的指針上定義,可是多虧了內嵌,咱們給匿名struct類型來定義方法也有了手段。這個例子中咱們爲變量起了一個更具表達性的名字:cache。由於sync.Mutex類型被嵌入到了這個struct裏,其Lock和Unlock方法也就都被引入到了這個匿名結構中了,這讓咱們可以以一個簡單明瞭的語法來對其進行加鎖解鎖操做。

    var cache = struct {
        sync.Mutex
        mapping map[string]string
    }{
        mapping: make(map[string]string),
    }
    
    
    func Lookup(key string) string {
        cache.Lock()
        v := cache.mapping[key]
        cache.Unlock()
        return v
    }

方法值和方法表達式

  • 咱們常常選擇一個方法,而且在同一個表達式裏執行,好比常見的p.Distance()形式,實際上將其分紅兩步來執行也是可能的。p.Distance叫做「選擇器」,選擇器會返回一個方法"值"->一個將方法(Point.Distance)綁定到特定接收器變量的函數。由於已經在前文中指定過了,這個函數能夠不經過指定其接收器便可被調用,只要傳入函數的參數便可:

    p := Point{1, 2}
    q := Point{4, 6}
    
    distanceFromP := p.Distance        // method value
    fmt.Println(distanceFromP(q))      // "5"
    var origin Point                   // {0, 0}
    fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
    
    scaleP := p.ScaleBy // method value
    scaleP(2)           // p becomes (2, 4)
    scaleP(3)           //      then (6, 12)
    scaleP(10)          //      then (60, 120)
  • 當T是一個類型時,方法表達式可能會寫做T.f或者(*T).f,會返回一個函數"值",這種函數會將其第一個參數用做接收器,因此能夠用一般(譯註:不寫選擇器)的方式來對其進行調用:

    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"
    
    scale := (*Point).ScaleBy
    scale(&p, 2)
    fmt.Println(p)            // "{2 4}"
    fmt.Printf("%T\n", scale) // "func(*Point, float64)"
    // 譯註:這個Distance其實是指定了Point對象爲接收器的一個方法func (p Point) Distance(),
    // 但經過Point.Distance獲得的函數須要比實際的Distance方法多一個參數,
    // 即其須要用第一個額外參數指定接收器,後面排列Distance方法的參數。
  • 當你根據一個變量來決定調用同一個類型的哪一個函數時,方法表達式就顯得頗有用了。你能夠根據選擇來調用接收器各不相同的方法。下面的例子,變量op表明Point類型的addition或者subtraction方法,Path.TranslateBy方法會爲其Path數組中的每個Point來調用對應的方法:

    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)
        }
    }

封裝

  • 一個對象的變量或者方法若是對調用方是不可見的話,通常就被定義爲「封裝」。封裝有時候也被叫作信息隱藏,同時也是面向對象編程最關鍵的一個方面。
  • Go語言只有一種控制可見性的手段:大寫首字母的標識符會從定義它們的包中被導出,小寫字母的則不會。
  • 這種基於名字的手段使得在語言中最小的封裝單元是package,而不是像其它語言同樣的Class。一個struct類型的字段對同一個包的全部代碼都有可見性,不管你的代碼是寫在一個函數仍是一個方法裏。
  • 封裝提供了三方面的優勢。首先,由於調用方不能直接修改對象的變量值,其只須要關注少許的語句而且只要弄懂少許變量的可能的值便可。

    第二,隱藏實現的細節,能夠防止調用方依賴那些可能變化的具體實現,這樣使設計包的程序員在不破壞對外的api狀況下能獲得更大的自由。

    封裝的第三個優勢也是最重要的優勢,是阻止了外部調用方對對象內部的值任意地進行修改。由於對象內部變量只能夠被同一個包內的函數修改,因此包的做者可讓這些函數確保對象內部的一些值的不變性。好比下面的Counter類型容許調用方來增長counter變量的值,而且容許將這個值reset爲0,可是不容許隨便設置這個值(譯註:由於壓根就訪問不到):

    type Counter struct { n int }
    func (c *Counter) N() int     { return c.n }
    func (c *Counter) Increment() { c.n++ }
    func (c *Counter) Reset()     { c.n = 0 }
  • 只用來訪問或修改內部變量的函數被稱爲setter或者getter,例子以下,好比log包裏的Logger類型對應的一些函數。在命名一個getter方法時,咱們一般會省略掉前面的Get前綴。這種簡潔上的偏好也能夠推廣到各類類型的前綴好比Fetch,Find或者Lookup。

    package log
    type Logger struct {
        flags  int
        prefix string
        // ...
    }
    func (l *Logger) Flags() int
    func (l *Logger) SetFlags(flag int)
    func (l *Logger) Prefix() string
    func (l *Logger) SetPrefix(prefix string)
  • Go的編碼風格不由止直接導出字段。固然,一旦進行了導出,就沒有辦法在保證API兼容的狀況下去除對其的導出,因此在一開始的選擇必定要通過深思熟慮而且要考慮到包內部的一些不變量的保證,還有將來可能的變化。
相關文章
相關標籤/搜索