C語言中文網 目錄

Go語言方法和接收器

Go 語言中的方法(Method)是一種作用于特定類型變量的函數。這種特定類型變量叫做接收器(Receiver)。

如果將特定類型理解為結構體或“類”時,接收器的概念就類似于其他語言中的 this 或者 self。

在 Go 語言中,接收器的類型可以是任何類型,不僅僅是結構體,任何類型都可以擁有方法。

提示

在面向對象的語言中,類擁有的方法一般被理解為類可以做的事情。在 Go 語言中“方法”的概念與其他語言一致,只是 Go 語言建立的“接收器”強調方法的作用對象是接收器,也就是類實例,而函數沒有作用對象。

為結構體添加方法

本節中,將會使用背包作為“對象”,將物品放入背包的過程作為“方法”,通過面向過程的方式和 Go 語言中結構體的方式來理解“方法”的概念。

1) 面向過程實現方法

面向過程中沒有“方法”概念,只能通過結構體和函數,由使用者使用函數參數和調用關系來形成接近“方法”的概念,代碼如下:
type Bag struct {
    items []int
}

// 將一個物品放入背包的過程
func Insert(b *Bag, itemid int) {
    b.items = append(b.items, itemid)
}

func main() {

    bag := new(Bag)

    Insert(bag, 1001)
}
代碼說明如下:
  • 第 1 行,聲明 Bag 結構,這個結構體包含一個整型切片類型的 items 的成員。
  • 第 6 行,定義了 Insert() 函數,這個函數擁有兩個參數,第一個是背包指針(*Bag),第二個是物品ID(itemid)。
  • 第 7 行,用 append() 將 itemid 添加到 Bag 的 items 成員中,模擬往背包添加物品的過程。
  • 第 12 行,創建背包實例 bag。
  • 第 14 行,調用 Insert() 函數,第一個參數放入背包,第二個參數放入物品 ID。

Insert() 函數將 *Bag 參數放在第一位,強調 Insert 會操作 *Bag 結構體。但實際使用中,并不是每個人都會習慣將操作對象放在首位。一定程度上讓代碼失去一些范式和描述性。同時,Insert() 函數也與 Bag 沒有任何歸屬概念。隨著類似 Insert() 的函數越來越多,面向過程的代碼描述對象方法概念會越來越麻煩和難以理解。

2) Go語言的結構體方法

將背包及放入背包的物品中使用 Go 語言的結構體和方法方式編寫:為 *Bag 創建一個方法,代碼如下:
type Bag struct {
    items []int
}

func (b *Bag) Insert(itemid int) {
    b.items = append(b.items, itemid)
}

func main() {

    b := new(Bag)

    b.Insert(1001)
}
第 5 行中,Insert(itemid int) 的寫法與函數一致。(b*Bag) 表示接收器,即 Insert 作用的對象實例。

每個方法只能有一個接收器,如下圖所示。


圖:接收器

第 13 行中,在 Insert() 轉換為方法后,我們就可以愉快地像其他語言一樣,用面向對象的方法來調用 b 的 Insert。

接收器——方法作用的目標

接收器的格式如下:

func (接收器變量 接收器類型) 方法名(參數列表) (返回參數) {
    函數體
}

對各部分的說明:
  • 接收器變量:接收器中的參數變量名在命名時,官方建議使用接收器類型名的第一個小寫字母,而不是 self、this 之類的命名。例如,Socket 類型的接收器變量應該命名為 s,Connector 類型的接收器變量應該命名為 c 等。
  • 接收器類型:接收器類型和參數類似,可以是指針類型和非指針類型。
  • 方法名、參數列表、返回參數:格式與函數定義一致。

接收器根據接收器的類型可以分為指針接收器、非指針接收器。兩種接收器在使用時會產生不同的效果。根據效果的不同,兩種接收器會被用于不同性能和功能要求的代碼中。

1) 理解指針類型的接收器

指針類型的接收器由一個結構體的指針組成,更接近于面向對象中的 this 或者 self。

由于指針的特性,調用方法時,修改接收器指針的任意成員變量,在方法結束后,修改都是有效的。

在下面的例子,使用結構體定義一個屬性(Property),為屬性添加 SetValue() 方法以封裝設置屬性的過程,通過屬性的 Value() 方法可以重新獲得屬性的數值。使用屬性時,通過 SetValue() 方法的調用,可以達成修改屬性值的效果。
package main

import "fmt"

// 定義屬性結構
type Property struct {
    value int  // 屬性值
}

// 設置屬性值
func (p *Property) SetValue(v int) {

    // 修改p的成員變量
    p.value = v
}

// 取屬性值
func (p *Property) Value() int {
    return p.value
}

func main() {

    // 實例化屬性
    p := new(Property)

    // 設置值
    p.SetValue(100)

    // 打印值
    fmt.Println(p.Value())

}
運行程序,輸出如下:
100

