Go part 5 結構體,方法與接收器

結構體

結構體定義python

結構體的定義只是一種內存佈局的描述(至關因而一個模板),只有當結構體實例化時,纔會真正分配內存空間app

結構體是一種複合的基本類型,經過關鍵字 type 定義爲 自定義 類型後,使結構體更便於使用ide

定義一個簡單的結構體:函數

  • 類型名(Point)在同一個包內不能重複
  • 字段名(X,  Y)必須惟一
type Point struct {
    X int
    Y int
}

 

同類型的變量也能夠寫在一行:佈局

type Color struct {
    R, G, B byte
}

 

實例性能

結構體實例與實例間的內存是徹底獨立的,this

能夠經過多種方式實例化結構體,根據實際需求選用不一樣的寫法spa

1)基本的實例化形式(不推薦)設計

結構體是一種值類型,能夠像整型,字符串同樣,以 var 開頭的方式聲明結構體便可完成實例化3d

基本實例化格式:

var ins T
其中,T 爲結構體類型,ins 爲結構體的實例

var ins *T
建立指針類型的結構體,T 爲結構體指針類型

Demo

func main(){
    type Person struct{
        Name string
        Age int
    }

    var p Person
    fmt.Printf("%T\n", p)
    
    p.Name = "johny"
    p.Age = 12
    fmt.Println(p.Name, p.Age)
}

運行結果:
main.Person
johny 12

 

用聲明的方式建立指針類型的結構體,而後進行賦值會觸發 panic(空指針引用)

func main(){
    type Person struct{
        Name string
        Age int
    }

    var p1 *Person
    // var p1 *Person = new(Person)
    (*p1).Name = "anson"
    (*p1).Age = 13
    fmt.Println((*p1).Name, (*p1).Age)
}

運行結果:
panic: runtime error: invalid memory address or nil pointer dereference
View Code 

 

2)建立指針類型的結構體(推薦使用

Go語言中,可使用 new 關鍵字對值類型(包括結構體,整型,字符串等)進行實例化,獲得指針類型

ins := new(T)

其中,T 爲結構體類型,ins 爲指針類型 *T

Demo(定義並實例化一個遊戲玩家信息的結構體)

func main(){
    type Player struct{
        Name string
        HealthPoint int
        MagicPoint int
    }
    player := new(Player)
    fmt.Printf("%T\n", player)

    player.Name = "johny"
    player.HealthPoint = 100
    player.MagicPoint = 100

    fmt.Println(player.Name, player.HealthPoint, player.MagicPoint)
}

運行結果:
*main.Player
johny 100 100

  

3)使用 鍵值對 初始化結構體(實例化時直接填充值)

每一個鍵對應結構體中的一個字段,值對應字段中須要初始化的值

鍵值對的填充是可選的,不須要初始化的字段能夠不在初始化語句塊中體現(字段的默認值,是字段類型的默認值,例如:整數是0,字符串是 "",布爾是 false,指針是 nil 等)

實例化結構體

player := Player{
    Name: "johny",
    HealthPoint: 100,
    MagicPoint: 100,
}

其中,player是結構體實例,Player是結構體類型名,中間是鍵值對(字段名: 初始值)

 

實例化指針類型的結構體

player := &Player{
    Name: "johny",
    HealthPoint: 100,
    MagicPoint: 100,
}

 

結構體的嵌套(遞歸)

結構體成員中只能包含結構體的指針類型,包含非指針類型會引發編譯錯誤(invalid recursive type 'People')

func main(){
    type People struct{
        Name string
        Child *People
    }

    relation := People{
        Name: "grandPa",
        Child: &People{
            Name: "father",
            Child: &People{
                Name: "me",
            },
        },
    }
    
    // 與上面等效
    me := People{
        Name: "me",
    }

    father := People{
        Name: "father",
        Child: &me,
    }

    grandPa := People{
        Name: "grandPa",
        Child: &father,
    }

    fmt.Printf("%T\n%v\n", relation, relation.Child.Child.Name)
    fmt.Printf("%T\n%v\n", grandPa, grandPa.Child.Child.Name)
}

運行結果:
main.People
me
main.People
me

 

匿名結構體

func main(){
    ins := struct{
        Name string
        Age int
    }{
        Name: "johny",
        Age: 12,
    }

    fmt.Printf("%T\n", ins)
    fmt.Println(ins.Name, ins.Age)
} 

運行結果:
struct { Name string; Age int }
johny 12

  

模擬構造函數 初始化結構體

若是使用結構體來描述貓的特性,那麼根據貓的名稱與顏色能夠有不一樣的類型,那麼可使用不一樣名稱與顏色能夠構造不一樣貓的實例:

type Cat struct{
    Name string
    Color string
}

