什么是可设置( 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 是个匿名函数

其中比较不好理解的就是 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 就能完全理解了。

参考文档

laws-of-reflection

go addressable 详解

Go语言_反射篇

一篇理解什么是CanSet, CanAddr?的更多相关文章

  1. java提高篇-----理解java的三大特性之封装

    在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...

  2. C++雾中风景番外篇:理解C++的复杂声明与声明解析

    在学习C系列语言的过程之中,理解C/C++的复杂声明一直是初学者很困扰的问题.笔者初学之时也深受困扰,对很多规则死记硬背.后续在阅读<C专家编程>之后,尝试在编译器的角度来理解C/C++的 ...

  3. C#基础篇 - 理解委托和事件

    1.委托 委托类似于C++中的函数指针(一个指向内存位置的指针).委托是C#中类型安全的,可以订阅一个或多个具有相同签名方法的函数指针.简单理解,委托是一种可以把函数当做参数传递的类型.很多情况下,某 ...

  4. 深入理解定时器系列第一篇——理解setTimeout和setInterval

    × 目录 [1]setTimeout [2]setInterval [3]运行机制[4]作用[5]应用 前面的话 很长时间以来,定时器一直是javascript动画的核心技术.但是,关于定时器,人们通 ...

  5. Java提高篇——理解String 及 String.intern() 在实际中的应用

    1. 首先String不属于8种基本数据类型,String是一个对象.   因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ...

  6. thymeleaf第二篇:理解原理并为后面springboot进行整合进行铺垫

    官方入门之从虚拟商店理解thymeleaf 参考文档: 简单使用Thymeleaf API渲染模板生成静态页面 邮件通知改造之Thymeleaf渲染模板生成静态页面--看懂会帮助理解springboo ...

  7. 第四篇:理解vue代码

    解释以下代码: 实现输入框中能够打字的功能 <el-input v-model="input" placeholder="在这打字"></el ...

  8. java提高篇(四)-----理解java的三大特性之多态

    面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...

  9. java提高篇(二)-----理解java的三大特性之继承

    在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...

随机推荐

  1. 研究生杂谈-1粗粒度(Coarse-grained)VS细粒度(fine-grained)

    粒度似乎是根据项目模块划分的细致程度区分的,一个项目模块(或子模块)分得越多,每个模块(或子模块)越小,负责的工作越细,就说粒度越细,否则为粗粒度. 简而言之: 粗粒度:模块的功能太过于集中. 细粒度 ...

  2. 栈帧的内部结构--局部变量表(Local Variables)

    每个栈帧中包含: 局部变量表(Local Variables) 操作数栈(Opreand Stack) 或表达式栈 动态链接 (Dynamic Linking) (或指向运行时常量的方法引用) 动态返 ...

  3. ARM架构下的Docker环境,OpenJDK官方没有8版本镜像,如何完美解决?

    为什么需要ARM架构下的OpenJDK8的Docker镜像? 对现有的Java应用,之前一直运行在x86处理器环境下,编译和运行都是JDK8,如今在树莓派的Docker环境运行(或者其他ARM架构电脑 ...

  4. 详解如何使用koa实现socket.io官网的例子

    socket.io官网中使用express实现了一个最简单的IM即时聊天,今天我们使用koa来实现一下利用 socket.io 实现消息实时推送 框架准备 1.确保你本地已经安装好了nodejs和np ...

  5. websocket+sockjs+stompjs详解及实例

    最近有项目需求要用到websocket,刚开始以为很简单,但是随着遇到问题,深入了解,才知道websocket并不是想象中的那么简单,这篇文章主要是考虑websocket在客户端的使用. 1.http ...

  6. Python练习题 002:奖金计算

    [Python练习题 002]企业发放的奖金根据利润提成.利润(I)低于或等于10万元时,奖金可提10%:利润高于10万元,低于20万元时,低于10万元的部分按10%提成,高于10万元的部分,可可提成 ...

  7. Visual C++中各种文件的作用(详细)

    参考:http://blog.sina.com.cn/s/blog_6975d67c0100r3kx.html DSW:全称是Developer Studio Workspace,最高级别的配置文件, ...

  8. 微型直流电机控制基本方法 L298N模块

    控制任务 让单个直流电机在L298N模块驱动下,完成制动.自由停车,正反转,加减速等基本动作 芯片模块及电路设计 图1 L298N芯片引脚 图2 L298N驱动模块 表1 L298N驱动模块的控制引脚 ...

  9. 【题解】[SDOI2015]星际战争

    \(\color{red}{Link}\) \(\text{Solution:}\) 观察到,如果一个时间\(T\)可以完成任务,则\(T+1\)这个时间也可以完成任务. 于是我们可以二分. 为了避免 ...

  10. DX12龙书 00 - 环境配置:通过 Visual Studio 2019 运行示例项目

    0x00 安装 Visual Studio 2019 安装 Visual Studio 2019 以及相关组件. 注:安装组件时带的 Windows 10 SDK 可以在 Individual com ...