一篇理解什麼是CanSet, CanAddr?

什麼是可設置( CanSet )

首先需要先明確下,可設置是針對 reflect.Value 的。普通的變數要轉變成為 reflect.Value 需要先使用 reflect.ValueOf() 來進行轉化。

那麼為什麼要有這麼一個「可設置」的方法呢?比如下面這個例子:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println(v.CanSet()) // false

golang 裡面的所有函數調用都是值複製,所以這裡在調用 reflect.ValueOf 的時候,已經複製了一個 x 傳遞進去了,這裡獲取到的 v 是一個 x 複製體的 value。那麼這個時候,我們就希望知道我能不能通過 v 來設置這裡的 x 變數。就需要有個方法來輔助我們做這個事情: CanSet()

但是, 非常明顯,由於我們傳遞的是 x 的一個複製,所以這裡根本無法改變 x 的值。這裡顯示的就是 false。

那麼如果我們把 x 的地址傳遞給裡面呢?下面這個例子:

var x float64 = 3.4
v := reflect.ValueOf(&x)
fmt.Println(v.CanSet()) // false

我們將 x 變數的地址傳遞給 reflect.ValueOf 了。應該是 CanSet 了吧。但是這裡卻要注意一點,這裡的 v 指向的是 x 的指針。所以 CanSet 方法判斷的是 x 的指針是否可以設置。指針是肯定不能設置的,所以這裡還是返回 false。

那麼我們下面需要可以通過這個指針的 value 值來判斷的是,這個指針指向的元素是否可以設置,所幸 reflect 提供了 Elem() 方法來獲取這個「指針指向的元素」。

var x float64 = 3.4
v := reflect.ValueOf(&x)
fmt.Println(v.Elem().CanSet()) // true

終於返回 true 了。但是這個 Elem() 使用的時候有個前提,這裡的 value 必須是指針對象轉換的 reflect.Value。(或者是介面對象轉換的 reflect.Value)。這個前提不難理解吧,如果是一個 int 類型,它怎麼可能有指向的元素呢?所以,使用 Elem 的時候要十分注意這點,因為如果不滿足這個前提,Elem 是直接觸發 panic 的。

在判斷完是否可以設置之後,我們就可以通過 SetXX 系列方法進行對應的設置了。

var x float64 = 3.4
v := reflect.ValueOf(&x)
if v.Elem().CanSet() {
    v.Elem().SetFloat(7.1)
}
fmt.Println(x)

更複雜的類型

對於複雜的 slice, map, struct, pointer 等方法,我寫了一個例子:

package main

import (
	"fmt"
	"reflect"
)

type Foo interface {
	Name() string
}

type FooStruct struct {
	A string
}

func (f FooStruct) Name() string {
	return f.A
}

type FooPointer struct {
	A string
}

func (f *FooPointer) Name() string {
	return f.A
}

func main() {
	{
		// slice
		a := []int{1, 2, 3}
		val := reflect.ValueOf(&a)
		val.Elem().SetLen(2)
		val.Elem().Index(0).SetInt(4)
		fmt.Println(a) // [4,2]
	}
	{
		// map
		a := map[int]string{
			1: "foo1",
			2: "foo2",
		}
		val := reflect.ValueOf(&a)
		key3 := reflect.ValueOf(3)
		val3 := reflect.ValueOf("foo3")
		val.Elem().SetMapIndex(key3, val3)
		fmt.Println(val) // &map[1:foo1 2:foo2 3:foo3]
	}
	{
		// map
		a := map[int]string{
			1: "foo1",
			2: "foo2",
		}
		val := reflect.ValueOf(a)
		key3 := reflect.ValueOf(3)
		val3 := reflect.ValueOf("foo3")
		val.SetMapIndex(key3, val3)
		fmt.Println(val) // &map[1:foo1 2:foo2 3:foo3]
	}
	{
		// struct
		a := FooStruct{}
		val := reflect.ValueOf(&a)
		val.Elem().FieldByName("A").SetString("foo2")
		fmt.Println(a) // {foo2}
	}
	{
		// pointer
		a := &FooPointer{}
		val := reflect.ValueOf(a)
		val.Elem().FieldByName("A").SetString("foo2")
		fmt.Println(a) //&{foo2}
	}
}

上面的例子如果都能理解,那基本上也就理解了 CanSet 的方法了。

特別可以關注下,map,pointer 在修改的時候並不需要傳遞指針到 reflect.ValueOf 中。因為他們本身就是指針。

所以在調用 reflect.ValueOf 的時候,我們必須心裡非常明確,我們要傳遞的變數的底層結構。比如 map, 實際上傳遞的是一個指針,我們沒有必要再將他指針化了。而 slice, 實際上傳遞的是一個 SliceHeader 結構,我們在修改 Slice 的時候,必須要傳遞的是 SliceHeader 的指針。這點往往是需要我們注意的。

CanAddr

