一、定义

方法 是与对象实例绑定的特殊函数。

方法 是面向对象编程的基本概念,用于维护和展示对象的自身状态。对象是内敛的,每个实例都有各自不同的独立特征,以 属性 和 方法 来暴露对外通信接口。普通函数则专注于算法流程,通过接收参数来完成特定逻辑运算,并返回最终结果。换句话说,方法是有关联状态的,而函数通常没有。

方法 和 函数 定义语法区别的在于前者有 前置实例 接收参数(receiver),编译器以此确定方法所属类型。在某些语言里,尽管没有显示定义,但会在调用时隐式传递 this 实例参数。

可以为 当前包,以及除 接口 和 指针 以外的任何类型定义方法。

type N intfunc (n N) toString() string {return fmt.Sprintf("%#x", n)
}func main()  {var a N = 5println(a.toString())
}

输出:

0x19

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

type N intfunc (N) test() {println("hi!")
}

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

type N intfunc (n N) value() { // func value(n N)n++fmt.Printf("v: %p, %v\n", &n, n)
}func (n *N) pointer() { // func pointer(n *N)(*n)++fmt.Printf("p: %p, %v\n", n, *n)
}func main()  {var a N = 25a.value()a.pointer()fmt.Printf("a: %p, %v\n", &a, a)
}

输出:

v: 0xc42000a290, 26  // receiver 被复制
p: 0xc42000a268, 26
a: 0xc42000a268, 26

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

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

输出:

v: 0xc42000a290, 26
p: 0xc42000a268, 26v: 0xc42000a2c0, 27
p: 0xc42000a268, 27

不能用多级指针调用方法。

func main()  {var a N = 25p := &ap2 := &pp2.value()       // 错误:calling method value with receiver p2 (type **N)// requires explicit dereferencep2.pointer()   // 错误:calling method pointer with receiver p2 (type **N)// requires explicit dereference
}

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

type X struct {}func (x *X) test() {println("hi!", x)
}func main()  {var a *Xa.test() // 相当于 test(nil)X{}.test() // 错误:cannot take the address of X literal
}

将方法看作普通函数,就很容易理解 receiver 的传参方式。

如何选择方法的 receiver 类型?

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

二、匿名字段

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

type data struct {sync.Mutexbuf [1024]byte
}func main()  {d := data{}d.Lock() // 编译器会处理为 sync.(*Mutex).Lock() 调用defer d.Unlock()
}

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

type user struct {}type manager struct {user
}func (user) toString() string {return "user"
}func (m manager) toString() string {return m.user.toString() + "; manager"
}func main()  {var m managerprintln(m.toString())println(m.user.toString())
}

输出:

user; manager
user

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

三、方法集

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

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

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

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 TmethodSet(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)

输出结果符合预期,但我们也注意到某些方法的 receiver 类型发生了改变。真实情况是,这些都是由编译器按方法集所需自动生成的额外包装方法。

$ nm test | grep "main\."
...

方法集 仅影响 接口实现 和 方法表达式转换,与通过 实例 或 实例指针 调用方法无关。实例并不使用方法集,而是直接调用(或通过隐式字段名)。

很显然,匿名字段就是为方法集准备的。否则,完全没必要为少写个字段名而大费周章。

面向对象的三大特征“封装”、“继承”和“多态”,Go 仅实现了部分特征,它更倾向于“组合优先于继承”这种思想。将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。而且其简短一致的调用方式,更是隐藏了内部实现细节。

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

尽管接口也是多态的一种实现形式,但我认为应该和基于继承体系的多态分离开来。

四、表达式

方法 和 函数 一样,除直接调用外,还可赋值给变量,或作为参数传递。依照具体引用方式的不同,可分为 expression 和 value 两种状态。

Method Expression

通过类型引用的 method expression 会被还原为 普通函数样式,receiver 是第一参数,调用时须显式传参。至于类型,可以是 T 或 *T,只要目标方法存在于该类型方法集中即可。

