Press "Enter" to skip to content

Go 面向对象编程篇(七):类型断言

在 Java、PHP 等语言的面向对象编程实现中,提供了 instanceof 关键字来进行接口和类型的断言,这种断言其实就是判定一个对象是否是某个类(包括父类)或接口的实例。

Go 语言设计地非常简单,所以没有提供类似的关键字,而是通过类型断言运算符 .(type) 来实现,其中 type 对应的就是要断言的类型。下面,我们来看下具体的使用示例。

接口类型断言

首先来看接口类型断言。

上篇教程介绍的 Number 类、Number1Number2 接口为例,在 Go 语言中,要断言 Number2 接口类型实例 num2 是否也是 Number1 接口类型(即 num2 是否实现了 Number1 接口,本质上则是 num1 是否实现了 Number1 接口),可以这么做:

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

我们通过 num2.(Number1) 这个表达式断言 num2 是否是 Number1 类型的实例,如果是,ok 值为 true,然后执行 if 语句块中的代码;否则 ok 值为 false,不执行 if 语句块中的代码。

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

结构体类型断言

接下来我们来看下结构体类型断言。

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

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

这样一来,AnimalDog 类就都实现了 IAnimal 接口,要查询 IAnimal 接口类型的实例是否是 Dog 结构体类型,可以这么做:

var animal = NewAnimal("中华田园犬")
var pet = NewPet("泰迪")
var ianimal IAnimal = NewDog(&animal, pet)
if dog, ok := ianimal.(Dog); ok {
    fmt.Println(dog.GetName())
    fmt.Println(dog.Call())
    fmt.Println(dog.FavorFood())
}

如果 ianimal 变量是 Dog 类型,则 ok 值为 true,执行 if 语句块中的代码;否则 ok 值为 false

需要注意的是,在 Go 语言结构体类型断言时,子类的实例并不归属于父类,即使子类和父类属性名和成员方法列表完全一致,因为类与类之间的「继承」是通过组合实现的,并不是 Java/PHP 中的那种父子继承关系,这是新手需要注意的地方。同理,父类实现了某个接口,不代表组合类它的子类也实现了这个接口。

比如,我们把上述代码中的 ianimal.(Dog) 替换成 ianimal.(Animal),则查询结果的 ok 值为 false。当然,由于 Dog 实现了 IAnimal 接口,所以接口类型断言 ianimal.(IAnimal) 也会成功,但是如果 Dog 没有实现该接口,则断言失败,即使父类 Animal 实现了这个接口也不行。

所以,学院君这里使用父子类来称呼,完全是为了方便大家对比理解,实际上已经和传统的面向对象编程中的父子类完全不是一个概念了,其本质原因就是 Go 使用了组合而非继承来构建类与类之间的关联和层次关系。

基于反射动态断言类型

此外,还可以基于反射在运行时动态进行类型断言,使用 reflect 包提供的 TypeOf 函数即可实现。正如我们在变长参数中演示的那样:

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 = NewAnimal("中华田园犬")
var pet = NewPet("泰迪")
var ianimal IAnimal = NewDog(&animal, pet)
fmt.Println(reflect.TypeOf(ianimal))

返回的结果是 animal.Dog

对于基本数据类型,比如 intstringbool 这些,不必通过反射,直接使用 variable.(type) 表达式即可获取 variable 变量对应的类型值:

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 语言 fmt 标准库中的 Println() 函数底层就是基于类型断言将传入参数值转化为字符串进行打印的:

func (p *pp) printArg(arg interface{}, verb rune) {
    p.arg = arg
    p.value = reflect.Value{}

    ...

    // Some types can be done without reflection.
    switch f := arg.(type) {
    case bool:
        p.fmtBool(f, verb)
    case float32:
        p.fmtFloat(float64(f), 32, verb)
    case float64:
        p.fmtFloat(f, 64, verb)
    case complex64:
        p.fmtComplex(complex128(f), 64, verb)
    case complex128:
        p.fmtComplex(f, 128, verb)
    case int:
        p.fmtInteger(uint64(f), signed, verb)
    case int8:
        p.fmtInteger(uint64(f), signed, verb)
    case int16:
        p.fmtInteger(uint64(f), signed, verb)
    case int32:
        p.fmtInteger(uint64(f), signed, verb)
    case int64:
        p.fmtInteger(uint64(f), signed, verb)
    case uint:
        p.fmtInteger(uint64(f), unsigned, verb)
    case uint8:
        p.fmtInteger(uint64(f), unsigned, verb)
    case uint16:
        p.fmtInteger(uint64(f), unsigned, verb)
    case uint32:
        p.fmtInteger(uint64(f), unsigned, verb)
    case uint64:
        p.fmtInteger(f, unsigned, verb)
    case uintptr:
        p.fmtInteger(uint64(f), unsigned, verb)
    case string:
        p.fmtString(f, verb)
    case []byte:
        p.fmtBytes(f, verb, "[]byte")
    case reflect.Value:
        // Handle extractable values with special methods
        // since printValue does not handle them at depth 0.
        if f.IsValid() && f.CanInterface() {
            p.arg = f.Interface()
            if p.handleMethods(verb) {
                return
            }
        }
        p.printValue(f, verb, 0)
    default:
        // If the type is not simple, it might have methods.
        if !p.handleMethods(verb) {
            // Need to use reflection, since the type had no
            // interface methods that could be used for formatting.
            p.printValue(reflect.ValueOf(f), verb, 0)
        }
    }
}

其中 arg 对应的是外部传入的每个待打印参数值。interface{} 表示空接口类型,在 Go 语言中,空接口可以表示任意类型,关于空接口以及它与反射结合来实现更复杂的类型功能,将是我们下篇教程重点探讨的内容。

4 Comments

  1. idClass_Lee
    idClass_Lee 2022年6月29日

    func myPrintf 需要大写为“MyPrintf”吗

发表回复