《Go学习笔记 . 雨痕》方法
一、定义
方法 是与对象实例绑定的特殊函数。
方法 是面向对象编程的基本概念,用于维护和展示对象的自身状态。对象是内敛的,每个实例都有各自不同的独立特征,以 属性 和 方法 来暴露对外通信接口。普通函数则专注于算法流程,通过接收参数来完成特定逻辑运算,并返回最终结果。换句话说,方法是有关联状态的,而函数通常没有。
方法 和 函数 定义语法区别的在于前者有 前置实例 接收参数(receiver),编译器以此确定方法所属类型。在某些语言里,尽管没有显示定义,但会在调用时隐式传递 this 实例参数。
可以为 当前包,以及除 接口 和 指针 以外的任何类型定义方法。
type N int func (n N) toString() string {
return fmt.Sprintf("%#x", n)
} func main() {
var a N = 5
println(a.toString())
}
输出:
0x19
方法同样不支持重载(overload)。receiver 参数名没有限制,按惯例会选用简短有意义的名称(不推荐使用 this、self)。如果 方法内部并不引用实例,可省略参数名,仅保留类型。
type N int func (N) test() {
println("hi!")
}
方法 可看作特殊的函数,那么 receiver 的类型自然可以是 基础类型 或 指针类型。这会关系到调用时对象实例是否被复制。
type N int func (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 = 25 a.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 = 25
p := &a a.value()
a.pointer() p.value()
p.pointer()
}
输出:
v: 0xc42000a290, 26
p: 0xc42000a268, 26 v: 0xc42000a2c0, 27
p: 0xc42000a268, 27
不能用多级指针调用方法。
func main() {
var a N = 25 p := &a
p2 := &p p2.value() // 错误:calling method value with receiver p2 (type **N)
// requires explicit dereference p2.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 *X
a.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.Mutex
buf [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 manager println(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 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)
输出结果符合预期,但我们也注意到某些方法的 receiver 类型发生了改变。真实情况是,这些都是由编译器按方法集所需自动生成的额外包装方法。
$ nm test | grep "main\."
...
方法集 仅影响 接口实现 和 方法表达式转换,与通过 实例 或 实例指针 调用方法无关。实例并不使用方法集,而是直接调用(或通过隐式字段名)。
很显然,匿名字段就是为方法集准备的。否则,完全没必要为少写个字段名而大费周章。
面向对象的三大特征“封装”、“继承”和“多态”,Go 仅实现了部分特征,它更倾向于“组合优先于继承”这种思想。将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。而且其简短一致的调用方式,更是隐藏了内部实现细节。
组合没有父子依赖,不会破坏封装。且整体和布局松耦合,可任意增加来实现扩展。各单元持有单一职责,互无关联,实现和维护更加简单。
尽管接口也是多态的一种实现形式,但我认为应该和基于继承体系的多态分离开来。
四、表达式
方法 和 函数 一样,除直接调用外,还可赋值给变量,或作为参数传递。依照具体引用方式的不同,可分为 expression 和 value 两种状态。
Method Expression
通过类型引用的 method expression 会被还原为 普通函数样式,receiver 是第一参数,调用时须显式传参。至于类型,可以是 T 或 *T,只要目标方法存在于该类型方法集中即可。
type N int func (n N) test() {
fmt.Printf("test.n: %p, %d\n", &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: 0xc42008c030, 25
test.n: 0xc42008c048, 25
test.n: 0xc42008c058, 25
尽管 *N 方法集包装的 test() 方法 receiver 类型不同,但编译器会保证按原定义类型拷贝传值。
当然,也可直接以表达式方式调用。
Method Value
基于 实例 或 指针引用 的 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 方法的 receiver 是 N 类型,所以复制 n,等于 101 n++
f2 := p.test // 复制 *p,等于 102 n++
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 int func (n N) test() {
fmt.Printf("test.n: %p, %v\n", &n, n)
} func call(m func()) {
m()
} func main() {
var n N = 100
p := &n fmt.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 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: 0xc420072188, 103
test.n: 0xc420072188, 103
test.n: 0xc420072188, 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() // 报错:panic: runtime error: invalid memory address or nil pointer dereference
}
《Go学习笔记 . 雨痕》方法的更多相关文章
- 《Go学习笔记 . 雨痕》类型
一.基本类型 清晰完备的预定义基础类型,使得开发跨平台应用时无须过多考虑符合和长度差异. 类型 长度 默认值 说明 bool 1 false byte 1 0 uint8 int, uint 4, ...
- 《Go学习笔记 . 雨痕》反射
一.类型(Type) 反射(reflect)让我们能在运行期探知对象的类型信息和内存结构,这从一定程度上弥(mi)补了静态语言在动态行为上的不足.同时,反射还是实现元编程的重要手段. 和 C 数据结构 ...
- 《Go学习笔记 . 雨痕》流程控制(if、switch、for range、goto、continue、break)
Go 精简(合并)了流控制语句,虽然某些时候不够便捷,但够用. if...else... 条件表达式值必须是布尔类型,可省略括号,且左花括号不能另起一行. func main() { x := 3 i ...
- 深度学习笔记:优化方法总结(BGD,SGD,Momentum,AdaGrad,RMSProp,Adam)
深度学习笔记:优化方法总结(BGD,SGD,Momentum,AdaGrad,RMSProp,Adam) 深度学习笔记(一):logistic分类 深度学习笔记(二):简单神经网络,后向传播算法及实现 ...
- Object C学习笔记10-静态方法和静态属性
在.NET中我们静态使用的关键字static有着举足轻重的作用,static 方法可以不用实例化类实例就可以直接调用,static 属性也是如此.在Object C中也存在static关键字,今天的学 ...
- java学习笔记5--类的方法
接着前面的学习: java学习笔记4--类与对象的基本概念(2) java学习笔记3--类与对象的基本概念(1) java学习笔记2--数据类型.数组 java学习笔记1--开发环境平台总结 本文地址 ...
- Java设计模式学习笔记(三) 工厂方法模式
前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 1. 简介 上一篇博客介绍了简单工厂模式,简单工厂模式存在一个很严重的问题: 就是当系统需要引入 ...
- JavaScript学习笔记之 数组方法一 堆栈 和队列
数组的方法 以及 堆栈的操作的方法 JavaScript是一种弱类型语言,不像其它程序语言需要严格定义数据类型.在JavaScript中数组可以任意修改变动,这样也就出现了一个问题,如果边遍历数组边操 ...
- jQuery学习笔记之extend方法小结
在学习jQuery的时候,学习到了$.extend的主要用法,在此做一个简单的总结. (1)当只写一个对象自变量时,拓展的是jQuery的工具方法,如: $.extend({ aaa:function ...
随机推荐
- shell邮件发送功能实现
本文中以163邮箱为例,测试shell邮件发送功能.常见的工具有:mailx.sendmail.mutt等. 1.设置邮件客户端 (1)启用pop3.smtp服务,以支持第三方客户端支持 (2)设置授 ...
- mysql修改时区
查看mysql当前时间 查看mysql时区设置 查看mysql数据库的时区 mysql修改时区的几种方法
- ubuntu 开机自动挂载分区
转载: http://blog.sina.com.cn/s/blog_142e95b170102vx2a.html 我的计算机是双硬盘,一个是windows系统,一个是Fedora和ubuntu系统. ...
- 使用Docx4j创建word文档
原文标题:Creating Word documents with Docx4j 原文链接:http://blog.iprofs.nl/2012/09/06/creating-word-documen ...
- Oracle中Inventory目录作用以及如何重建此目录 oraInst.loc 文件
inventory 英 [ˈɪnvəntri] 美 [ˈɪnvəntɔ:ri] n. 清查; 存货清单; 财产目录,财产目录的编制; 存货总值; vt. 盘存; 编制…的目录; 开列…的清单; 总结 ...
- Android Studio引用第三方jar包(例如:使用LitePal)
如何使用LitePal的教程网上很多,不过对于新手,这些教程中遗漏了将第三方jar包拷贝到app->libs路径下后,还需要add as library. 下载LitePal 这时操作步骤如下: ...
- python基础--time和datetime模块
一:说明在Python中,通常有这几种方式来表示时间:1)时间戳 2)格式化的时间字符串 3)元组(struct_time)共九个元素.由于Python的time模块实现主要调用C库,所以各个平台可能 ...
- MXNet深度学习库简介
MXNet深度学习库简介 摘要: MXNet是一个深度学习库, 支持C++, Python, R, Scala, Julia, Matlab以及JavaScript等语言; 支持命令和符号编程; 可以 ...
- java 异常链
1.) 常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,被称为异常链. 2.)Throwable子类在构造器中可以接受一个cause(因由)对象作为参数.这个cause就是 ...
- Laravel 生成二维码的方法
(本实例laravel 版本 >=5.6, PHP版本 >=7.0) 1.首先,添加 QrCode 包添加到你的 composer.json 文件的 require 里: "re ...