接口篇(三):接口和类型查询及转化

PHP 中的接口/类型查询

在 PHP 语言中,我们可以通过类型运算符 instanceof 判断某个变量是否属于指定类或接口的实例:

<?php

interface I {
    public function hello();
}

class A implements I {
    public function hello() {
        print("Hello from A\n");
    }
}

class B extends A {
    public function greeter() {
        print("Hello from B\n");
    }
}

$a = new A;
$b = new B;

var_dump($b instanceof B);
var_dump($b instanceof A);
var_dump($b instanceof I);

以上示例代码三个 var_dump 语句打印的结果都是 bool(true)。Go 也支持类似的查询,不过对关键字惜字如金的 Go 语言不是通过类似 instanceof 这种类型运算符来实现接口和类型查询的,那么它是怎么实现的呢?下面我们来一一揭晓。

Go 语言的接口/类型查询

接口查询和转化

首先来看接口查询。

上篇教程介绍的 Number 类、Number1Number2 接口为例,在 Go 语言中,要查询接口 Number2 指向的对象实例 num2 是否属于接口 Number1,可以这么做:

var num1 oop1.Number = 1;
var num2 oop2.Number2 = &num1;
if num3, ok := num2.(oop1.Number1); ok {
    fmt.Println(num3.Equal(1))
}

我们通过 num2.(oop1.Number1) 这个表达式判断 num2 是否是 Number1 的实例,如果是,则返回转化为 Number1 接口类型的实例 num3ok 值为 true,然后执行 if 语句块中的代码;否则 ok 值为 false,不执行 if 语句块中的代码。所以 num2.(oop1.Number1) 做了两件事情,一个是做接口查询,将查询结果作为第二个返回值,另一个是对类型进行转化,转化后的类型是圆括号中对应的查询接口。

需要注意的是,接口查询是否成功要在运行期才能够确定,它不像接口赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。

类型查询和转化

接下来我们来看下类型查询。

Go 语言的类型查询实现语法和接口查询一样,我们以前面类的继承教程中定义的 AnimalDog 类为例,它们都位于 oop 包中,由于接口/类型查询语法左侧的变量类型必须是接口类型,所以我们需要在 oop 包中新增一个 IAnimal 接口(首字母大写才能在包外可见,这一点和类名、方法名、变量名、属性字段名一样):

type IAnimal interface {
    GetName() string
    Call() string
    FavorFood() string
}

这样一来,AnimalDog 类都实现了 IAnimal 接口,要查询 IAnimal 接口指向的实例是否属于 Dog 类型,可以这么做(Animal 中的 name 属性首字母改成大写):

var animal = oop.Animal{"小狗"}
var ianimal oop.IAnimal = oop.Dog{animal}
if dog, ok := ianimal.(oop.Dog); ok {
    fmt.Println(dog.GetName())
}

同样,如果 ianimal 变量是 oop.Dog 类型,则 ok 值为 truedog 值为转化为 Dog 类型后的实例,执行 if 语句块中的代码;否则 ok 值为 false

需要注意的是,在 Go 语言类型查询时,归属于子类的实例并不归属于父类,因为类与类之间的「继承」是通过组合实现的,并不是 PHP/Java 语言中的那种父子继承关系,比如上述代码中我们把 ianimal.(oop.Dog) 替换成 ianimal.(oop.Animal) 则查询结果 ok 值为 false

类型查询并不经常使用,它更多是个补充,需要配合接口查询使用,此外,还可以利用反射进行类型查询,正如我们在变长参数教程中演示的那样:

func myPrintf(args ...interface{}) {
    for _, arg := range args {
        switch reflect.TypeOf(arg).Kind() {
        case reflect.Int:
            fmt.Println(arg, "is an int value.")
        case reflect.String:
            fmt.Printf("\"%s\" is a string value.\n", arg)
        case reflect.Array:
            fmt.Println(arg, "is an array type.")
        default:
            fmt.Println(arg, "is an unknown type.")
        }
    }
}

因此,如果要获取 ianimal 的实际类型,可以通过 reflect.TypeOf(ianimal) 获取:

var animal = oop.Animal{"小狗"}
var ianimal oop.IAnimal = oop.Dog{animal}
fmt.Println(reflect.TypeOf(ianimal));
if dog, ok := ianimal.(oop.Dog); ok {
    fmt.Println(dog.GetName())
}
fmt.Println(reflect.TypeOf(ianimal));

转化前后打印的类型值都是 oop.Dog

对于基本数据类型,比如 intstringbool 这些,不必通过反射,直接通过 type 关键字即可获取对应的类型值:

func myPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
        case int:
            fmt.Println(arg, "is an int value.")
        case string:
            fmt.Printf("\"%s\" is a string value.\n", arg)
        case bool:
            fmt.Println(arg, "is a bool value.")
        default:
            fmt.Println(arg, "is an unknown type.")
        }
    }
}

Go 语言标准库中的 Println() 函数底层就是基于类似的类型查询对传入参数值进行打印的。

上一篇: 接口篇(二):通过接口赋值实现接口与实现类的映射

下一篇: 接口篇(四):通过接口组合实现接口继承