关于“具名类型值向空接口赋值策略”的讨论
本议题采用 实时线上会议 / 论坛回帖 结合的模式,可直接回复本贴或参与线上会议讨论,线上会议时间为 2023年5月13日20点至21点,会议视频:
以下为正文:
背景知识:接口的一些实现细节
-
从概念上看,一个接口值(接口类型的实例)主要由两部分组成:具体类型、以及该具体类型的值;其中,具体类型值保存在接口值内
data
指针地址处; -
将一个具体类型的值向接口赋值时,执行的是
传值
复制,既分配一段存储该具体类型的内存,地址存入接口值的data
处,然后将具体类型值拷贝至其中。
背景知识:凹语言中具名类型方法(Method)的定义
下面是一个凹语言中为具名类型定义方法并使用的例子:
type T struct {
member: int
}
func T.Set(n int) {
this.member = n
}
func T.Print() {
println("member:", this.member)
}
func main() {
var v T
v.Set(42)
v.Print()
}
其中 func T.Set(){...}
和 func T.Print(){...}
分别为类型 T
定义了名为 Set
和 Print
的方法。熟悉 Go 语言的朋友很容易看出,凹语言中的方法定义更接近C++风格,与 Go 相比有两个显著的不同:
-
方法函数都可以改变类型实例的内部状态。如果套用 Go 的术语,可以理解为:凹语言方法没有值类型接收器(receiver),只有指针型接收器;
-
在方法内部,使用
this.
选择器访问类型实例的成员。
由于方法接收器只能为指针,因此凹语言中的值类型只能实现空接口,而不能实现非空接口。例如:
type T struct {
member: int
}
func T.Set(n int) {
this.member = n
}
func T.Print() {
println("member:", this.member)
}
type Printer interface {
Print()
}
func main() {
var v T
v.Set(42)
var p Printer
p = &v
p.Print()
p = v //Invalid
}
在上例中,p = &v
可以赋值,p.Print()
执行正常;但 p = v
编译不过,因为类型 T
的值类型不满足 Printer
接口定义。
具名类型值向空接口赋值的两种策略
为什么要将讨论对象限定为“具名类型值向空接口赋值”呢?因为根据前述介绍,在凹语言中,值类型无法实现非空接口,因此其只能
向空接口赋值。
策略1:严格遵守原始语义,使用值拷贝,向空接口传入的是值,取回时拿到的也是值;
策略2:向空接口中传入具名类型值(非指针)时,拷贝一份,但是实际存入空接口中的是该拷贝的地址,并且标记为指针;也就是说传入时是值,但取回时是地址(该地址是复制品的地址,因此不会影响原始父本)。下面的例子可以更直观的解释该策略:
type T struct {}
func T.Print() { println("This is T.") }
type Printer interface {
Print()
}
func main() {
var v1 T
var i1 interface{} = v1
var v2 *T = i1.(*T) //This will work
v2.Print()
var i2 Printer = i1.(Printer) //This will work too
i2.Print()
}
在该策略下,v1
赋值给了空接口 i1
,但 i1
实际保存的类型为 *T
,后续可以通过 i1.(*T)
将其取出,并且由于 *T
满足 Printer
接口定义,i1
甚至可以转换类型为 Printer
。
注意,该代码仅为展示策略2之用,目前的凹编译器并未实现该策略。
由于凹语言中不存在值类型接收器,因此如果严格遵守原始语义,将值类型赋值给空接口只能起到数据包装的作用——即使该类型实现了非空接口,也无法直接访问。
在策略2中,将值类型赋值给空接口时,实际上改变了类型(由 T
变为 *T
),这使得接口“升格”。但缺点是改变了语义,可能导致与 Go 的不兼容性。