《Go语言学习笔记》读书笔记(6)方法

3 分钟阅读

方法

方法是与对象实例绑定的特殊函数。 方法是面向对象编程的基本概念,用于维护和展示对象的自身状态。对象是内敛的,每个实例都有各自不同的独立特征,以属性和方法来暴露对外通信接口。普通函数则专注于算法流程,通过接收参数来完成特定逻辑运算,并返回最终结果。换句话说,方法是有关联的而函数通常没有。 方法和函数定义语法区别,在于前者有前置实例接收参数,编译器以此确定方法所数类型。 可以为当前包,以及除接口和指针以外的任何类型定义方法。

type N int
func (n N) toString() string {
  return fmt.Sprintf("%#x", n)
}

func main() {
  var a N = 25
  println(a.toString())
}
/// 输出:
/// 0x19

方法同样不支持重载(overload)。receiver参数名没有限制,按惯例会选用简短有意义的名称。如方法内部并不引用实例,可省略参数名,仅保留类型。

type N int

func (N) test() {
  println("hi~")
}

方法可看作特殊的函数,那么receiver的类型自然可以是基础类型或指针类型。这会关系到调用时对象实例是否被赋值。

type N int
func (n N) value() {
  n++
  fmt.Printf("v: %p, %v\n", &n, n)
}

func (n *N) pointer() {
  (*n)++
  fmt.Printf("p: %p, %v\n", n, *n)
}

func main() {
  var a N = 25
  a.value()
  a.pointer()

  fmt.Printf("a: %p, %v\n", &a, a)
}

/// 输出:
/// v: 0xc8200741c8, 26 /// receiver 被复制
/// p: 0xc8200741c0, 26
/// a: 0xc8200741c0, 26

可使用实例值或指针调用方法,编译器会根据方法receiver类型自动在基础类型和指针类型间转换。

func main() {
  var a N = 25
  p := &a
  a.value()
  a.pointer()
  p.value()
  p.pointer()

  /// p2 := &p   错误
}
/// 输出:
/// v: 0xc82999a2c0, 26
/// p: 0xc82999a298, 26
/// v: 0xc82000a2f0, 27
/// v: 0xc82000a298, 27

指针类型的receiver必须时合法指针(包括nil), 或能获取实例地址。

type X struct {}

func (x *X) test() {
  println("hi!", x)
}

func main() {
  var a *X
  a.test()  ///相当于test(nil)
  X{}.test()  /// 错误无法获取地址
}

如何选择方法的receiver类型:

  • 要修改实例状态,用*T
  • 无需修改状态的小对象或固定值,建议用T
  • 大对象建议用*T, 以减少复制成本。
  • 引用类型、字符串、函数等指针包装对象,直接用T。
  • 若包含Mutex等同步字段,用*T,避免因复制造成锁操作无效
  • 其他无法确定的情况,都用*T

匿名字段

可以访问匿名字段成员那样调用其方法,有编译器负责查找。

type data struct {
  sync.Mutex
  buf [1024]byte
}

func main() {
  d := data()
  d.Lock()  /// 编译器会处理为 sync.(*Mutex).Lock()调用
  defer d.Unlock()
}

方法也会有同名遮蔽问题。但利用这一特性可实现类似(override)操作。

type user struct {}
type manager struct {
  user
}

fun (user) toString() string {
  return "user"
}

func (m manager) toString() string {
  return m.user.toString() + "; manager"
}

func main() {
  var m manager
  println(m.toString())
  println(m.user.toString())
}
/// 输出:
/// user; manager
/// user

尽可能直接访问匿名字段的成员和方法,但他们依然不属于继承关系。

方法集

类型有一个与之相关的方法集,这决定了它是否实现某个接口。

  • 类型T方法集包含所有receiver T方法。
  • 类型*T方法集包含所有recever T + *T方法。
  • 匿名嵌入S, T方法集包含所有receiver S方法。
  • 匿名嵌入*S, T方法集包含所有receiver S+*S方法。
  • 匿名嵌入S*S, *T方法集包含所有receiver S+*S方法。

可利用反射测试这些规则。

type S struct{}
type T struct{
  S // 匿名嵌入字段
}
func (S) sVal() {}
func (*S) sPtr() {}
func (T) tVal() {}
func (*T) tPtr() {}

func methodSet(a interface{}) {
  t := reflect.TypeOf(a)
  for i, n := 0, t.NumMethod(); i < n; i++ {
    m := t.Method(i)
    fmt.Println(m.Name, m.Type)
  }
}

