第六章方法

  • 方法声明

方法的声明和普通函数的声明类似,只是在函数名字前面多了一个参数,这个参数把这个方法绑定到这个参数对应的类型上

package geometry

import (
    "fmt"
    "math"
)

type Point struct {
    X, Y float64
}
//普通的函数
func Distance(p, q Point) float64 {
    return math.Hypot(q.X - p.X, q.Y - p.Y)
}
//Point类型的方法,附加的参数p称为方法的接收者,它源自早先的面向对象语言,用来描述主调方法就像向对象发送消息
//Go语言中,接收者不使用特殊名(比如this或者self),而是我们自己选择接收者名字,就像其他的参数变量一样
//调用方法的时候,接收者在方法名的前面,这就和声明保持一致
//方法 func (p Point) Distance(q Point) float64 { return math.Hypot(q.X - p.X, q.Y - p.Y) } func main(){ p := Point{1, 2} q := Point{4, 6}
fmt.Println(Distance(p, q))
//包级别的函数
    //调用方法的时候,接收者在方法名的前面,这就和声明保持一致
    fmt.Println(p.Distance(q)) //Point类型的方法
    //表达式p.Distance称作选择子(selector),它为接收者p选择合适的Distance方法
    //选择子也用于选择结构类型中的某些字段值
    
    //编译器会通过方法名和接收者的类型决定调用哪一个函数
    //path[i-1]是Point类型,因此调用Point.Distance
    //perim是Path类型,因此调用Path.Distance
    perim := Path{
        {1, 1},
        {5, 1},
        {5, 4},
        {1, 1},
    }
    
    fmt.Println(perim.Distance())
}

//Path是连接多个点的直线段
type Path []Point
//Distance方法返回路径的长度
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这样的结构体类型,但依旧可以给它定义方法
//Go和许多其他面向对象的语言不同,可以将方法绑定到任何类型上
//可以很方便地为简单的类型(如数字、字符串、slice、map,甚至函数等)定义附加的行为
//同一个包下的任何类型都可以声明方法,只要它的类型既不是指针类型也不是接口类型

使用方法的第一个好处:命名可以比函数更简短。在包的外部进行调用的时候,方法能够使用更加简短的名字且省略包的名字

  • 指针接收者的方法

由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者一个实参太大我们希望避免复制整个实参,因此必须使用指针来传递变量的地址。这也同样适用于更新接收者:将他绑定到指针类型,比如*Point

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

//这个方法的名字是(*Point).ScaleBy
//圆括号是必须的,没有圆括号,表达式会被解析为*(Point.ScaleBy)

//在真实的程序中,习惯上遵循如果Point的任何一个方法使用指针接收者,那么所有的Point方法都应该使用指针接收者,即使有些方法并不一定需要

//命名类型Point与指向它们的指针*Point是唯一可以出现在接收者声明处的类型。为防止混淆,不允许使用本身是指针的类型进行方法声明
/**如果接收者p是Point类型的变量,但方法要求一个*point接收者,可以使用简写:*/
p.ScaleBy(2)
/**实际上编译器会对变量进行&p的隐式转换。只有变量才允许这么做,包括结构体字段,像p.X和数组或者slice的元素,比如perim[0]。不能对一个不能取地址的Point接收者参数调用*Point方法,因为无法获取临时变量的地址*/
Point{1,2}.ScaleBy(2)//编译错误:不能获得Point类型字面量的地址

//如果实参接收者是*Point类型,以Point.Distance的方式调用Point类型的方法是合法的,因为我们可以从地址中获取Point的值;只要解引用指向接收者的指针值即可。编译器自动插入一个隐式的*操作符

pptr := &(Point{1, 2})

pptr.Distance(q)  //等价于
(*pptr).Distance(q)
//在合法的方法调用表达式中,只有符合下面三种形式的语句才能够成立

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

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

//实参接收者和形参接收者是同一个类型
Point{1,2}.Distance(q) //Point
pptr.ScaleBy(2) //*Point 

//实参接收者是T类型,形参接收者是*T类型。编译器会隐式地获取变量的地址
p.ScaleBy(2)  //隐式转换为(&p)

//实参接收者是*T类型,形参接收者是T类型。编译器会隐式地解析引用接收者,获得实际的值
pptr.Distance(q) //隐式转换为(*pptr)

如果所有类型T方法的接收者是T自己(而非*T),那么复制它的实例是安全的;调用方法的时候都必须进行一次复制。在任何方法的接收者是指针的情况下,应该避免复制T的实例,因为这么做可能会破坏内部原本的数据