在 reflect 包裡面可以看到,除了 CanSet 之外,還有一個 CanAddr 方法。它們兩個有什麼區別呢?

CanAddr 方法和 CanSet 方法不一樣的地方在於:對於一些結構體內的私有欄位,我們可以獲取它的地址,但是不能設置它。

比如下面的例子:

package main

import (
	"fmt"
	"reflect"
)

type FooStruct struct {
	A string
	b int
}


func main() {
	{
		// struct
		a := FooStruct{}
		val := reflect.ValueOf(&a)
		fmt.Println(val.Elem().FieldByName("b").CanSet())  // false
		fmt.Println(val.Elem().FieldByName("b").CanAddr()) // true
	}
}


所以,CanAddr 是 CanSet 的必要不充分條件。一個 Value 如果 CanAddr, 不一定 CanSet。但是一個變數如果 CanSet,它一定 CanAddr。

源碼

假設我們要實現這個 Value 元素 CanSet 或者 CanAddr,我們大概率會相到使用標記位標記。事實也確實是這樣。

我們先看下 Value 的結構:

type Value struct {
	typ *rtype
	ptr unsafe.Pointer
	flag
}

這裡要注意的就是,它是一個嵌套結構,嵌套了一個 flag,而這個 flag 本身就是一個 uintptr。

type flag uintptr

這個 flag 非常重要,它既能表達這個 value 的類型,也能表達一些元資訊(比如是否可定址等)。flag雖然是uint類型,但是它用位來標記表示。

首先它需要表示類型,golang 中的類型有27個:

const (
	Invalid Kind = iota
	Bool
	Int
	Int8
	Int16
	Int32
	Int64
	Uint
	Uint8
	Uint16
	Uint32
	Uint64
	Uintptr
	Float32
	Float64
	Complex64
	Complex128
	Array
	Chan
	Func
	Interface
	Map
	Ptr
	Slice
	String
	Struct
	UnsafePointer
)

所以使用5位(2^5-1=63)就足夠放這麼多類型了。所以 flag 的低5位是結構類型。

第六位 flagStickyRO: 標記是否是結構體內部私有屬性
第七位 flagEmbedR0: 標記是否是嵌套結構體內部私有屬性
第八位 flagIndir: 標記 value 的ptr是否是保存了一個指針
第九位 flagAddr: 標記這個 value 是否可定址
第十位 flagMethod: 標記 value 是個匿名函數

20201026181333

其中比較不好理解的就是 flagStickyRO,flagEmbedR0

看下面這個例子:


type FooStruct struct {
	A string
	b int
}

type BarStruct struct {
	FooStruct
}

{
    	b := BarStruct{}
        val := reflect.ValueOf(&b)
        c := val.Elem().FieldByName("b")
		fmt.Println(c.CanAddr())
}

這個例子中的 c 的 flagEmbedR0 標記位就是1了。

所以我們再回去看 CanSet 和 CanAddr 方法

func (v Value) CanAddr() bool {
	return v.flag&flagAddr != 0
}

func (v Value) CanSet() bool {
	return v.flag&(flagAddr|flagRO) == flagAddr
}

他們的方法就是把 value 的 flag 和 flagAddr 或者 flagRO (flagStickyRO,flagEmbedR0) 做「與」操作。

而他們的區別就是是否判斷 flagRO 的兩個位。所以他們的不同換句話說就是「判斷這個 Value 是否是私有屬性」,私有屬性是只讀的。不能Set。

應用

在開發 collection (//github.com/jianfengye/collection)庫的過程中,我就用到這麼一個方法。我希望設計一個方法 func (arr *ObjPointCollection) ToObjs(objs interface{}) error,這個方法能將 ObjPointCollection 中的 objs reflect.Value 設置為參數 objs 中。

func (arr *ObjPointCollection) ToObjs(objs interface{}) error {
	arr.mustNotBeBaseType()

	objVal := reflect.ValueOf(objs)
	if objVal.Elem().CanSet() {
		objVal.Elem().Set(arr.objs)
		return nil
	}
	return errors.New("element should be can set")
}

使用方法:

func TestObjPointCollection_ToObjs(t *testing.T) {
	a1 := &Foo{A: "a1", B: 1}
	a2 := &Foo{A: "a2", B: 2}
	a3 := &Foo{A: "a3", B: 3}

	bArr := []*Foo{}
	objColl := NewObjPointCollection([]*Foo{a1, a2, a3})
	err := objColl.ToObjs(&bArr)
	if err != nil {
		t.Fatal(err)
	}
	if len(bArr) != 3 {
		t.Fatal("toObjs error len")
	}
	if bArr[1].A != "a2" {
		t.Fatal("toObjs error copy")
	}
}

總結

CanAddr 和 CanSet 剛接觸的時候是會有一些懵逼,還是需要稍微理解下 reflect.Value 的 flag 就能完全理解了。

參考文檔

laws-of-reflection
go addressable 詳解
Go語言_反射篇

Tags: