Go 言語はリフレクションを通じて変数の値を変更します

Go 言語はリフレクションを通じて変数の値を変更します

 
 
Go 言語の x、xf[1]、*p などの式は変数を表すことができますが、x + 1 や f(2) などの式は変数ではありません。変数は、値が格納されるアドレス指定可能なメモリ空間であり、格納された値はメモリ アドレスを通じて更新できます。

 

Reflect.Values にも同様の区別があります。一部の値はアドレス指定可能ですが、その他の値はアドレス指定可能ではありません。次の宣言ステートメントを考えてみましょう。

x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)

このうち、aに対応する変数はアドレスを取ることができません。 a の値は整数 2 の単なるコピーであるためです。 b の値も受け入れられないアドレスです。 c の値は依然としてアドレスではなく、ポインター &x の単なるコピーです。実際、reflect.ValueOf(x) によって返されるすべての Reflect.Value はアドレス指定できません。しかし、d については、c の逆参照メソッドによって生成され、別の変数を指すため、望ましいアドレスになります。任意の変数 x に対応するアドレス指定可能なアドレスの値を取得するには、reflect.ValueOf(&x).Elem() を呼び出します。

これに対処できるかどうかは、reflect.Value の CanAddr メソッドを呼び出すことで判断できます。

fmt.Println(a.CanAddr()) // “false”
fmt.Println(b.CanAddr()) // “false”
fmt.Println(c.CanAddr()) // “false”
fmt.Println(d.CanAddr()) // “true”

ポインターを介して間接的にreflect.Valueを取得すると、アドレス指定できない値で始まっている場合でも、その値はアドレス指定可能になります。リフレクション メカニズムでは、アドレスの取得をサポートするかどうかに関するルールはすべて類似しています。たとえば、スライスのインデックス式 e[i] には、最初の e 式がサポートしていない場合でも、アドレス指定可能なポインタが暗黙的に含まれます。

同様に、元のreflect.ValueOf(e).Index(i)の値がサポートされていない場合でも、reflect.ValueOf(e).Index(i)の値に対処することをお勧めします。

Reflect.Value を使用してラップされた値を変更する場合、従うべきルールがいくつかあります。コードがルールに従って設計および記述されていない場合、少なくともオブジェクトの値を変更することはできず、プログラムは実行中にクラッシュします。

要素を決定および取得するための関連メソッド

要素の取得、アドレスの取得、値の変更を行うためにreflect.Valueを使用する属性メソッドについては、次の表を参照してください。

反射値オブジェクトの判定と要素の取得方法
メソッド名 述べる
Elem() value 言語層*操作と同様に、値が指す要素の値を取得します。値の型がポインタまたはインターフェイスではなく、null ポインタに対して nil の値が返されるとクラッシュが発生します。
Addr() value 言語層&操作と同様に、アドレス指定可能な値のアドレスを返します。値がアドレス指定できない場合にクラッシュする
CanAddr() bool value 値がアドレス指定可能かどうかを示します
CanSet() bool value 戻り値を変更できるかどうか。値がアドレス指定可能であり、エクスポートされたフィールドである必要があります

値変更関連のメソッド

以下の表に、reflect.Value を使用して値を変更する関連メソッドを示します。

値を変更するための Reflection Value オブジェクトのメソッド
セット(x値) 値を、渡された反映値オブジェクトの値に設定します
Setlnt(x int64) 値を設定するには int64 を使用します。値の型が int、int8、int16、int32、int64 でない場合にクラッシュが発生する
SetUint(x uint64) 値を設定するには uint64 を使用します。値の型が uint、uint8、uint16、uint32、uint64 でない場合にクラッシュが発生する
SetFloat(x float64) 値を設定するには float64 を使用します。値の型が float32 でない場合、float64 はクラッシュします。
SetBool(x bool) bool を使用して値を設定します。値が bod 型でない場合にクラッシュが発生する
SetBytes(x []bytes) バイト配列[]bytes 値を設定します。値が []byte 型でない場合にクラッシュが発生する
SetString(x String) 文字列値を設定します。値が文字列型でない場合にクラッシュが発生する

上記のメソッドは、reflect.Value の CanSet が false を返し、それでも値を変更するとクラッシュします。

値の種類がわかっている場合は、その値の種類に応じた反映設定値を使用するようにしてください。

値変更の条件の 1 つ: 対処可能

リフレクションを通じて変数の値を変更するための前提条件の 1 つは、値がアドレス指定可能であることです。簡単に言えば、この変数は変更可能でなければなりません。サンプルコードは次のとおりです。

package main

import (
    "reflect"
)

func main() {

    // 整数変数aを宣言し、最初の値を割り当てる
    var a int = 1024

    // 変数aの反射値オブジェクトを取得する
    valueOfA := reflect.ValueOf(a)

    // aを1に変更しようとする(ここでクラッシュが発生します)
    valueOfA.SetInt(1)
} 

プログラムがクラッシュし、エラーが出力されます。

panic: reflect: reflect.Value.SetInt using unaddressable value

このエラーは、SetInt がアドレス指定できない値を使用していることを意味します。 Reflect.ValueOf から渡されるのは、a のアドレスではなく、a の値です。もちろん、この Reflect.Value をアドレス指定することはできません。コードを少し変更して、再度実行します。

package main

import (
    "fmt"
    "reflect"
)

func main(){

    //整数変数aを宣言して初期値を割り当てる
    var a int = 1024

    //変数aの反射値オブジェクト(aのアドレス)を取得する
    valueOfA := reflect.ValueOf(&a)

    //aのアドレスの要素(aの値)を取り出す
    valueOfA = valueOfA.Elem()

    //aの値を1に変更する
    valueOfA.SetInt(1)

    //aの値をプリントする
    fmt.Println(valueOfA.Int())
} 

コード出力は次のとおりです。

1

コードの分析は次のとおりです。

  • 14行目では、変数aの値がreflect.ValueOf()に渡されます。このとき、reflect.ValueOf()で返されるvalueOfAには変数aのアドレスが保持されます。
  • 17行目ではreflect.Value型のElem()メソッドを使用してアドレスの要素、つまりaの値を取得しています。 Reflect.Value の Elem() メソッドによって返される値の型も、Reflect.Value です。
  • 20 行目、この時点では valueOfA が a の値を表しており、アドレス指定できます。 SetInt() メソッドを使用して値を設定するときにクラッシュが発生しなくなりました。
  • 23 行目では、変更された値が正しく出力されています。

ヒント

Reflect.Value がアドレス指定できない場合、Addr() メソッドを使用して値のアドレスを取得できず、同時にクラッシュが発生します。 Reflect.ValueのAddr()メソッドは言語層の&演算に似ている、Elem()メソッドは言語層の*演算に似ているというわけではありません。言語層の操作と同等です。

値を変更するための条件の 1 つは、エクスポートされていることです。

構造体のメンバーでは、フィールドがエクスポートされていない場合、リフレクションを使用しなくてもアクセスできますが、リフレクションによる変更はできません。コードは次のとおりです。

package main

import (
    "reflect"
)

func main() {

    type dog struct {
            legCount int
    }
    //dogインスタンスの反射値を取得する
    valueOfDog := reflect.ValueOf(dog{})

    // legCountフィールドの値を取得する
    vLegCount := valueOfDog.FieldByName("legCount")

    // legCountの値を設定しようとする(ここでクラッシュが発生する)
    vLegCount.SetInt(4)
} 

プログラムは次のエラーでクラッシュします。

panic: reflect: reflect.Value.SetInt using value obtained using unexported field

エラーは次のことを意味します: SetInt() で使用される値は、エクスポートされていないフィールドからのものです。

この値を変更できるようにするには、フィールドをエクスポートする必要があります。 Dog の LegCount のメンバーの最初の文字を大文字にし、リフレクションにアクセスできるように LegCount をエクスポートします。変更されたコードは次のとおりです。

 type dog struct {
    LegCount int
} 

次に、フィールド名に従ってフィールドの値を取得するときに、文字列のフィールドの最初の文字を大文字にします。

 vLegCount:=valueOfDog.FieldByName("LegCount") 

プログラムを再度実行すると、エラーがまだ報告されていることがわかります。

panic: reflect: reflect.Value.SetInt using unaddressable value

このエラーは、13 行目で構築された valueOfDog 構造体インスタンスをアドレス指定できないため、そのフィールドを変更できないことを意味します。コードを変更し、構造体のポインターを取得し、reflect.Value の Elem() メソッドを通じて値のリフレクション値オブジェクトを取得します。変更された完全なコードは次のとおりです。

package main

import (
    "reflect"
    "fmt"
)

func main() {

    type dog struct {
            LegCount int
    }
    // 犬のインスタンスのアドレスの反射値オブジェクトを取得する
    valueOfDog := reflect.ValueOf(&dog{})

    // 犬のインスタンスのアドレス要素を取得する
    valueOfDog = valueOfDog.Elem()

    // legCountフィールドの値を取得する
    vLegCount := valueOfDog.FieldByName("LegCount")

    // legCountの値を設定しようとする(ここでクラッシュが発生する)
    vLegCount.SetInt(4)

    fmt.Println(vLegCount.Int())
} 

コード出力は次のとおりです。

4

コードの説明は次のとおりです。

  • 11 行目で、LegCount の最初の文字を大文字にして、このフィールドをエクスポートします。
  • 14行目、犬インスタンスポインタの反射値オブジェクトを取得します。
  • 17行目でdogインスタンスのポインタ要素、つまりdogのインスタンスを取得します。
  • 20 行目、dog 構造体の LegCount フィールドのメンバー値を取得します。
  • 23行目、メンバー値を変更します。
  • 25 行目で、メンバーの値を出力します。

値の変更は、表面的な意味でアドレス指定可能と呼ばれます。言い換えれば、値は「設定可能」でなければなりません。次に、変数値を変更する場合の一般的な手順は次のとおりです。

  1. この変数のアドレスを取得するか、この変数が配置されている構造体がすでにポインター型であるかを取得します。
  2. 値のラッピングには、reflect.ValueOf を使用します。
  3. 値オブジェクト (Value) の内部オブジェクトがポインターの場合、set to set を使用するとクラッシュ エラーが報告されるため、ポインター値が指す要素値オブジェクト (Value) を Value.Elem() を通じて取得します。
  4. Value.Set を使用して値を設定します。
 

「 Go 言語はリフレクションを通じて変数の値を変更します」についてわかりやすく解説!絶対に観るべきベスト2動画

【たった1時間で学べる】Go言語のプログラミング初心者向けの超入門講座【文字書き起こし、ソースコードも完全無料!】
【プログラミング入門】これからはじめるGo(Go言語/golang)- tenntenn Conference 2022