func main() {
  var t T
  methodSet(t)  ///< 显示T方法集
  println("-------")
  methodSet(&t) ///< 显示*T方法集
}
/// 输出:
/// sVal func(main.T)
/// tVal func(main.T)
/// ---------
/// sPtr func(*main.T)
/// sVal func(*main.T)
/// tPtr func(*main.T)
/// tVal func(*main.T)

方法集影响接口实现和方法表达式转换,于通过实例或实例指针调用方法无关。实例并不使用方法集,而是直接调用(或通过隐式字段名). 很显然,匿名字段就是为方法准备的。否则,完全没必要为少写个字段名而大费周折。 面向对象的三大特征”封装”,”继承”和”多态”, Go仅仅实现了部分特征,它更倾向于”组合优于继承“这种思想。将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。而且其简短一致的调用方式,更是隐藏了内部实现细节。

组合没有父子依赖,不会破坏封装。且整体和局部松耦合,可任意增加来实现实现扩展。各单元持有单一职责,互无关联,实现和维护更加简单。

表达式

方法和函数一样,除直接调用外还可以赋值给变量,或作为参数传递。依照具体引用方式不同,可分为expressionvalue两种状态。 通过类型引用Method expression会被还原为普通函数央视,receiver是第一参数,调用时须显示传参。至于类型,可以是T*T, 只要目标方法集中即可。

type N int
func (n N) test() {
  fmt.Printf("test.n: %p, %d", &n, n)
}
func main() {
  var n N = 25
  fmt.Printf("main.n: %p, %d\n", &n, n)
  f1 := N.test  ///< func(n N)
  f1(n)
  f2 := (*N).test ///< func(n *N)
  f2(&n)  ///< 按方法集中的签名传递正确类型的参数
}
/// 输出:
/// main.n: 0xc82000a140, 25
/// test.n: 0xc82000a158, 25
/// test.n: 0xc82000a168, 25

当然,也可直接以表达式方式调用。

func main(){
  var n N = 25
  N.test(n)
  (*N).test(&n) ///< 注意: *N 须使用括号,以免语法解析错误。
}

基于实例或指针引用的method value, 参数签名不会改变,依旧按正常方式调用。 但当method value被赋值给变量或作为参数传递时,会立即计算并复制该方法执行所需的receiver对象,与其绑定,以便在稍后执行时,能隐式传入receiver参数。

type N int
func (n N) test() {
  fmt.Printf("test.n: %p, %v\n", &n, n)
}
func main() {
  var n N = 100
  p := &n
  n++
  f1 := n.test  //< 因为test方法的reveiver是类型,所以复制n, 等于101

  n++
  f2 := p.test  ///< 复制*p, 等于102
  n++
  fmt.Printf("main.n: %p, %v\n", p, n)
  f1()
  f2()
}
/// 输出:
/// main.n: 0xc829976028, 103
/// test.n: 0xc820076060, 101
/// test.n: 0xc820076060, 102

编译器会为method value生成一个包装函数,实现间接调用。至于receiver复制,和闭包的实现方法基本相同,打包成funcval, 经由DX寄存器传递。

method value作为参数是,会复制含receiver在内的整个method value

func call(m func()) {
  m()
}
func main(){
  var n N = 100
  p := &n
  fmt.Printf("main.h: %p, %v", p, n)
  n++
  call(n.test)
  n++
  call(p.test)
}
/// 输出:
/// main.n 0x82000a288, 100
/// test.n 0x82000a2c0, 101
/// main.n 0x82000a2d0, 102

当然,如果目标方法的receiver是指针类型,那么被复制的仅是指针。

type N int
func (n *N) test() {
  fmt.Printf("test.n: %p, %v\n", n, *n)
}
func main() {
  var n N = 100
  p := &n
  n++
  f1 := n.test  ///< 因为test方法的receiver是*N类型, 所以复制&n
  n++
  f2 := p.test  ///< 复制p指针
  n++
  fmt.Printf("main.n: %p, %v\n", p, n)
  f1()  ///< 延迟调用,n == 103
  f2()
}
/// 输出:
/// main.n: 0xc82000a298, 103
/// test.n: 0xc82000a298, 103
/// test.n: 0xc82000a298, 103

只要receiver参数类型正确,使用nil同样可以执行。

type N int
func (N) value() {}
func (*N) pointer() {}

func main(){
  var p *N
  p.pointer() // method value
  (*N)(nil).pointer() // method value
  (*N).pointer(nil) // method expression
  /// p.value() 错误: invalid memory address or nil pointer dereference
}