接口篇(二):通过接口赋值实现接口与实现类的映射
上篇教程我们介绍了接口定义及实现,和 PHP 一样,Go 语言的接口不支持直接实例化,只能通过实现类实现接口声明的所有方法,不过不同之处在于 Go 语言接口支持赋值操作,从而快速实现接口与实现类的映射,与之相比,PHP 要实现接口与实现类的映射,只能基于 IoC 容器通过依赖注入实现,就像 Laravel 框架底层服务容器所做的那样,要复杂的多。
接口赋值在 Go 语言中分为如下两种情况:
- 将实现接口的对象实例赋值给接口;
- 将一个接口赋值给另一个接口。
下面我们通过代码实例逐个介绍对应的实现和注意事项。
将对象实例赋值给接口
先看看将指定类型的对象实例赋值给接口,这要求该对象对应的类实现了接口要求的所有方法,这个是自然,否则也就不能算作实现该接口了,例如之前我们在为基本类型添加方法这篇教程中定义过一个 Integer
类型:
type Integer int
func (a Integer) Equal(i Integer) bool {
return a == i
}
func (a Integer) LessThan(i Integer) bool {
return a < i
}
func (a Integer) MoreThan(i Integer) bool {
return a > i
}
func (a *Integer) Increase(i Integer) {
*a = *a + i
}
func (a *Integer) Decrease(i Integer) {
*a = *a - i
}
相应地,我们定义一个接口 IntNumber
:
type IntNumber interface {
Equal(i Integer) bool
LessThan(i Integer) bool
MoreThan(i Integer) bool
Increase(i Integer)
Decrease(i Integer)
}
按照 Go 语言的约定,Integer
类型实现了 IntNumber
接口。然后我们可以这样将 Integer
类型对应的对象实例赋值给 IntNumber
接口:
var a Integer = 1
var b IntNumber = &a
注意到上述赋值语句中,我们将对象实例 a
的指针引用赋值给了接口变量,为什么要这么做呢?因为 Go 语言会根据类似下面这样的非指针成员方法:
func (a Integer) Equal(i Integer) bool
自动生成一个新的与之对应的指针成员方法:
func (a *Integer) Equal(i Integer) bool {
return (*a).Equal(i)
}
这样一来,类型 *Integer
就存在所有 IntNumber
接口中声明的方法,而 Integer
类型不包含指针方法 Increase
和 Decrease
(关于这一点我们前面已经介绍过),所以严格来说,只有 *Integer
类型实现了 IntNumber
接口。如果我们贸然将 a
的值引用赋值给 b
,编译时会报错:
cannot use a (type Integer) as type IntNumber in assignment:
Integer does not implement IntNumber (Decrease method has pointer receiver)
显然,如果 Integer
类中实现的方法不是指针方法,则进行接口赋值时,传递对象实例的值引用给接口变量即可,否则需要传递指针变量。为了验证这一点,我们可以再创建一个新的接口 IntNumber2
:
type IntNumber2 interface {
Equal(i Integer) bool
LessThan(i Integer) bool
MoreThan(i Integer) bool
}
然后将对象实例 a
的值引用赋值给 IntNumber
接口变量:
var a Integer = 1
var b1 IntNumber = &a
var b2 IntNumber2 = a
则上面两条接口赋值语句都可以编译通过。
将接口赋值给接口
接下来我们来看将一个接口赋值给另一个接口:在 Go 语言中,只要两个接口拥有相同的方法列表(与顺序无关),那么它们就是等同的,可以相互赋值。
下面我们来编写对应的示例代码,这是第一个接口 Number1
,位于 oop1
包中:
package oop1
type Number1 interface {
Equal(i int) bool
LessThan(i int) bool
MoreThan(i int) bool
}
这是第二个接口 Number2
,位于 oop2
包中:
package oop2
type Number2 interface {
Equal(i int) bool
MoreThan(i int) bool
LessThan(i int) bool
}
这里我们定义了两个接口,一个叫 oop1.Number1
,一个叫 oop2.Number2
,两者都定义三个相同的方法,只是顺序不同而已。在 Go 语言中,这两个接口实际上并无区别,因为:
- 任何实现了
oop1.Number1
接口的类,也实现了oop2.Number2
; - 任何实现了
oop1.Number1
接口的对象实例都可以赋值给oop2.Number2
,反之亦然; - 在任何地方使用
oop1.Number1
接口与使用oop2.Number2
并无差异。
接下来我们定义一个实现了这两个接口的类 Number
:
type Number int;
func (n Number) Equal(i int) bool {
return int(n) == i
}
func (n Number) LessThan(i int) bool {
return int(n) < i
}
func (n Number) MoreThan(i int) bool {
return int(n) > i
}
所以下面这些赋值代码都是合法的,会编译通过:
var num1 Number = 1
var num2 oop1.Number1 = num1
var num3 oop2.Number2 = num2
此外,接口赋值并不要求两个接口完全等价(方法完全相同)。如果接口 A 的方法列表是接口 B 的方法列表的子集,那么接口 B 可以赋值给接口 A。例如,假设 Number2
接口定义如下:
type Number2 interface {
Equal(i int) bool
MoreThan(i int) bool
LessThan(i int) bool
Add(i int)
}
要让 Number
类继续保持实现这两个接口,需要在 Number
类定义中新增一个 Add
方法实现:
func (n *Number) Add(i int) {
*n = *n + Number(i)
}
接下来,将上面的接口赋值语句改写如下即可:
var num1 oop1.Number = 1
var num2 oop2.Number2 = &num1
var num3 oop1.Number1 = num2
但是反过来不行:
var num1 oop1.Number = 1
var num2 oop1.Number1 = &num1
var num3 oop2.Number2 = num2 // 这一段编译出错
因为 Number1
接口中没有 Add
方法,这一点和 Java 中子类实例可以直接赋值给父类,而父类实例不能直接赋值给子类颇有些异曲同工。
15 Comments
文章最后的 “子类实例可以直接赋值给父类,而父类实例不能直接赋值给子类” 在PHP中是指 instanceof 的意思吗? 比如 A是父类 B是子类 $a = new A $b = new B, $a instanceof A ,$b instanceof b 并且$b instanceof A
这里应该只适用于 Java,PHP 是弱类型语言,直接赋值 $a = $b, $b = $a 都是没问题的 ?
IntNumber 接口下的文字描述,安装 -> 按照?
是的 已修正
将对象实例赋值给接口,之所以要传指针&a,是因为接口里面至少一个方法在对象实例中使用了指针传值的原因,如果接口内所有的方法在被赋值的对象中不存在任何指针传值的方法,直接传值a也可以,不知道我的理解是否正确,我在代码中实际测试中确实如此
可以这么理解
以类型
a
为例,*a
可以调用归属于a
或*a
的方法,而a
只能调用归属于a
的方法,更深层次的原因是*a
可以转化为a
,而通过a
并不总是能获取*a
从“laravel学院”到“学院君”,学到了很多知识,果断订阅了
感谢支持 ?
最后的示例:
var num1 oop1.Number = 1
是不是应该是:var num1 Number = 1
我看了下之前的源码 Number 是定义在 oop1 包里面的。。。后续做教程,应该配套一个源码仓库,不同章节对应不同分支代码,这一点以后需要改进