func NewCatByName(name string) *Cat {
    return &Cat{
        Name: name,
    }
}

func NewCatByColor(color string) *Cat {
    return &Cat{
        Color: color,
    }
}

func main(){
    cat1 := NewCatByName("johny")
    cat2 := NewCatByColor("white")
    fmt.Printf("%T\n%T\n", cat1, cat2)
    fmt.Printf("%v\n%v\n", cat1.Name, cat2.Color)
}

運行結果:
*main.Cat
*main.Cat
johny
white

  

帶有 繼承關係 的結構體的構造與初始化

貓是基本結構體(只有姓名和顏色)

黑貓繼承自貓,是子結構體(不只有姓名和顏色,還有技能)

使用不一樣的兩個構造函數分別構造貓與黑貓兩個結構體實例:

// 貓的結構體
type Cat struct{
    Name string
    Color string
}

// 構造貓的函數
func NewCat(name, color string) *Cat {
    return &Cat{
        Name: name,
        Color: color,
    }
}

// 黑貓的結構體(繼承了貓,增長了技能字段)
type BlackCat struct{
    Cat
    Skill string
}

// 構造黑貓的函數(不能用)
//func NewBlackCat(name, color, skill string) *BlackCat {
//    return &BlackCat{
//        Name: name,
//      Color: color,
//      Skill: skill,
//    }
//}

// 構造黑貓的函數
func NewBlackCat(name, color, skill string) *BlackCat {
    blackCat := &BlackCat{}
    blackCat.Name = name
    blackCat.Color = color
    blackCat.Skill = skill
    return blackCat
}

func main(){
    cat := NewCat("tom", "white")
    blackCat := NewBlackCat("blackTom", "black", "climb tree")
    fmt.Printf("%T\n%T\n", cat, blackCat)
    fmt.Printf("%v\n%v\n", cat.Name, cat.Color)
    fmt.Printf("%v\n%v\n%v\n", blackCat.Name, blackCat.Color, blackCat.Skill)
}

運行結果:
*main.Cat
*main.BlackCat
tom
white
blackTom
black
climb tree

Cat 結構體相似於面向對象中的「基類」。BlackCat 嵌入 Cat 結構體,相似於面向對象中的「派生」。實例化時,BlackCat 中的 Cat 也會一併被實例化

 

結構體匿名字段

上面的 黑貓 與 貓 的繼承關係中,定義黑貓字段的時候,就用到了匿名字段:

// 黑貓的結構體(繼承了貓,增長了技能字段)
type BlackCat struct{
    Cat
    Skill string
}

1)匿名的結構體字段:

  • 能夠直接訪問其成員變量:上述繼承例子中的:blackCat.Name;也可使用詳細的字段一層層的進行訪問(字段名就是它的類型名 Cat)

2)匿名的基本類型字段:

type Data struct {
    int
    float32
    bool
}

func main(){
    var data Data
    data.int = 100
    fmt.Println(data.int, data.float32, data.bool)
}

運行結果:
100 0 false

一個結構體中只能有一個同類型的匿名字段,不須要擔憂結構體字段重複問題 

 

方法與接收器

方法(method)是一種做用於特定類型的函數,這種特定類型叫作接收器(receiver)(目標接收器)

若是將特定類型理解爲結構體或「類」時,接收器的概念就至關因而實例,也就是其它語言中的 this 或 self

接收器的類型能夠是任何類型,不單單是結構體,任何類型均可以擁有方法

 

爲結構體添加方法

需求說明:使用揹包做爲「對象」,將物品放入揹包的過程做爲「方法」,經過面向過程的方式和結構體的方式來解釋「方法」的概念

1)面向過程方式:

type Bag struct{
    items []string
}

func put(bag *Bag, item string) {
    bag.items = append(bag.items, item)
}

func main(){
    var bag *Bag = new(Bag)
    var item string = "foods"
    put(bag, item)
    fmt.Println(bag.items)
}

運行結果:
[foods]

  

2)結構體方法:

爲 *Bag 建立一個方法,(bag *Bag) 表示接收器,即 put 方法做用的對象實例

type Bag struct{
    items []string
}

func (b *Bag) put(item string) {
    b.items = append(b.items, item)
}

func main(){
    var bag *Bag = new(Bag)
    var item string = "foods"
    bag.put(item)
    fmt.Println(bag.items)
}

運行結果:
[foods]

 

結構體方法的繼承 

模擬面向對象的設計思想(人和鳥的特性)

type Flying struct {}
type Walkable struct {}

func (f Flying) fly(){
    fmt.Println("can fly")
}

func (w Walkable) walk(){
    fmt.Println("can walk")
}

type Person struct {
    Walkable
}

type Bird struct {
    Flying
    Walkable
}

func main(){
    var p *Person = new(Person)
    fmt.Printf("%T\n", p)
    p.walk()

    var b *Bird= new(Bird)
    fmt.Printf("%T\n", b)
    b.fly()
    b.walk()
}

運行結果:
*main.Person
can walk
*main.Bird
can fly
can walk

 

爲任意類型添加方法

由於結構體也是一種類型,給其它類型添加方法和給結構體添加方法同樣

給基本類型添加方法:

type myInt int

func (a *myInt) set(num int){
    *a = myInt(num)
}

func main(){
    var a myInt
    a.set(66)
    fmt.Printf("%T %v\n", a, a)
}

運行結果:
66

  

time 包中的基本類型方法:

time.Second 的類型是 Duration,而 Duration 實際是一個 int64 的類型

對於 Duration 類型有一個 String() 方法,能夠將 Duration 的值轉爲字符串

func main(){
    var a string = (time.Second*2).String()
    var b time.Duration = time.Second*2
    fmt.Printf("%T %v\n", a, a)
    fmt.Printf("%T %v\n", b, b)
}

運行結果:
string 2s
time.Duration 2s

 

接收器

每一個方法只能有一個接收器,以下圖:

接收器的格式以下:

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

各部分說明:

  • 接收器變量:命名,官方建議使用接收器類型的第一個小寫字母,例如,Socket 類型的接收器變量應該爲 s,Connector 類型應該命名爲 c
  • 接收器類型:與參數相似,能夠是指針類型和非指針類型,兩種接收器會被用於不一樣性能和功能要求的代碼中(須要作更新操做時,用指針類型)
  • 方法名、參數列表、返回參數:與函數定義相同

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

更接近於面向對象中的 this 或 self;因爲指針的特性,調用方法時,修改接收器指針的任意成員變量,在方法結束後,修改都是有效的

Demo:接收一個結構體的指針,並作修改

// 定義屬性結構
type Person struct{
    name string
}

// 設置name
func (p *Person) setName (name string){
    p.name = name
}
// 獲取name
func (p *Person) getName()(name string){
    return p.name
}

func main(){
    var person *Person = new(Person)
    person.setName("johny")
    name := person.getName()
    fmt.Println(name)
}

 

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

當方法做用於非指針接收器時,會在代碼運行時將接收器的值複製一份;能夠獲取接收器的成員值,但修改後無效

Demo:定義一個空間座標(二維)的結構體,接收非指針的結構體,兩點進行相加

type Point struct{
    x int
    y int
}

func (p1 Point) add(p2 Point) Point{
    return Point{p1.x + p2.x, p1.y + p2.y}
}

func main(){
    var p1 Point = Point{1,1}
    var p2 Point = Point{1,2}

    p := p1.add(p2)
    fmt.Printf("(%d, %d)\n", p.x, p.y)
}

運行結果:
(2, 3)

 

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

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

 

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

在遊戲中,通常使用二維矢量保存玩家的位置,使用矢量計算能夠計算出玩家移動的位置,下面的 demo 中,首先實現二維矢量對象,接着構造玩家對象,最後使用矢量對象和玩家對象共同模擬玩家移動的過程

1)實現二維矢量結構

矢量是數據中的概念,二維矢量擁有兩個方向的信息,同時能夠進行加、減、乘(縮放)、距離、單位化等計算

在計算機中,使用擁有 x 和 y 兩個份量的 Vecor2 結構體實現數學中二維向量的概念,以下:

package main

import "math"

type Vector struct {
    x float32
    y float32
}

// 座標點操做的方法
func (v1 Vector) add(v2 Vector) Vector {
    return Vector{v1.x + v2.x, v1.y + v2.y}
}

func (v1 Vector) sub(v2 Vector) Vector {
    return Vector{v1.x - v2.x, v1.y - v2.y}
}

func (v1 Vector) multi(speed float32) Vector {
    return Vector{v1.x * speed, v1.y * speed}
}

// 計算距離
func (v1 Vector) distanceTo(v2 Vector) float32 {
    dx := v1.x - v2.x
    dy := v1.y - v2.y
    distance := math.Sqrt(float64(dx*dx + dy*dy))
    return float32(distance)
}

// 矢量單位化
func (v1 Vector) normalize() Vector {
    mag := v1.x * v1.x + v1.y * v1.y
    if mag > 0 {
        oneOverMag := 1 / float32(math.Sqrt(float64(mag)))
        return Vector{v1.x * oneOverMag, v1.y * oneOverMag}
    } else {
        return Vector{0, 0}
    }
}
Vector

 

2)實現玩家對象

玩家對象負責存儲玩家的當前位置、目標位置和移動速度,使用 moveTo() 爲玩家設定目的地座標,使用 update() 更新玩家座標

package main

type Player struct {
    currentVector Vector
    targetVector Vector
    speed float32
}