type N intfunc (n N) test() {fmt.Printf("test.n: %p, %d\n", &n, n)
}func main()  {var n N = 25fmt.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: 0xc42008c030, 25
test.n: 0xc42008c048, 25
test.n: 0xc42008c058, 25

尽管 *N 方法集包装的 test() 方法 receiver 类型不同,但编译器会保证按原定义类型拷贝传值。

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

Method Value

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

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

输出:

main.n: 0xc42000a268, 103
test.n: 0xc42000a2a0, 101
test.n: 0xc42000a2b0, 102

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

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

type N intfunc (n N) test()  {fmt.Printf("test.n: %p, %v\n", &n, n)
}func call(m func())  {m()
}func main() {var n N = 100p := &nfmt.Printf("main.n: %p, %v\n", p, n)n++call(n.test)n++call(p.test)
}

输出:

main.n: 0xc420072188, 100
test.n: 0xc4200721c0, 101
test.n: 0xc4200721d0, 102

当然,如果目标方法的 receiver 是指针类型,那么被复制的仅是指针(注:指针值,及指针指向的内容没有变!)。

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

输出:

main.n: 0xc420072188, 103
test.n: 0xc420072188, 103
test.n: 0xc420072188, 103

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

type N intfunc (N) value() {}
func (*N) pointer() {}func main()  {var p *Np.pointer()          // method value(*N)(nil).pointer()  // method value(*N).pointer(nil)    // method expression//p.value()          // 报错:panic: runtime error: invalid memory address or nil pointer dereference
}

转载于:https://www.cnblogs.com/52php/p/6347375.html

《Go学习笔记 . 雨痕》方法相关推荐

  1. python3学习笔记 雨痕_Python 3 学习笔记:数字和布尔

    数字 基本类型 整数 在 Python 编程中,整数就是数学意义上的整数,包括正整数.负整数和零,且它的位数是任意的.根据表示方法的不同,可以分为: 二进制整数 八进制整数 十进制整数 十六进制整数 ...

  2. 《Go学习笔记 . 雨痕》类型

    一.基本类型 清晰完备的预定义基础类型,使得开发跨平台应用时无须过多考虑符合和长度差异. 类型 长度 默认值 说明 bool 1 false   byte 1 0 uint8 int, uint 4, ...

  3. 《Go学习笔记 . 雨痕》反射

    一.类型(Type) 反射(reflect)让我们能在运行期探知对象的类型信息和内存结构,这从一定程度上弥(mi)补了静态语言在动态行为上的不足.同时,反射还是实现元编程的重要手段. 和 C 数据结构 ...

  4. 《Go学习笔记 . 雨痕》流程控制(if、switch、for range、goto、continue、break)

    Go 精简(合并)了流控制语句,虽然某些时候不够便捷,但够用. if...else... 条件表达式值必须是布尔类型,可省略括号,且左花括号不能另起一行. func main() {x := 3if ...

  5. 深度学习笔记:优化方法总结(BGD,SGD,Momentum,AdaGrad,RMSProp,Adam)

    深度学习笔记(一):logistic分类  深度学习笔记(二):简单神经网络,后向传播算法及实现  深度学习笔记(三):激活函数和损失函数  深度学习笔记:优化方法总结  深度学习笔记(四):循环神经 ...

  6. 2020-4-5 深度学习笔记17 - 蒙特卡罗方法 3 ( 马尔可夫链蒙特卡罗方法MCMC-先验分布/后验分布/似然估计,马尔可夫性质)

    第十七章 蒙特卡罗方法 中文 英文 2020-4-4 深度学习笔记17 - 蒙特卡罗方法 1 (采样和蒙特卡罗方法-必要性和合理性) 2020-4-4 深度学习笔记17 - 蒙特卡罗方法 2 ( 重要 ...

  7. Python学习笔记Task11.魔法方法

    Python学习笔记Task11.魔法方法 魔法方法格式__init__ 1.基本 init(self[,-]) new(cls[,-]) del(self) str(self) repr(self) ...

  8. Laravel学习笔记汇总——Collection方法详解

    ## Laravel学习笔记汇总--Collection方法详解 本文参考:https:// laravel.com/docs/8.x/collections // 返回整个底层的数组 collect ...

  9. java学习笔记5--类的方法

    接着前面的学习: java学习笔记4--类与对象的基本概念(2) java学习笔记3--类与对象的基本概念(1) java学习笔记2--数据类型.数组 java学习笔记1--开发环境平台总结 本文地址 ...

最新文章

  1. CentOS7下启动Nginx出现Failed to start nginx.service:unit not found
  2. 我的hadoop学习之路
  3. matlab溢出的标志inf,关于C#:溢出与信息
  4. Exchange 2010迁移Exchange 2013(一)共存部署
  5. 保守官僚 诺基亚就这样迷失在智能机时代?
  6. 08:石头剪刀布【一维数组】
  7. 【学校集训】【USACO15DecG】Bessie's Dream
  8. 6.苹果官方鼠标移动速度慢问题解决(Magic Mouse)
  9. 2021年下种子磁力最好用的网盘
  10. 国外优秀Windows7桌面插件RAINMETER
  11. 「数商云专辑」服装/服饰电商平台解决方案
  12. java的inputbox_InputBox函数的使用方法
  13. 电商设计师如何正确认知自己的价值
  14. 062:vue+openlayers绘制正方形、矩形、六芒星( 代码示例 )
  15. meta标签http-equiv属性实现自动刷新页面和重定向
  16. oracle定时器每天下午6点_强力巨彩冠名!6月23日下午15点直播抢先看
  17. UE角色以及角色动画超详细流程干货!这次是step by step!
  18. 关于word不能存档解决办法
  19. deflate树与deflate编码
  20. 圆周率 php算法,PHP坐标圆周率计算

热门文章

  1. 史上最全AI开源项目集结,近万篇附代码的论文分门别类整理好
  2. pwm驱动电机 为什么pwm不能太快_认识直流电机的PWM驱动控制电路
  3. linux文件类型缩写,常见Linux系统目录、文件类型、ls命令、alias命令
  4. 最详细的SSD论文笔记
  5. 电信设置的nat 虚拟服务器192.168.1.3 是什么,VMware WorkStation的三种网络连接方式详解...
  6. 计算机适配器有什么作用,例举适配器是什么
  7. 软考网络管理员学习笔记1之第一章计算机硬件基础
  8. Log图文详解(Log.v,Log.d,Log.i,Log.w,Log.e)!
  9. linux httpd 内存,apache占用内存过高耗完内存?
  10. Git---安装步骤