www.zhblog.net

类型断言


定义接口时使用的名称,只是一个方便取用及阅读的标示,最重要的是接口中定义的行为,以及实际的接收者类型。因此,若你打算从一个接口转换至另一个接口,只要行为符合就可以了。例如以下是可行的:

package main

import "fmt"

type ATester interface {
    test()
}

type BTester interface {
    test()
}

type Subject struct {
    name string
}

func (s *Subject) test() {
    fmt.Println(s)
}

func main() {
    var testerA ATester = &Subject{"Test"}
    var testerB BTester = testerA
    testerA.test()
    testerB.test()
}

在第二个指定时,编译器会检查testerA的类型定义,也就是接口中,是否定义了test()行为,若是则可通过编译,若否就编译错误。例如以下的情况:

package main

import "fmt"

type ATester interface {
    testA()
}

type BTester interface {
    testB()
}

type Subject struct {
    name string
}

func (s *Subject) testA() {
    fmt.Println(s)
}

func (s *Subject) testB() {
    fmt.Println(s)
}

func main() {
    var testerA ATester = &Subject{"Test"}
    var testerB BTester = testerA // 错误:ATester does not implement BTester
    testerA.testA()
    testerB.testB()
}

就算testerA存储的结构实例,确实有实现testB()这个方法,然而从编译器的角度来看,testerA的行为只有testA(),而看不到它有testB()的行为,因此上面这个范例会编译错误。

Comma-ok 类型断言

如果真的要通过编译,可以使用类型断言(Type assertion)

...同前…略

func main() {
    var testerA ATester = &Subject{"Test"}
    var testerB BTester = testerA.(BTester) 
    testerA.testA()
    testerB.testB()
}

x.(T)这个语法,x的类型是某接口,而T是预期的类型,或者是值实现的另一个接口名称,在〈接口入门〉中谈过,接口底层存储了类型与值的信息,x.(T)是在告知编译器,在执行时期再来断言类型,也就是执行时期再来判断x底层存储的值,类型是否为T,若是就返回底层存储的值。

类型断言与类型转换不同,类型转换是将值的类型转换为另一类型,编译器会检查两个类型的数据结构是否相同,若否会发生编译错误。

断言是执行时期进行的,在底下的范例中,执行时期会断言value底层存储的值,其类型为Duck

package main

import "fmt"

type Duck struct {
    name string
}

func main() {
    values := [...](interface{}){
        Duck{"Justin"},
        Duck{"Monica"},
    }

    for _, value := range values {
        duck := value.(Duck)
        fmt.Println(duck.name)
    }
}

如果value底层存储的值,其类型为实际上不是Duck类型,那么操作duck时会发生执行时期错误,为了避免这类错误发生,可以进行 Comma-ok 类型断言,例如:

package main

import "fmt"

type Duck struct {
    name string
}

func main() {
    values := [...](interface{}){
        Duck{"Justin"},
        Duck{"Monica"},
        [...]int{1, 2, 3, 4, 5},
        map[string]int{"caterpillar": 123456, "monica": 54321},
    }

    for _, value := range values {
        if duck, ok := value.(Duck); ok {
            fmt.Println(duck.name)
        }
    }
}

第一个duck变量是Duck类型,若value底层存储的值确实是Duck类型,ok变量会是true,否则ok会是false,因此,在上面的例子中,只会针对Duck显示其name的值。

在〈接口入门〉中谈过,底下的范例会是false

var acct *Account = nil
var savings Savings = acct
fmt.Println(savings == nil) // false

实际上savings底层存储的值确实是nil,透过类型断言的话可以取出。例如:

var acct *Account = nil
var savings Savings = acct
fmt.Println(savings.(*Account) == nil) // true

类型 switch 测试

依照上面的说明,如果想测试多个类型,可以用多个if...else if,例如:

package main

import "fmt"

type Duck struct {
    name string
}

func main() {
    values := [...](interface{}){
        Duck{"Justin"},
        Duck{"Monica"},
        [...]int{1, 2, 3, 4, 5},
        map[string]int{"caterpillar": 123456, "monica": 54321},
        10,
    }

    for _, value := range values {
        if duck, ok := value.(Duck); ok {
            fmt.Println(duck.name)
        } else if arr, ok := value.([5]int); ok {
            fmt.Println(arr)
        } else if passwds, ok := value.(map[string]int); ok {
            fmt.Println(passwds)
        } else if i, ok := value.(int); ok {
            fmt.Println(i)
        } else {
            fmt.Println("非预期之类型")
        }
    }
}

不过,针对这个情况,使用类型switch测试会更为适合:

package main

import "fmt"

type Duck struct {
    name string
}

func main() {
    values := [...](interface{}){
        Duck{"Justin"},
        Duck{"Monica"},
        [...]int{1, 2, 3, 4, 5},
        map[string]int{"caterpillar": 123456, "monica": 54321},
        10,
    }

    for _, value := range values {
        switch v := value.(type) {
        case Duck:
            fmt.Println(v.name)
        case [5]int:
            fmt.Println(v[0])
        case map[string]int:
            fmt.Println(v["caterpillar"])
        case int:
            fmt.Println(v)
        default:
            fmt.Println("非预期之类型")
        }
    }
}

value.(type)这样的语法,只能用在switch之中。

来看个实际的应用,在 Go 的fmt中,有个 print.go 的源码,其中有一段是针对传入的实参,是实现了Error接口或Stringer接口,若实现了Error接口,则调用其Error()方法,若实现了Stringer接口,就调用其String()方法:

720             switch v := p.arg.(type) {
721             case error:
722                 handled = true
723                 defer p.catchPanic(p.arg, verb)
724                 p.printArg(v.Error(), verb, depth)
725                 return
726 
727             case Stringer:
728                 handled = true
729                 defer p.catchPanic(p.arg, verb)
730                 p.printArg(v.String(), verb, depth)
731                 return
732             }




展开阅读全文

评论

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 心情