Go语言方法和接收器
发布时间:2020-12-16 09:37:07 所属栏目:大数据 来源:网络整理
导读:在Go语言中,结构体就像是类的一种简化形式,那么类的方法在哪里呢?在Go语言中有一个概念,它和方法有着同样的名字,并且大体上意思相同,Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。 接收器类
在Go语言中,结构体就像是类的一种简化形式,那么类的方法在哪里呢?在Go语言中有一个概念,它和方法有着同样的名字,并且大体上意思相同,Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。 接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误 invalid receiver type… 。接收器也不能是一个指针类型,但是它可以是任何其他允许类型的指针,一个类型加上它的方法等价于面向对象中的一个类,一个重要的区别是,在Go语言中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的。 类型 T(或 T)上的所有方法的集合叫做类型 T(或 T)的方法集。 因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法,但是如果基于接收器类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收器类型上存在,比如在同一个包里这么做是允许的。 提示在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在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) }代码说明如下:
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 (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
接收器根据接收器的类型可以分为指针接收器、非指针接收器,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。 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 代码说明如下:
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} 代码说明如下:
由于例子中使用了非指针接收器,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} }代码说明如下:
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 实例化后,设定玩家移动的最终目标点,之后开始进行移动的过程,这是一个不断更新位置的循环过程,每次检测玩家是否靠近目标点附近,如果还没有到达,则不断地更新位置,让玩家朝着目标点不停的修改当前位置,如下代码所示: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()) } }代码说明如下:
本例中使用到了结构体的方法、构造函数、指针和非指针类型方法接收器等,读者通过这个例子可以了解在哪些地方能够使用结构体。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |