Go基礎系列:接口類型斷言和type-switch


接口轉回成具體類型

接口實例中可以存放各種實現了接口的類型實例,在有需要的時候,還可以通過ins.(Type)ins.(*Type)的方式將接口實例ins直接轉回Type類型的實例。

var i int = 30
var ins interface{}

// 接口實例ins中保存的是int類型
ins = i
x := ins.(int)  // 接口轉回int類型的實例i
println(x) //輸出30

但注意,這時候的i和x在底層不是同一個對象,它們的地址是不同的。

var i int = 30
var ins interface{}

ins = i
x := ins.(int)
println("i addr: ",&i,"x addr: ",&x)

輸出:

0xc042049f68
0xc042049f60

注意,接口實例轉回時,接口實例中存放的是什么類型,才能轉換成什么類型。同類型的值類型實例和指針類型實例不能互轉,不同類型更不能互轉。

在不能轉換時,Golang將直接以Panic的方式終止程序。但可以處理轉換失敗時的panic,這時需要類型斷言,也即類型檢測。

接口類型探測:類型斷言

類型探測的方式和類型轉換的方式都是ins.(Type)ins.(*Type)。當處於單個返回值上下文時,做的是類型轉換,當處於兩個返回值的上下文時,做的是類型探測。類型探測的第一個返回值是類型轉換之后的類型實例,第二個返回值是布爾型的ok返回值。

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

// 如果ins保存的是指針類型的*Type,則輸出
if t, ok := ins.(*Type); ok {
	fmt.Printf("%T\n", 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\n", v)
	}

	// 值類型的實例
	s2 := Square{4.0}
	ins2 = s2
	if v, ok := ins2.(Square); ok {
		fmt.Printf("ins2: %T\n", 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結構

直接用if v,ok := ins.(Type);ok {}的方式做類型探測在探測類型數量多時不是很方便,需要重復寫if結構。

Golang提供了switch...case結構用於做多種類型的探測,所以這種結構也稱為type-switch。這是比較方便的語法,比如可以判斷某接口如果是A類型,就執行A類型里的特有方法,如果是B類型,就執行B類型里的特有方法。

用法如下:

switch v := ins.(type) {
case *Square:
	fmt.Printf("Type Square %T\n", v)
case *Circle:
	fmt.Printf("Type Circle %T\n", 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\n", v)
	case Square:
		fmt.Printf("Type Square %T\n", v)
	case *Circle:
		fmt.Printf("Type Circle %T\n", 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是錯誤的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM