Go基礎系列:介面型別斷言和type-switch

駿馬金龍發表於2018-11-01

介面型別探測:型別斷言

介面例項中儲存了實現介面的型別例項,型別的例項有兩種:值型別例項和指標型別例項。在程式執行過程中,介面例項儲存的例項型別可能會動態改變。例如:

// ins是介面例項
var ins Shaper

// ins儲存值型別的例項
ins = c1

// 一段時間後...
...

// ins儲存指標型別的例項,儲存的型別發生改變
ins = c2

// 一段時間後...

// ins可能儲存另一個型別例項
ins = s1

所以,需要一種探測介面例項所儲存的是值型別還是指標型別。

探測的方法是:ins.(Type)ins.(*Type)。它們有兩個返回值,第二個返回值是ok返回值,布林型別,第一個返回值是探測出的型別。也可以只有一個返回值:探測出的型別。

// 如果ins儲存的是值型別的Type,則輸出
if t, ok := ins.(Type); ok {
    fmt.Printf("%T
", v)
}

// 如果ins儲存的是指標型別的*Type,則輸出
if t, ok := ins.(*Type); ok {
    fmt.Printf("%T
", v)
}

// 一個返回值的探測
t := ins.(Type)
t := ins.(*Type)

以下是一個例子:

package main

import "fmt"

// Shaper 介面型別
type Shaper interface {
    Area() float64
}

// Square struct型別
type Square struct {
    length float64
}

// Square型別實現Shaper中的方法Area()
func (s Square) Area() float64 {
    return s.length * s.length
}

func main() {
    var ins1, ins2 Shaper

    // 指標型別的例項
    s1 := new(Square)
    s1.length = 3.0
    ins1 = s1
    if v, ok := ins1.(*Square); ok {
        fmt.Printf("ins1: %T
", v)
    }

    // 值型別的例項
    s2 := Square{4.0}
    ins2 = s2
    if v, ok := ins2.(Square); ok {
        fmt.Printf("ins2: %T
", v)
    }
}

上面兩個Printf都會輸出,因為它們的型別判斷都返回true。如果將ins2.(Square)改為ins2.(*Square),第二個Printf將不會輸出,因為ins2它儲存的是值型別的例項。

以下是輸出結果:

ins1: *main.Square
ins2: main.Square

特別需要注意的是,ins必須明確是介面例項。例如,以下前兩種宣告是有效的,第三種推斷型別是錯誤的,因為它可能是介面例項,也可能是型別的例項副本。

var ins Shaper     // 正確
ins := Shaper(s1)  // 正確
ins := s1          // 錯誤

當ins不能確定是介面例項時,用它來進行測試,例如ins.(Square)將會報錯:

invalid type assertion:ins.(Square) (non-interface type (type of ins) on left)

它說明了左邊的ins是非介面型別(non-interface type)。

另一方面,通過介面型別斷言(ins.(Type)),如果Type是一個介面型別,就可以判斷介面例項ins中所儲存的型別是否也實現了Type介面。例如:

var r io.Read
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

var w io.Writer
w = r.(io.Writer)

上面的r是io.Read介面的一個例項變數,它裡面儲存的是tty和它的型別,即(tty, *os.File),然後斷言r的型別,探測它裡面的型別*File是否也實現了io.Writer介面,如果實現了,則儲存到io.Writer介面的例項變數w中,這樣w例項也將儲存(tty,*os.File)

由於任意內容都實現了空介面,所以,總是可以把一個介面例項無需通過任何斷言地賦值給一個空介面例項:

var empty interface{}
empty = w

現在empty也儲存了(tty,*os.File)

type Switch結構

switch流程控制結構還可以用來探測介面例項儲存的型別。這種結構稱為type-switch

用法如下:

switch v := ins.(type) {
case *Square:
    fmt.Printf("Type Square %T
", v)
case *Circle:
    fmt.Printf("Type Circle %T
", v)
case nil:
    fmt.Println("nil value: nothing to check?")
default:
    fmt.Printf("Unexpected type %T", v)
}

其中ins.(type)中的小寫type是固定的詞語。

以下是一個使用示例:

package main

import (
    "fmt"
)

// Shaper 介面型別
type Shaper interface {
    Area() float64
}

// Circle struct型別
type Circle struct {
    radius float64
}

// Circle型別實現Shaper中的方法Area()
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

// Square struct型別
type Square struct {
    length float64
}

// Square型別實現Shaper中的方法Area()
func (s Square) Area() float64 {
    return s.length * s.length
}

func main() {
    s1 := &Square{3.3}
    whichType(s1)

    s2 := Square{3.4}
    whichType(s2)

    c1 := new(Circle)
    c1.radius = 2.3
    whichType(c1)
}

func whichType(n Shaper) {
    switch v := n.(type) {
    case *Square:
        fmt.Printf("Type Square %T
", v)
    case Square:
        fmt.Printf("Type Square %T
", v)
    case *Circle:
        fmt.Printf("Type Circle %T
", v)
    case nil:
        fmt.Println("nil value: nothing to check?")
    default:
        fmt.Printf("Unexpected type %T", v)
    }
}

上面的type-switch中,之所以沒有加上case Circle,是因為Circle只實現了指標型別的receiver,根據Method Set對介面的實現規則,只有指標型別的Circle示例才算是實現了介面Shaper,所以將值型別的示例case Circle放進type-switch是錯誤的。

相關文章