nil是一个合法的接收者:就像一些函数允许nil指针作为实参,方法的接收者也一样,尤其是当nil是类型中有意义的零值(如map和slice类型)时,更是如此

//IntList是整型链表
//*IntList的类型nil代表空列表
type IntList struct {
    Value int
    Tail *IntList
}

//Sum返回列表元素的总和
func (list *IntList) Sum() int {
    if list == nil {
        return 0
    }
    return list.Value + list.Tail.Sum()
}

//当定义一个类型允许nil作为接收者时,应当在文档注释中显式地标明
// net/url包种Values类型的部分定义:

package url
//Values映射字符串到字符串列表
type Values map[string][]string

//Get返回第一个具有给定key的值
//如不存在,则返回空字符串
func (v values) Get(key string) string {
    if vs := v[key];len(vs)>0{
return vs[0]
}
return "" }

//Add添加一个键值到对应key列表中
func (v Values) Add(key, value string) {
v[key] = append(v[key], value)
}

 它的实现是map类型但也提供了一系列方法来简化map的操作,它的值是字符串slice,即一个多重map。使用者可以使用它固有的操作方式(make、slice字面量、m[key]),或者使用它的方法,或者同时使用:

m := url.Values{"lang": {"en"}}  //直接构造
m.Add("item", "1")
m.Add("item", "2")

fmt.Println(m.Get("lang"))
fmt.Println(m.Get("q"))
fmt.Println(m.Get("item"))
fmt.Println(m["item"])

m = nil
fmt.Println(m.Get("item"))
fmt.Println(m["item"])

m = nil
fmt.Println(m.Get("item"))
m.Add("item", "3")  //宕机:赋值给空的map类型
//在最后一个Get调用中,nil接收者充当一个空map。它可以等同地写成Values(nil),Get("item"),但是nil.Get("item")不能通过编译,因为nil的类型没有确定。最后的Add方法会发生宕机因为它尝试更新一个空的map
//因为url.Values是map类型而且map间接地指向它的键/值对,所以url.Values.Add对map中元素的任何更新和删除操作对调用者都是可见的。然而,和普通函数一样,方法对引用本身做的任何改变,比如设置url.Values为nil或者使它指向一个不同的map数据结构,都不会在调用者身上产生作用??
  • 通过结构体内嵌组成类型
type Point struct{ X, Y float64 }

type ColoredPoint struct {
    Point
    Color color.RGBA
}

//内嵌类型Point,和ColoredPoint并无继承关系
//需要接收Point类型的参数时,如果传入了一个ColoredPoint类型的参数,会报错
//编译错误:不能将ColoredPoint转换为Point类型

内嵌的字段会告诉编译器生成额外的包装方法来调用Point声明的方法,相当于:

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

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

匿名字段类型可以是个指向命名类型的指针,这时,字段和方法间接地来自于所指向的对象。这可以让我们共享通用的结构以及使对象之间的关系更加动态、多样化

  • 方法变量与表达式

通常都在相同的表达式里使用和调用方法,就像在p.Distance()中,但是把两个操作分开也是可以的。选择子p.Distance可以赋予一个方法变量,它是一个函数,把方法Point.Distance绑定到一个接收者p上。函数只需要提供实参而不需要提供接收者就能够调用

p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance //方法变量   选择子 p.Distance
fmt.Println(distanceFromP(q))
var origin Point
fmt.Println(distanceFromP(origin))

scaleP := p.ScaleBy
scaleP(2)  //p变成(2,4)

如果包内的API调用一个函数值,并且使用者期望这个函数的行为是调用一个特定接收者的方法,方法变量就非常有用

与方法变量相关的是方法表达式。和调用普通的函数不同,在调用方法的时候必须提供接收者,并且按照选择子的语法进行调用。而方法表达式写成T.f或者(*T).f,其中T是类型,是一种函数变量,把原来方法的接收者替换成函数的第一个形参,因此它可以像平常的函数一样调用

p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance //方法表达式
T.f或者(*T).f,其中T是类型,是一种函数变量,把原来方法的接收者替换成函数的第一个形参
fmt.Println(distance(p, q))
fmt.Printf("%T ", distance)

scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p)
fmt.Printf("%T ", scale)

//如果需要用一个值来代表多个方法中的一个,而方法都属于同一个类型,方法变量可以帮助你调用这个值所对应的方法来处理不同的接收者
type Point struct { X, Y float64}

func (p Point) Add(q Point) Point {
    return Point{
        
    }
}
  • 示例:位向量
  • 封装
原文地址:https://www.cnblogs.com/liushoudong/p/13039294.html