代碼說明如下:
  • 第 6 行,定義一個屬性結構,擁有一個整型的成員變量。
  • 第 11 行,定義屬性值的方法。
  • 第 14 行,設置屬性值方法的接收器類型為指針。因此可以修改成員值,即便退出方法,也有效。
  • 第 18 行,定義獲取值的方法。
  • 第 25 行,實例化屬性結構。
  • 第 28 行,設置值。此時成員變量變為 100。
  • 第 31 行,獲取成員變量。

2) 理解非指針類型的接收器

當方法作用于非指針接收器時,Go 語言會在代碼運行時將接收器的值復制一份。在非指針接收器的方法中可以獲取接收器的成員值,但修改后無效。

點(Point)使用結構體描述時,為點添加 Add() 方法,這個方法不能修改 Point 的成員 X、Y 變量,而是在計算后返回新的 Point 對象。Point 屬于小內存對象,在函數返回值的復制過程中可以極大地提高代碼運行效率,詳細過程請參考下面的代碼。
package main

import (
    "fmt"
)

// 定義點結構
type Point struct {
    X int
    Y int
}

// 非指針接收器的加方法
func (p Point) Add(other Point) Point {

    // 成員值與參數相加后返回新的結構
    return Point{p.X + other.X, p.Y + other.Y}
}

func main() {

    // 初始化點
    p1 := Point{1, 1}
    p2 := Point{2, 2}

    // 與另外一個點相加
    result := p1.Add(p2)

    // 輸出結果
    fmt.Println(result)

}
代碼輸出如下:
{3 3}

代碼說明如下:
  • 第 8 行,定義一個點結構,擁有 X 和 Y 兩個整型分量。
  • 第 14 行,為 Point 結構定義一個 Add() 方法。傳入和返回都是點的結構,可以方便地實現多個點連續相加的效果,例如:
    P4 := P1.Add( P2 ).Add( P3 )
  • 第 23 和 24 行,初始化兩個點 p1 和 p2。
  • 第 27 行,將 p1 和 p2 相加后返回結果。
  • 第 30 行,打印結果。

由于例子中使用了非指針接收器,Add() 方法變得類似于只讀的方法,Add() 方法內部不會對成員進行任何修改。

3) 指針和非指針接收器的使用

在計算機中,小對象由于值復制時的速度較快,所以適合使用非指針接收器。大對象因為復制性能較低,適合使用指針接收器,在接收器和參數間傳遞時不進行復制,只是傳遞指針。

示例:二維矢量模擬玩家移動

在游戲中,一般使用二維矢量保存玩家的位置。使用矢量運算可以計算出玩家移動的位置。本例子中,首先實現二維矢量對象,接著構造玩家對象,最后使用矢量對象和玩家對象共同模擬玩家移動的過程。

1) 實現二維矢量結構

矢量是數學中的概念,二維矢量擁有兩個方向的信息,同時可以進行加、減、乘(縮放)、距離、單位化等計算。在計算機中,使用擁有 X 和 Y 兩個分量的 Vec2 結構體實現數學中二維向量的概念。詳細實現請參考下面的代碼。
package main

import "math"

type Vec2 struct {
    X, Y float32
}

// 加
func (v Vec2) Add(other Vec2) Vec2 {

    return Vec2{
        v.X + other.X,
        v.Y + other.Y,
    }

}

// 減
func (v Vec2) Sub(other Vec2) Vec2 {

    return Vec2{
        v.X - other.X,
        v.Y - other.Y,
    }
}

// 乘
func (v Vec2) Scale(s float32) Vec2 {

    return Vec2{v.X * s, v.Y * s}
}

// 距離
func (v Vec2) DistanceTo(other Vec2) float32 {
    dx := v.X - other.X
    dy := v.Y - other.Y

    return float32(math.Sqrt(float64(dx*dx + dy*dy)))
}

// 插值
func (v Vec2) Normalize() Vec2 {
    mag := v.X*v.X + v.Y*v.Y
    if mag > 0 {
        oneOverMag := 1 / float32(math.Sqrt(float64(mag)))
        return Vec2{v.X * oneOverMag, v.Y * oneOverMag}
    }

    return Vec2{0, 0}
}
代碼說明如下:
  • 第 5 行聲明了一個 Vec2 結構體,包含兩個方向的單精度浮點數作為成員。
  • 第 10~16 行定義了 Vec2 的 Add() 方法。使用自身 Vec2 和通過 Add() 方法傳入的 Vec2 進行相加。相加后,結果以返回值形式返回,不會修改 Vec2 的成員。
  • 第 20 行定義了 Vec2 的減法操作。
  • 第 29 行,縮放或者叫矢量乘法,是對矢量的每個分量乘上縮放比,Scale() 方法傳入一個參數同時乘兩個分量,表示這個縮放是一個等比縮放。
  • 第 35 行定義了計算兩個矢量的距離。math.Sqrt() 是開方函數,參數是 float64,在使用時需要轉換。返回值也是 float64,需要轉換回 float32。
  • 第 43 行定義矢量單位化。

2) 實現玩家對象

玩家對象負責存儲玩家的當前位置、目標位置和速度。使用 MoveTo() 方法為玩家設定移動的目標,使用 Update() 方法更新玩家位置。在 Update() 方法中,通過一系列的矢量計算獲得玩家移動后的新位置,步驟如下。

① 使用矢量減法,將目標位置(targetPos)減去當前位置(currPos)即可計算出位于兩個位置之間的新矢量,如下圖所示。


圖:計算玩家方向矢量

② 使用 Normalize() 方法將方向矢量變為模為 1 的單位化矢量。這里需要將矢量單位化后才能進行后續計算,如下圖所示。


圖:單位化方向矢量

③ 獲得方向后,將單位化方向矢量根據速度進行等比縮放,速度越快,速度數值越大,乘上方向后生成的矢量就越長(模很大),如下圖所示。


圖:根據速度縮放方向

④ 將縮放后的方向添加到當前位置后形成新的位置,如下圖所示。


圖:縮放后的方向疊加位置形成新位置

下面是玩家對象的具體代碼:
package main

type Player struct {
    currPos   Vec2    // 當前位置
    targetPos Vec2    // 目標位置
    speed     float32 // 移動速度
}

// 移動到某個點就是設置目標位置
func (p *Player) MoveTo(v Vec2) {

    p.targetPos = v
}

// 獲取當前的位置
func (p *Player) Pos() Vec2 {
    return p.currPos
}

// 是否到達
func (p *Player) IsArrived() bool {

    // 通過計算當前玩家位置與目標位置的距離不超過移動的步長,判斷已經到達目標點
    return p.currPos.DistanceTo(p.targetPos) < p.speed
}

// 邏輯更新
func (p *Player) Update() {

    if !p.IsArrived() {

        // 計算出當前位置指向目標的朝向
        dir := p.targetPos.Sub(p.currPos).Normalize()

        // 添加速度矢量生成新的位置
        newPos := p.currPos.Add(dir.Scale(p.speed))

        // 移動完成后,更新當前位置
        p.currPos = newPos
    }

}

// 創建新玩家
func NewPlayer(speed float32) *Player {

    return &Player{
        speed: speed,
    }
}
代碼說明如下:
  • 第 3 行,結構體 Player 定義了一個玩家的基本屬性和方法。結構體的 currPos 表示當前位置,speed 表示速度。
  • 第 10 行,定義玩家的移動方法。邏輯層通過這個函數告知玩家要去的目標位置,隨后的移動過程由 Update() 方法負責。
  • 第 15 行,使用 Pos 方法實現玩家 currPos 的屬性訪問封裝。
  • 第 20 行,判斷玩家是否到達目標點。玩家每次移動的半徑就是速度(speed),因此,如果與目標點的距離小于速度,表示已經非常靠近目標,可以視為到達目標。
  • 第 27 行,玩家移動時位置更新的主要實現。
  • 第 29 行,如果已經到達,則不必再更新。
  • 第 32 行,數學中,兩矢量相減將獲得指向被減矢量的新矢量。Sub() 方法返回的新矢量使用 Normalize() 方法單位化。最終返回的 dir 矢量就是移動方向。
  • 第 35 行,在當前的位置上疊加根據速度縮放的方向計算出新的位置 newPos。
  • 第 38 行,將新位置更新到 currPos,為下一次移動做準備。
  • 第 44 行,玩家的構造函數,創建一個玩家實例需要傳入一個速度值。

3) 處理移動邏輯

將 Player 實例化后,設定玩家移動的最終目標點。之后開始進行移動的過程,這是一個不斷更新位置的循環過程,每次檢測玩家是否靠近目標點附近,如果還沒有到達,則不斷地更新位置,讓玩家朝著目標點不停的修改當前位置,如下代碼所示:
package main

import "fmt"

func main() {

    // 實例化玩家對象,并設速度為0.5
    p := NewPlayer(0.5)

    // 讓玩家移動到3,1點
    p.MoveTo(Vec2{3, 1})

    // 如果沒有到達就一直循環
    for !p.IsArrived() {

        // 更新玩家位置
        p.Update()

        // 打印每次移動后的玩家位置
        fmt.Println(p.Pos())
    }

}
代碼說明如下:
  • 第 8 行,使用 NewPlayer() 函數構造一個 *Player 玩家對象,并設移動速度為 0.5,速度本身是一種相對的和抽象的概念,在這里沒有單位,可以根據實際效果進行調整,達到合適的范圍即可。
  • 第 11 行,設定玩家移動的最終目標為 X 為 3,Y 為 1。
  • 第 14 行,構造一個循環,條件是沒有到達時一直循環。
  • 第 17 行,不停地更新玩家位置,如果玩家到達目標,p.IsArrived 將會變為 true。
  • 第 20 行,打印每次更新后玩家的位置。

本例中使用到了結構體的方法、構造函數、指針和非指針類型方法接收器等,讀者通過這個例子可以了解在哪些地方能夠使用結構體。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo