一篇理解什麼是CanSet, CanAddr?
什麼是可設定( CanSet )
首先需要先明確下,可設定是針對 reflect.Value 的。普通的變數要轉變成為 reflect.Value 需要先使用 reflect.ValueOf() 來進行轉換。 那為什麼要有這麼「可設定」的方法呢?例如下面這個例子:var x float64 = 3.4v := reflect.ValueOf(x)fmt.Println(v.CanSet()) // false
#但是, 非常明顯,由於我們傳遞的是 x 的一個複製,所以這裡根本無法改變 x 的值。這裡顯示的就是 false。
那如果我們把 x 的位址傳遞給裡面呢?下面這個範例:
var x float64 = 3.4v := reflect.ValueOf(&x)fmt.Println(v.CanSet()) // false
我們將 x 變數的位址傳遞給 reflect.ValueOf 了。應該是 CanSet 了吧。但這裡要注意一點,這裡的 v 指向的是 x 的指標。所以 CanSet 方法判斷的是 x 的指標是否可以設定。指標是肯定不能設定的,所以這裡還是回傳 false。
那麼我們下面需要可以透過這個指標的 value 值來判斷的是,這個指標所指向的元素是否可以設置,所幸 reflect 提供了 Elem() 方法來取得這個「指標所指向的元素」。
var x float64 = 3.4v := 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.4v := reflect.ValueOf(&x)if v.Elem().CanSet() { v.Elem().SetFloat(7.1)}fmt.Println(x)
對於複雜的slice, map, struct, pointer 等方法,我寫了一個例子:package mainimport (
"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}
}}
所以在呼叫 reflect.ValueOf 的時候,我們必須心裡非常明確,我們要傳遞的變數的底層結構。例如 map, 實際上傳遞的是一個指針,我們沒有必要再將他指針化了。而 slice, 實際上傳遞的是一個 SliceHeader 結構,我們在修改 Slice 的時候,必須傳遞的是 SliceHeader 的指標。這一點往往是需要我們注意的。
CanAddr在 reflect 套件裡面可以看到,除了 CanSet 之外,還有一個 CanAddr 方法。它們兩個有什麼差別呢? CanAddr 方法和 CanSet 方法不一樣的地方在於:對於一些結構體內的私有字段,我們可以取得它的位址,但不能設定它。 例如下面的範例:
package mainimport ( "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}
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 是个匿名函数
其中比较不好理解的就是 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 (https://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 就能完全理解了。
以上是什麼是CanSet, CanAddr?的詳細內容。更多資訊請關注PHP中文網其他相關文章!