1. 程式人生 > 實用技巧 >go基礎5-方法

go基礎5-方法

方法

go中同時有函式和方法.方法是包含了接收者( receiver)的函式,接收者可以是命名型別或結構體型別的值或指標.所有給定型別的方法屬於該型別的方法集.go中的方法是一種有繫結行為的特殊函式,繫結行為用於標識函式所屬.

方法宣告

方法:在函式名稱前增加變數的宣告方式.這個附加的引數會將該函式附加到這種型別上,即為變數型別定義一個獨佔的方法.方法上的變數必須屬於一種型別,不能使用原始型別.

type Point struct{ X, Y float64 }

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

// 方法 表示Distance是Point的一個方法
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// 宣告型別
type I int

func (i I) Sum(q I) int {
    return int(i) + int(q)
}

上面變數p稱為方法的接收器;接收器命名建議使用變數的首字母,簡短一致;
方法呼叫方式:使用所屬方式

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", 函式呼叫
fmt.Println(p.Distance(q))  // "5", 方法呼叫

這種所屬方式使用形式叫做選擇器.它會根據變數後的名稱選擇相應的方法或欄位來使用.
對於給定的型別,其內部方法的命名必須唯一,不同型別可以使用相同方法名;

基於指標物件的方法

函式呼叫使用指標可以避免參數拷貝,資料修改更加直接;&取地址;*取表示式值

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

一般會約定如果Point類有一個指標作為接收器的方法,那麼所有Point的方法都必須有一個指標接收器,及時哪些不需要指標接收器的函式.????

方法宣告中,不允許型別名本身就是指標的接收器.

type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type

呼叫指標型別方法(*Point).ScaleBy,只需要提供一個Point型別指標即可:

// 方法一
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r)
// 方法二
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p)
// 方法三
q := Point{1, 2}
(&q).ScaleBy(2)
fmt.Println(q)
//方法四
p.ScaleBy(2)

方法四中go會隱式用&p去呼叫方法,但它只適用於變數.
不管接收器是指標型別還是非指標型別,都是可以通過指標/非指標型別呼叫,編譯器會幫你做型別轉換;
指標非指標型別的選擇要看物件本身是否比較大,是否需要原值修改;
宣告為非指標變數時,呼叫會產生一次拷貝;
指標型別接收器會指向同一記憶體地址,操作同一個資料;

嵌入結構體來擴充套件型別

type ColoredPoint struct {
    Point
    Color color.RGBA
}

通過組合方式來擴充套件資料型別,呼叫時也跟定義在自身上一致.如:cp.X=cp.Point.X.但是嵌入結構體只是型別組合不是繼承關係.
可以定義匿名欄位,用於直接使用型別的方法.

方法值和方法表示式

方法呼叫可以拆分為兩個步驟:方法繫結和呼叫

p := Point{1, 2}
q := Point{4, 6}
p.Distance(q)
<===>
distanceFromP := p.Distance        // method value
distanceFromP(q)      // "5"

p.Distance會返回將方法和特定接收器變數繫結的函式,後續使用傳入函式引數即可,不用在指定接收器.

方法表示式中型別T呼叫可以省略接收器變數:

distance := Point.Distance
fmt.Println(distance(p, q))

類似於靜態變數.

bit陣列

bit陣列適用於集合間的並集,交集操作.通常使用無符號數或稱為"字"的slice來表示.
每個元素的每一位都表示集合裡的一個值.每個元素:指位數的分組數.如1byte=8bit,13/8代表有兩個byte,就是有兩個元素;元素裡每一位都會進行值儲存;當集合的第i位被設定時,我們才說這個集合包含元素i.

// 值新增
func (s *IntSet) Add(x int) {
    word, bit := x/64, uint(x%64)
    for word >= len(s.words) {
        s.words = append(s.words, 0)
    }
    s.words[word] |= 1 << bit
}
// 取值
func (s *IntSet) String() string {
    var buf bytes.Buffer
    buf.WriteByte('{')
    for i, word := range s.words {
        if word == 0 {
            continue
        }
        for j := 0; j < 64; j++ {
            if word&(1<<uint(j)) != 0 {
                if buf.Len() > len("{") {
                    buf.WriteByte(' ')
                }
                fmt.Fprintf(&buf, "%d", 64*i+j)
            }
        }
    }
    buf.WriteByte('}')
    return buf.String()
}

封裝

封裝:呼叫方不可見物件中的變數或方法.go中只有一種可見性控制手段:大小寫識別符號;這種限制同樣適用於struct或型別的方法;

type IntSet struct {
    words []uint64
}

type IntSet []uint64

上面兩種寫法的底層實現一致,但是欄位的外部可見性不同.但是struct的內部欄位是包內可見,無論你定義在函式還是方法裡.
封裝的優點:

  • 降低外部呼叫方使用難度
  • 隱藏實現細節
  • 阻止外部呼叫方對物件內部的值任意地修改