// 初始化玩家,設置速度
func newPlayer(speed float32) Player {
    return Player{speed: speed}
}

// 設置目標位置
func (p *Player) moveTo(v Vector) {
    p.targetVector = v
}

// 獲取當前位置
func (p Player) posision() Vector {
    return p.currentVector
}

// 是否到達目標位置
func (p Player) isArrived() bool {
    return p.currentVector.distanceTo(p.targetVector) < p.speed
}

// 更新玩家位置
func (p *Player) update() {
    // 使用矢量減法,將目標位置 targetVector 減去當前位置 currentVector,便可得出移動方向的新矢量
    directionVector := p.targetVector.sub(p.currentVector)
    // 矢量單位化
    normalizeVector := directionVector.normalize()
    // 計算 x, y 方向上改變的距離
    pointChange := normalizeVector.multi(p.speed)
    // 玩家新的座標位置
    newVector := p.currentVector.add(pointChange)
    // 更新玩家座標
    p.currentVector = newVector
}
player

更新座標稍微複雜一些,須要經過矢量計算得到玩家移動後的新位置,步驟以下:

  1. 使用矢量減法,將目標位置(targetPos)減去當前位置(currPos)便可計算出位於兩個位置之間的新矢量
  2. 使用 normalize() 方法將方向矢量變爲模爲 1 的單位化矢量
  3. 而後用單位化矢量乘以玩家的速度,就能獲得玩家每次分別在 x, y 方向上移動的長度
  4. 將目標當前位置的座標與移動的座標相加,獲得新位置的座標,並作修改

 

3)主程序

玩家移動是一個不斷更新位置的循環過程,每次檢測玩家是否靠近目標點附近,若是尚未到達,則不斷地更新位置,並打印出玩家的當前位置,直到玩家到達終點

package main

import "fmt"

func main(){
    // 建立玩家,設置玩家速度
    var p Player = newPlayer(0.2)
    fmt.Println(p.speed)
    // 設置玩家目標位置
    p.moveTo(Vector{2, 2})
    p.currentVector = Vector{1, 3}
    fmt.Println(p.targetVector)

    for !p.isArrived() {
        // 更新玩家座標位置
        p.update()
        // 打印玩家位置
        fmt.Println(p.posision())
        // 一秒更新一次
        time.Sleep(time.Second)
    }

    fmt.Println("reach destination~")
}
View Code
  1. 將 Player 實例化,設定玩家終點座標,當前座標
  2. 更新玩家位置
  3. 每次移動後,打印玩家的位置座標
  4. 延時 1 秒(便於觀察效果)

實例(python版本)

抽空寫了個python版本,增強理解

# coding=utf-8
import math
import time


# 座標類
class Vector(object):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    # 相加
    def add(self, vector):
        self.x += vector.x
        self.y += vector.y

    # 相減
    def sub(self, vector):
        x = self.x - vector.x
        y = self.y - vector.y
        return Vector(x, y)

    # 相乘
    def multi(self, speed):
        self.x *= speed
        self.y *= speed
        return self

    # 計算距離
    def distance(self, vector):
        dx = self.x - vector.x
        dy = self.y - vector.y
        return math.sqrt(dx ** 2 + dy ** 2)

    # 矢量單位化
    def normalize(self):
        mag = self.x ** 2 + self.y ** 2
        if mag > 0:
            one_over_mag = 1 / math.sqrt(mag)
            vector = Vector(x=self.x * one_over_mag, y=self.y * one_over_mag)
        else:
            vector = Vector()
        return vector


# 玩家類
class Player(object):
    def __init__(self, current_vector=None, target_vector=None, speed=0):
        self.current_vector = current_vector
        self.target_vector = target_vector
        self.speed = speed

    # 獲取玩家座標
    def get_current_vector(self):
        return self.current_vector

    # 判斷是否到達終點
    def is_arrived(self):
        return self.current_vector.distance(self.target_vector) < self.speed

    # 更新玩家位置
    def update_vector(self):
        # 獲取方向矢量(固定值)
        direction_vector = self.target_vector.sub(self.current_vector)
        # 矢量單位化(固定值)
        normalize_vector = direction_vector.normalize()
        # 根據速度計算 x, y 方向上前進的長度
        ongoing_vector = normalize_vector.multi(self.speed)
        # 更新位置
        self.current_vector.add(ongoing_vector)


if __name__ == '__main__':
    p = Player()
    p.current_vector = Vector(0, 0)
    p.target_vector = Vector(2, 2)
    p.speed = 0.2

    while not p.is_arrived():
        p.update_vector()
        print(f"({p.current_vector.x}, {p.current_vector.y})")
        time.sleep(1)

    print("arrive at the destination")
View Code
相關文章
相關標籤/搜索