7.2. 非空接口

接口是方法的集合,接口声明的一般形式如下:

type 接口名 interface {
    方法集合
}

方法集合中的方法,其属性包括方法名以及方法的函数签名,比如我们定义一个接口如下:

type Stringer interface {
    String() => string
}

该接口名为 Stringer,其中包含一个名为 String 的方法,该方法没有输入参数,返回值为字符串。

如果一个具体类型 T 的方法集合,是某个接口 I 的方法集合 MethodSet_i的超集,那么我们称:类型 T 满足接口 I。换句话说,设类型 T 的方法集合为 St,接口 I 的方法集合为 Si,类型 T 满足接口 I 的充要条件是:任取 m ∈ Si,存在 m' ∈ St,使得 mm' 的名字相同,且函数签名相同。

如果类型 T 满足接口 I,那么类型为 T 的值将可以被赋值给 I 型的接口值,在执行赋值操作时,类型 T 的值将被拷贝至接口值内部;这也是上一节中,空接口是万能封包器的由来,因为按照上述定义,interface{} 的方法集为空,任何类型都满足它。

接口方法可以被调用,其调用将动态切至接口值内部所包含的具体值的同名方法(如果接口值内部所包含的具体值为nil,那么调用将触发运行时异常)。非空接口是凹语言中重要的抽象手段。不同类型的对象可以满足同一个接口,使得调用者可以通过接口,按照统一的方式使用不同类型的对象,因此接口作用的本质,是一组方法约定,该约定的检查(具体类型是否满足某个接口),是编译时完成的。下面是一个具体的例子:

// 版权 @2023 凹语言 作者。保留所有权利。

type Printer interface {
    Print()
}

type T1 struct {
    i: i32
}

func T1.Print {
    println("This is T1, this.i:", this.i)
}

type T2 struct {
    s: string
}

func T2.Print {
    println("This is T2, this.s:", this.s)
}

func PrintObj(p: Printer) {
    p.Print()
}

func main {
    p: Printer

    v1: T1
    v1.i = 42
    p = &v1
    PrintObj(p) // This is T1, this.i: 42

    v2: T2
    v2.s = "你好"
    p = &v2
    PrintObj(p) // This is T2, this.s: 你好
}

由此可见,同一个接口值 p 中封装了不同的对象时,使用同样的方法使用它,其行为也会随着对象类型的不同发生变化。

由于具名类型本身无法拥有方法,而只有其引用才能拥有方法(参考6.2节),因此上例中,v1 不能赋值给 p,而只有其引用 &v1 方可。如果试图将 v1 赋值给 p,将会引发编译错误。