北方的石头 發表於 2021-12-15 22:42:00

【Go反射】修改对象

<h1 id="前言">前言</h1>
<p>最近在写一个自动配置的库cfgm,其中序列化和反序列化的过程用到了大量反射,主要部分写完之后,我在这里回顾总结一下反射的基本操作。</p>
<p>上一篇【Go反射】读取对象中总结了利用反射读取对象的方法。</p>
<p>本篇总结一下写入操作,即对简单类型(int、uint、float、bool、string)、指针、切片、数组、map、结构体的修改操作,后记中讨论了<code>.CanSet()</code>的设计思想。</p>
<p>先声明一下后续代码中需要引入的包:</p>
<pre><code class="language-Go">import (
        "github.com/stretchr/testify/assert"
        "reflect"
        "testing"
)
</code></pre>
<h1 id="参考">参考</h1>
<ul>
<li>go.dev | reflect</li>
<li>go.dev | The Laws of Reflection</li>
</ul>
<h1 id="目录">目录</h1>
<ul>
<li>前言</li>
<li>参考</li>
<li>目录</li>
<li>基础知识
<ul>
<li>CanSet() 和 CanAddr()</li>
</ul>
</li>
<li>反射修改简单对象
<ul>
<li>通过Setter方法</li>
<li>直接调用Set()方法</li>
</ul>
</li>
<li>反射修改指针
<ul>
<li>将指针指向另一个对象</li>
<li>修改指针指向的对象的值</li>
<li>将指针置为nil</li>
</ul>
</li>
<li>反射修改数组
<ul>
<li>逐元素修改</li>
<li>整体修改</li>
</ul>
</li>
<li>反射修改切片
<ul>
<li>逐元素修改</li>
<li>整体修改</li>
<li>修改len和cap</li>
<li>从数组、切片创建切片</li>
<li>append</li>
</ul>
</li>
<li>反射修改结构体
<ul>
<li>查找字段并修改</li>
<li>修改私有字段</li>
</ul>
</li>
<li>反射修改map
<ul>
<li>逐对修改</li>
<li>添加键值对</li>
<li>删除键值对</li>
<li>总结SetMapIndex()</li>
</ul>
</li>
<li>总结</li>
<li>后记
<ul>
<li>为什么要设计CanSet()?</li>
<li>addressable的四条规则的合理性</li>
<li>为什么map的键和值都不是addressable的?</li>
<li>后记的总结</li>
</ul>
</li>
</ul>
<h1 id="基础知识">基础知识</h1>
<h2 id="canset-和-canaddr">CanSet() 和 CanAddr()</h2>
<p>我们通过反射修改一个对象,通常会使用到Value结构体的SetXxx()方法,而这些方法往往需要该Value结构体<code>.CanSet()</code>为true。</p>
<p>通过查看文档,我们知道<code>.CanSet()</code>为true的条件为:</p>
<blockquote>
<p>CanSet reports whether the value of v can be changed. A Value can be changed only if it is addressable and was not obtained by the use of unexported struct fields. If CanSet returns false, calling Set or any type-specific setter (e.g., SetBool, SetInt) will panic.</p>
</blockquote>
<p>即大部分情况下<code>.CanAddr()</code>为true即可,如果该Value是一个结构体的字段,则还需要满足该字段是可访问的(字段名首字母大写)。</p>
<p>而<code>.CanAddr()</code>为true的条件在文档中是这样描述的:</p>
<blockquote>
<p>CanAddr reports whether the value's address can be obtained with Addr. Such values are called addressable. A value is addressable if it is an element of a slice, an element of an addressable array, a field of an addressable struct, or the result of dereferencing a pointer. If CanAddr returns false, calling Addr will panic.</p>
</blockquote>
<p>即<code>.CanAddr()</code>描述了一个叫<code>addressable</code>的属性,这个属性描述的是Value结构体的性质(本质上是Value结构体的flag字段的一个标志位),而这个属性具有以下规则:</p>
<ol>
<li>切片中的元素;</li>
<li><code>addressable</code>的数组中的元素;</li>
<li><code>addressable</code>的结构体的字段;</li>
<li>对指针进行解引用得到的结果;</li>
</ol>
<p>其中第2、3条规则是<code>addressable</code>的传播规则,而第1、4条规则是它的产生规则。</p>
<p>当然,有一些特殊情况,我们在修改对象的时候,是不需要满足<code>.CanSet()==true</code>的,我目前已知的特例是map的<code>.SetMapIndex()</code>。</p>
<p>关于如何理解<code>.CanSet()</code>的设计,在后记中进行探讨。</p>
<h1 id="反射修改简单对象">反射修改简单对象</h1>
<h2 id="通过setter方法">通过Setter方法</h2>
<pre><code class="language-Go">func TestSetInt(t *testing.T) {
        var integer int = 1
        value := reflect.ValueOf(&amp;integer).Elem()

        value.SetInt(234)
        assert.Equal(t, 234, integer)
}
</code></pre>
<p>这里我们通过先取指针再解引用的方式,来获得了一个<code>addressable</code>的Value结构体<code>value</code>,然后调用int对应的Setter方法<code>.SetInt()</code>来写入新值。</p>
<p>类似于我们读取时使用的<code>.Int()</code>、<code>.String()</code>等方法,Value结构体也提供了对应的Setter:</p>
<table>
<thead>
<tr>
<th style="text-align: left">Kind</th>
<th style="text-align: left">方法</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">Int、Intxx</td>
<td style="text-align: left">SetInt() int64</td>
</tr>
<tr>
<td style="text-align: left">Uint、Uintxx</td>
<td style="text-align: left">SetUint() uint64</td>
</tr>
<tr>
<td style="text-align: left">String</td>
<td style="text-align: left">SetString() string</td>
</tr>
<tr>
<td style="text-align: left">Float32、Float64</td>
<td style="text-align: left">SetFloat() float64</td>
</tr>
<tr>
<td style="text-align: left">Bool</td>
<td style="text-align: left">SetBool() bool</td>
</tr>
</tbody>
</table>
<h2 id="直接调用set方法">直接调用Set()方法</h2>
<pre><code class="language-Go">func TestSetInt_Raw(t *testing.T) {
        var origin int = 1
        var target int = 234
        valueOrigin := reflect.ValueOf(&amp;origin).Elem()
        valueTarget := reflect.ValueOf(target)

        valueOrigin.Set(valueTarget)
        assert.Equal(t, 234, origin)
        origin = 567
        assert.Equal(t, 234, target)
}
</code></pre>
<p>这种方式要求获得一个目标值的Value结构体(这个结构体不要求<code>.CanAddr()</code>),目标值的Value必须拥有和旧值相同的Type(不是Kind)。</p>
<p>不过对于<strong>简单类型</strong>,当其Kind相同的时候,可以进行转换(更广泛的转换规则,在此不赘述,以后有空再研究研究):</p>
<pre><code class="language-Go">func TestSetInt_WrongType(t *testing.T) {
        type MyInteger int
        var origin int = 1
        var target MyInteger = 234
        valueOrigin := reflect.ValueOf(&amp;origin).Elem()
        valueTarget := reflect.ValueOf(target)

        // BOOM! panic: reflect.Set: value of type experiment.MyInteger is not assignable to type int
        // valueOrigin.Set(valueTarget)
        valueOrigin.Set(valueTarget.Convert(valueOrigin.Type()))
        assert.Equal(t, 234, origin)
}
</code></pre>
<p>事实上,<code>.Set()</code>方法似乎是很多情况下我们修改值唯一的选择(如指针),不过对于简单对象,使用Setter不仅更简单,而且性能略微微微微高一小些(不用检查传入的值)。</p>
<h1 id="反射修改指针">反射修改指针</h1>
<h2 id="将指针指向另一个对象">将指针指向另一个对象</h2>
<pre><code class="language-Go">func TestSetPtr(t *testing.T) {
        var integer int = 1
        ptr := &amp;integer
        ptrValue := reflect.ValueOf(&amp;ptr).Elem()
        var target int = 2
        targetValue := reflect.ValueOf(&amp;target).Elem()

        ptrValue.Set(targetValue.Addr())
        assert.Equal(t, 2, *ptr)
        assert.Equal(t, &amp;target, ptr)
        assert.NotEqual(t, &amp;integer, ptr)
}
</code></pre>
<p>通过对一个<code>addressable</code>的Value结构体调用<code>.Addr()</code>方法创建一个指向该对象的指针的Value结构体,然后将其通过<code>.Set()</code>赋值给目标指针即可。</p>
<h2 id="修改指针指向的对象的值">修改指针指向的对象的值</h2>
<pre><code class="language-Go">func TestSetPtr_ChangeTarget(t *testing.T) {
        var integer int = 1
        ptr := &amp;integer
        ptrValue := reflect.ValueOf(&amp;ptr).Elem()

        ptrValue.Elem().SetInt(2)
        assert.Equal(t, 2, integer)
}
</code></pre>
<p>指针指向的对象还是原来的对象,但是这个对象的值发生了改变。</p>
<p>由于指针解引用获得的Value结构体是<code>addressable</code>的,所以直接对其进行修改即可。</p>
<h2 id="将指针置为nil">将指针置为nil</h2>
<pre><code class="language-Go">func TestSetPtr_Nil(t *testing.T) {
        var integer int = 1
        ptr := &amp;integer
        ptrValue := reflect.ValueOf(&amp;ptr).Elem()

        ptrValue.Set(reflect.Zero(ptrValue.Type()))
        assert.Equal(t, (*int)(nil), ptr)
}
</code></pre>
<p><code>reflect.Zero()</code>创建一个给定Type的零值的反射对象(Value结构体),即默认值。注意创建的这个反射对象不是<code>addressable</code>的,但是通过<code>.Set()</code>将其赋值给一个<code>addressable</code>的反射对象后,这个反射对象仍旧是<code>addressable</code>的。</p>
<h1 id="反射修改数组">反射修改数组</h1>
<h2 id="逐元素修改">逐元素修改</h2>
<pre><code class="language-Go">func TestSetArray_Elem(t *testing.T) {
        array := [...]int{1, 2, 3}
        arrayValue := reflect.ValueOf(&amp;array).Elem()

        length := arrayValue.Len()
        for i := 0; i &lt; length; i++ {
                elemValue := arrayValue.Index(i)
                elemValue.SetInt(int64(i + 4))
        }
        assert.Equal(t, [...]int{4, 5, 6}, array)
}
</code></pre>
<p>通过前面,我们知道只要数组是<code>addressable</code>的,那么其中的元素也将是<code>addressable</code>的,所以我们仍要通过取地址再解引用来让数组为<code>addressable</code>的。</p>
<p>之后我们可以直接对其中的元素进行修改。</p>
<h2 id="整体修改">整体修改</h2>
<pre><code class="language-Go">func TestSetArray_All(t *testing.T) {
        origin := [...]int{1, 2, 3}
        target := [...]int{4, 5, 6}        // 必须长度相同
        originValue := reflect.ValueOf(&amp;origin).Elem()
        targetValue := reflect.ValueOf(target)

        originValue.Set(targetValue)
        assert.Equal(t, [...]int{4, 5, 6}, origin)
        target = 7
        assert.Equal(t, 6, origin)
}
</code></pre>
<p>整体修改时,注意两个数组的长度必须相同。</p>
<p>整体修改是对整个数组进行值拷贝,整体修改完成后,对其中一个的某元素进行修改,另一个不会随之修改(这与下文的切片是不一样的)。</p>
<h1 id="反射修改切片">反射修改切片</h1>
<h2 id="逐元素修改-1">逐元素修改</h2>
<pre><code class="language-Go">func TestSetSlice_Elem(t *testing.T) {
        slice := []int{1, 2, 3}
        sliceValue := reflect.ValueOf(slice)
        // sliceValue := reflect.ValueOf(&amp;slice).Elem()

        length := sliceValue.Len()
        for i := 0; i &lt; length; i++ {
                elemValue := sliceValue.Index(i)
                elemValue.SetInt(int64(i + 4))
        }
        assert.Equal(t, []int{4, 5, 6}, slice)
}
</code></pre>
<p>跟数组逐元素修改类似。不过由于切片中的元素无条件为<code>addressable</code>的,所以我们逐元素修改时不必像数组那样先取地址再解引用(不过其它情况仍旧需要这样做)。</p>
<h2 id="整体修改-1">整体修改</h2>
<pre><code class="language-Go">func TestSetSlice_All(t *testing.T) {
        origin := []int{1, 2, 3}
        target := []int{4, 5, 6, 7}
        originValue := reflect.ValueOf(&amp;origin).Elem()
        targetValue := reflect.ValueOf(target)

        originValue.Set(targetValue)
        assert.Equal(t, []int{4, 5, 6, 7}, origin)
        target = 8
        assert.Equal(t, 8, origin)
}
</code></pre>
<p>整体修改时,因为我们修改的是切片的描述结构体,而不是切片内的元素,所以有以下现象:</p>
<ul>
<li>需要先取地址再解引用;</li>
<li>赋值与被赋值的切片长度可以不一致;</li>
<li>赋值与被赋值的切片共享同一个底层数组,当通过一个切片修改了某个元素后,另一个切片也可能会观测到这次修改;</li>
</ul>
<h2 id="修改len和cap">修改len和cap</h2>
<pre><code class="language-Go">func TestSetSlice_LenAndCap(t *testing.T) {
        slice := []int{1, 2, 3, 4}
        sliceValue := reflect.ValueOf(&amp;slice).Elem()

        sliceValue.SetLen(3)
        assert.Equal(t, []int{1, 2, 3}, slice)
        assert.Equal(t, 4, cap(slice))
        // BOOM! panic: reflect: slice capacity out of range in SetCap
        // sliceValue.SetCap(2)
        // sliceValue.SetCap(5)
        sliceValue.SetCap(3)
        assert.Equal(t, 3, cap(slice))
}
</code></pre>
<p>通过<code>.SetLen()</code>和<code>.SetCap()</code>可以修改切片的len和cap,注意需要满足<span class="math inline">\(Len_{new} \leq Cap_{new} \leq Cap_{old}\)</span>,<span class="math inline">\(Len_{new} \leq Len_{old}\)</span>。</p>
<p>接下来两种方法本质是创建新的切片(<code>.CanAddr()</code>皆为false),不过我们一般将其视作修改切片的手段,所以这里还是将其纳入进来(将来研究反射创建对象的时候可能又要再看一遍)。</p>
<h2 id="从数组切片创建切片">从数组、切片创建切片</h2>
<p>再非反射中,我们可以通过数组、切片来创建新的切片:</p>
<pre><code class="language-Go">func TestCreatNewSlice(t *testing.T) {
        array := [...]int{1, 2, 3, 4, 5}
        slice1 := array
        slice2 := slice1
        assert.Equal(t, []int{2, 3, 4}, slice1)
        assert.Equal(t, []int{2, 3}, slice2)
        assert.Equal(t, 3, cap(slice2))
}
</code></pre>
<p>Value结构体提供了<code>.Slice()</code>和<code>.Slice3()</code>来完成这种操作:</p>
<pre><code class="language-Go">func TestSetSlice_FromArray(t *testing.T) {
        array := [...]int{1, 2, 3, 4}
        var slice []int
        sliceValue := reflect.ValueOf(&amp;slice).Elem()
        arrayValue := reflect.ValueOf(&amp;array).Elem() // arrayValue must be addressable

        sliceValue.Set(arrayValue.Slice(1, 3))
        assert.Equal(t, []int{2, 3}, slice)
        array = 5
        assert.Equal(t, 5, slice)
}

func TestSetSlice_FromSlice(t *testing.T) {
        slice := []int{1, 2, 3, 4}
        sliceValue := reflect.ValueOf(&amp;slice).Elem()

        sliceValue.Set(sliceValue.Slice3(1, 3, 3))
        assert.Equal(t, []int{2, 3}, slice)
        assert.Equal(t, 2, cap(slice))
}
</code></pre>
<p><code>.Slice()</code>和<code>.Slice3()</code>只能对切片和<code>addressable</code>的数组使用,其实质是创建了一个新的切片。通过<code>.Set()</code>方法,我们可以将新创建的切片赋值给目标切片。</p>
<h2 id="append">append</h2>
<pre><code class="language-Go">func TestSetSlice_Append(t *testing.T) {
        slice := []int{1, 2, 3}
        sliceValue := reflect.ValueOf(&amp;slice).Elem()
        elemValue := reflect.ValueOf(int(4))

        sliceValue.Set(reflect.Append(sliceValue, elemValue))
        assert.Equal(t, []int{1, 2, 3, 4}, slice)
}
</code></pre>
<p>使用起来和内置函数<code>append()</code>十分类似。</p>
<h1 id="反射修改结构体">反射修改结构体</h1>
<h2 id="查找字段并修改">查找字段并修改</h2>
<pre><code class="language-Go">func TestSetStruct_Field(t *testing.T) {
        type NameStruct struct {
                Name string
        }
        type MyStruct struct {
                NameStruct
                Age int
                NickName NameStruct
                secretName string
        }
        myStruct := MyStruct{NameStruct{"abc"}, 123, NameStruct{"def"}, "ghi"}
        structValue := reflect.ValueOf(&amp;myStruct).Elem()
        structValue.FieldByName("Name").SetString("name")
        structValue.FieldByName("Age").SetInt(35)
        structValue.FieldByName("NickName").FieldByName("Name").SetString("nick")
        // BOOM! panic: reflect: reflect.Value.SetString using value obtained using unexported field
        // structValue.FieldByName("secretName").SetString("secret")
        expect := MyStruct{NameStruct{"name"}, 35, NameStruct{"nick"}, "ghi"}
        assert.Equal(t, expect, myStruct)
}
</code></pre>
<p>通过上一篇中介绍的<code>.Field(i)</code>或者<code>.FieldByName()</code>方法获得字段的Value结构体,然后调用其<code>.Set()</code>方法或者Setter方法进行修改即可。</p>
<p>需要注意的是,私有字段(首字母小写)不可以通过反射直接修改,但可以通过一些手段来修改:</p>
<h2 id="修改私有字段">修改私有字段</h2>
<pre><code class="language-Go">func TestSetStruct_PrivateField(t *testing.T) {
        type MyStruct struct {
                privateField string
        }
        myStruct := MyStruct{"I'm private!"}
        targetStr := "No! I can access you!"
        structValue := reflect.ValueOf(&amp;myStruct).Elem()
        privateField, ok := structValue.Type().FieldByName("privateField")
        assert.True(t, ok)
        *(*string)(unsafe.Pointer(structValue.UnsafeAddr() + privateField.Offset)) = targetStr
        assert.Equal(t, MyStruct{targetStr}, myStruct)
}
</code></pre>
<p>本质是强行计算该字段的地址,然后修改该地址上的值。</p>
<h1 id="反射修改map">反射修改map</h1>
<p>由前面对于<code>addressable</code>的定义,我们知道map的子对象(所有key和value)都不是<code>addressable</code>的,所以没法像前面几种类型那样获取子对象的Value结构体,然后对其进行修改,似乎只能通过<code>.SetMapIndex()</code>来设置新值,我暂时没有找到可以直接修改的方法。</p>
<h2 id="逐对修改">逐对修改</h2>
<pre><code class="language-Go">func TestSetMap_Elem(t *testing.T) {
        dict := mapint {1: 1, 2: 2, 3: 3}
        dictValue := reflect.ValueOf(dict)
        // dictValue := reflect.ValueOf(&amp;dict).Elem()

        iter := dictValue.MapRange()
        for iter.Next() {
                key := iter.Key()
                value := iter.Value()
                dictValue.SetMapIndex(key, reflect.ValueOf(int(value.Int()) + 1))
        }
        target := mapint{1: 2, 2: 3, 3: 4}
        assert.Equal(t, target, dict)
}
</code></pre>
<p>这里因为<code>.SetMapIndex()</code>传入的key永远是在map中的,所以不会修改map的键值对个数,所以不会导致迭代器失效,所以直接只用<code>.MapRange()</code>进行遍历。</p>
<h2 id="添加键值对">添加键值对</h2>
<pre><code class="language-Go">func TestSetMap_AddElem(t *testing.T) {
        dict := mapint{1: 1, 2: 2, 3: 3}
        dictValue := reflect.ValueOf(dict)
        // dictValue := reflect.ValueOf(&amp;dict).Elem()

        keys := dictValue.MapKeys()
        for _, key := range keys {
                dictValue.SetMapIndex(reflect.ValueOf(int(key.Int())+3), reflect.ValueOf(int(4)))
        }
        target := mapint{1: 1, 2: 2, 3: 3, 4: 4, 5: 4, 6: 4}
        assert.Equal(t, target, dict)
}
</code></pre>
<p>当传入<code>.SetMapIndex()</code>的key不在map中时,将插入新的键值对,此时有可能触发扩容。注意这里不宜使用<code>.MapRange()</code>进行遍历,因为会向map添加新元素,执行结果不确定。</p>
<h2 id="删除键值对">删除键值对</h2>
<pre><code class="language-Go">func TestSetMap_DeleteElem(t *testing.T) {
        dict := mapint {1: 1, 2: 2, 3: 3, 4: 4, 5: 4, 6: 4}
        dictValue := reflect.ValueOf(dict)
        // dictValue := reflect.ValueOf(&amp;dict).Elem()

        keys := dictValue.MapKeys()
        for _, key := range keys {
                if key.Int() % 2 == 0 {
                        dictValue.SetMapIndex(key, reflect.Value{})
                }
        }
        target := mapint{1: 1, 3: 3, 5: 4}
        assert.Equal(t, target, dict)
}
</code></pre>
<p>当传入<code>.SetMapIndex()</code>的key在map中,且value为空的Value结构体,将删除该键值对。</p>
<h2 id="总结setmapindex">总结SetMapIndex()</h2>
<table>
<thead>
<tr>
<th style="text-align: left"></th>
<th style="text-align: left">key在map中</th>
<th style="text-align: left">key不在map中</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">value为空</td>
<td style="text-align: left">删除该键值对</td>
<td style="text-align: left">删除该键值对(实际上不会修改任何键值对,但是如果map扩容未完成会执行<code>growWork()</code>)</td>
</tr>
<tr>
<td style="text-align: left">value不为空</td>
<td style="text-align: left">修改map中该键对应的值</td>
<td style="text-align: left">为map添加新的键值对,可能触发扩容</td>
</tr>
</tbody>
</table>
<h1 id="总结">总结</h1>
<p>本文介绍了利用反射进行写入(修改)的操作,即对简单类型(int、uint、float、bool、string)和复杂类型(指针、切片、数组、map、结构体)的修改操作。</p>
<p>转载请注明原文地址:https://www.cnblogs.com/SnowPhoenix/p/15695730.html</p>
<hr>
<hr>
<hr>
<hr>
<h1 id="后记">后记</h1>
<h2 id="为什么要设计canset">为什么要设计CanSet()?</h2>
<p>按照《The Laws of Reflection》中的说法,<code>CanSet()</code>的设计是为了让反射的行为和非反射的情况下一致。</p>
<p>再来回顾一下决定<code>CanSet()</code>的两个要素:</p>
<ol>
<li>该Value结构体是否<code>addressable</code>;</li>
<li>如果该Value是一个结构体的字段,则还需要满足该字段是否是可访问的(字段名首字母大写);</li>
</ol>
<p>关于第二个要素的设计,按照<code>让反射的行为和非反射的情况下一致</code>的设计原则是容易解释的通的(虽然我们可能更希望反射能够提供绕过这层限制的能力),但是第一个要素要怎么解释呢?</p>
<h2 id="addressable的四条规则的合理性">addressable的四条规则的合理性</h2>
<p>我们先来看一个示例:</p>
<pre><code class="language-Go">func TestInterfaceCopy(t *testing.T) {
        produce := func(i interface{}) {
                switch v := i.(type) {
                case int:
                        v += 1
                case *int:
                        *v += 1
                }
        }

        integer := 1
        produce(integer)
        assert.Equal(t, 1, integer)
        produce(&amp;integer)
        assert.Equal(t, 2, integer)
}
</code></pre>
<p>通过示例我们看到,当一个对象绑定到一个<code>interface{}</code>时,实际上会对该对象进行一次复制。我们将<code>integer</code>直接绑定到<code>interface{}</code>上后,对<code>interface{}</code>进行的修改,实际上并不会影响原先的<code>integer</code>!</p>
<p>再来看反射中起始的函数<code>reflect.ValueOf()</code>,它接受的参数恰好就是一个<code>interface{}</code>,此时我们对返回的Value结构体进行操作很有可能并不会影响到原对象!</p>
<p>而当我们将一个指针绑定到一个<code>interface{}</code>时,对<code>interface{}</code>解引用后的修改,就可以在指针指向的对象上生效。这也是为什么<code>addressable</code>的规则中会规定解引用获得的Value结构体是<code>addressable</code>的。</p>
<p>同理,我们也就可以理解另外三条规则是怎么来的了。</p>
<p>再来个例子:</p>
<pre><code class="language-Go">func TestInterfaceChangePart(t *testing.T) {
        type MyStruct struct {
                Name string
                Age *int
                Tools []string
        }
        produce := func(i interface{}) {
                v, ok := i.(MyStruct)
                assert.True(t, ok)
                v.Name = "new"        // useless work
                *v.Age = 2
                v.Tools = "knife"
                v.Tools = append(v.Tools, "fork") // useless work
        }
        integer1 := 1
        obj := MyStruct{
                Name: "origin",
                Age: &amp;integer1,
                Tools: []string{"shovel", "pan"},
        }
        produce(obj)
        assert.Equal(t, "origin", obj.Name)
        assert.Equal(t, 2, *obj.Age)
        assert.Equal(t, []string{"shovel", "knife"}, obj.Tools)
}
</code></pre>
<p>在produce中对传入的<code>interface{}</code>进行了一系列修改,而其中的一些修改其实在退出函数后并不会影响传入的obj,而在反射中,尝试进行这些“无用”的修改就会因为<code>.CanSet()</code>为<code>false</code>而panic。</p>
<p>我们可以归纳出<code>.CanAddr()</code>为<code>true</code>的一个必要条件:</p>
<blockquote>
<p><strong>对该Value结构体的操作能够被外部观测到</strong></p>
</blockquote>
<h2 id="为什么map的键和值都不是addressable的">为什么map的键和值都不是addressable的?</h2>
<p>按照我C++的经验,不允许修改key是可以理解的,因为key关系到hash,关系到这个键值对被放到哪个bucket中,不应当被修改。</p>
<p>但是为什么不允许反射来修改value的值呢?难道说——其实Go根本就不允许修改value?</p>
<p>然后我就进行了一下尝试:</p>
<pre><code class="language-Go">func TestChangeMap(t *testing.T) {
        type MyStruct struct {
                Name string
                Age int
        }
        dict := mapMyStruct {1: {"A", 1}, 2: {"B", 2}}
        // Compile Error! cannot assign to struct field dict.Name in map
        // dict.Name = "C"
        _ = dict
}

func TestChangeMapPtr(t *testing.T) {
        type MyStruct struct {
                Name string
                Age int
        }
        dict := map*MyStruct {1: {"A", 1}, 2: {"B", 2}}
        dict.Name = "C"
        assert.Equal(t, "C", dict.Name)
}
</code></pre>
<p>果然Go语言根本就不允许直接修改value,并不是仅仅不允许通过反射修改value。我猜Go之所以不允许修改value,跟Go的map的扩容机制有关系(并不是立即扩容,而是将扩容操作分摊到map的其它操作中)。</p>
<p>那么map的值不是<code>addressable</code>也可以理解了,因为不反射也无法修改map中的value。</p>
<p>再结合不允许反射直接修改(虽然可以hack)结构体的私有字段的设计,我们就得出了<code>.CanSet()</code>为<code>true</code>的另一个必要条件:</p>
<blockquote>
<p><strong>不反射也可以完成对该对象的修改操作</strong></p>
</blockquote>
<h2 id="后记的总结">后记的总结</h2>
<p>正如《The Laws of Reflection》中所说,反射对象(Value结构体)和interface{}是息息相关的,反射的作用就是为操作<code>interface{}</code>提供工具。</p>
<p>而<code>.CanSet()</code>的设计,就是试探对反射对象修改的<strong>有效性</strong>和<strong>合法性</strong>,当对反射对象的修改有意义且合法时,修改操作才会被允许。</p>
<p>转载请注明原文地址:https://www.cnblogs.com/SnowPhoenix/p/15695730.html</p><br><br>
来源:https://www.cnblogs.com/SnowPhoenix/p/15695730.html
頁: [1]
查看完整版本: 【Go反射】修改对象