こちらは Goクイズ Advent Calendar 2020 - Qiita の17日目の記事です。
問題
今回はMethod values (メソッド値) の問題です。(前回のGo Language Specification輪読会で見付けたものです)
package main import "fmt" type I interface { M() int } type T struct { a int } func (t T) M() int { return t.a } func main() { var t *T = &T{a: 1} f1 := t.M var i I i = t f2 := i.M t.a = 2 fmt.Println(f1(), f2()) }
f1(), f2()
の結果を問う、引っ掛け無しの問題となります。
さて、答えはどれでしょう?
1 1
1 2
2 2
解答
https://play.golang.org/p/1tHE-qcsNtK
正解
スクロールした先にあります!
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
正解は、2の 1 2
です。
解説
この問題を解くためには、メソッド値のレシーバに設定される値がどのように定まるかを理解する必要があります。
先にまとめると、
- メソッド値のレシーバには、メソッド値を評価した時点での対象の式の値がコピーされる
- メソッド値のレシーバのコピーの挙動は、通常の変数のコピーの挙動と変わらない (と思われる)
と言う2点が気にすべきポイントとなります。
そもそもメソッド値とは?
メソッド値は、 x.M
のような形式で、静的型を持つ式 x
のメソッド M
を参照した時に得られる値です。
具体例を示します。
次のような型 MyInt
と、そのメソッド Double
があるとします。
このメソッド Double
は、単純にレシーバ自身を2倍して、その結果をint型で返します。
type MyInt int func (i MyInt) Double() int { return int(i * 2) }
この時、次のように MyInt
型の変数 a, b を宣言し、それぞれ値に 2, 3 を設定します。
var ( a MyInt = 2 b MyInt = 3 )
変数 a, b のメソッド Double
のメソッド値を、変数 fA, fB に設定して実行すると、それぞれ 4, 6 と言う値が得られます。
fA := a.Double fmt.Println(fA()) // 4 fB := b.Double fmt.Println(fB()) // 6
https://play.golang.org/p/uFslCHl1Ifn
変数 a, b に設定していた値を使って計算が行われていることから、メソッド値は メソッド値を得た時点でのレシーバの値をコピーして使用している ことがわかります。
レシーバの値のコピーの性質について
先ほどと同じ MyInt
型と Double
メソッドを利用した例です。
ここでは、同一の変数 a の値を変更してメソッド値を2回取得しています。
1度目はaに 2
が代入された状態で、2度目はaに 3
が代入された状態となっています。
var a MyInt = 2 f1 := a.Double a = 3 // 値を更新する f2 := a.Double fmt.Println(f1()) // 4 fmt.Println(f2()) // 6
結果として、f1, f2は、同一の変数から取得したメソッド値であるにも関わらず、実行結果が異なっていました。
これは、先ほど述べた通り メソッド値を得た時点でのレシーバの値をコピーしている からです。
Specにも同様の内容が書かれています。
上記の例で言う a.Double
の a
が、Specに書かれている The expression x
に相当します。
The expression x is evaluated and saved during the evaluation of the method value; the saved copy is then used as the receiver in any calls, which may be executed later.
下記の例のように、レシーバがポインタ型だった場合は値の変更がメソッド値にも反映されます。
type S struct { N int } func (s *S) Double() int { // レシーバ s はポインタ型 return s.N * 2 } func main() { var a *S = &S{ // a はポインタ型 N: 2, } f1 := a.Double a.N = 3 // 値を更新する f2 := a.Double fmt.Println(f1()) // 6 fmt.Println(f2()) // 6 }
https://play.golang.org/p/LYlufT7i4gO
変数がポインタ型でない場合、メソッドの呼び出しで &
を省略できるのと同じルールがメソッド値でも適用されます。
type S struct { N int } func (s *S) Double() int { // レシーバ s はポインタ型 return s.N * 2 } func main() { var a S = S{ // a はポインタ型でない N: 2, } f1 := a.Double // (&a).Double と書いているのと同じ a.N = 3 // 値を更新する f2 := a.Double // (&a).Double と書いているのと同じ fmt.Println(f1()) // 6 fmt.Println(f2()) // 6 }
https://play.golang.org/p/FMdoj2e8YSP
次のように、変数がポインタ型で、レシーバがポインタ型でない場合、自動で変数をdereferenceした結果がレシーバとしてコピーされるため、結果が 4, 6
となります。
これはこうでないと困りますが、非常にややこしい挙動ですね…。
type S struct { N int } func (s S) Double() int { // レシーバ s はポインタ型でない return s.N * 2 } func main() { var a *S = &S{ // a はポインタ型 N: 2, } f1 := a.Double a.N = 3 // 値を更新する f2 := a.Double fmt.Println(f1()) // 4 fmt.Println(f2()) // 6 }
https://play.golang.org/p/R9CN5BbJLXF
インタフェース型の変数のメソッド値について
メソッド値は、インタフェース型の変数からも取得することができます。
この時の値のコピーのルールがややこしく、難解な部分です。
Specには、次のように書かれています。
Although the examples above use non-interface types, it is also legal to create a method value from a value of interface type.
インタフェース型からも、非インタフェース型と同じようにメソッド値を取得できるとしか書かれていません。
もっともシンプルな例から見てみましょう。
初めに提示した MyInt
に対応する Doubler
interfaceを定義して、メソッド値を取得しています。
ここでの挙動は、先ほどの例と完全に同じで結果が 4, 6
となります。インタフェース型になっても、値がコピーされる挙動は変わりません。
type Doubler interface { Double() int } type MyInt int func (i MyInt) Double() int { return int(i * 2) } func main() { var a Doubler = MyInt(2) f1 := a.Double a = MyInt(3) // 値を更新する f2 := a.Double fmt.Println(f1()) // 4 fmt.Println(f2()) // 6 }
https://play.golang.org/p/Pm-I6kTwU3w
続いて、変数の動的型がポインタ型の例を見てみます。
これも直感的で、結果は 6, 6
となります。
type Doubler interface { Double() int } type S struct { N int } func (s *S) Double() int { return s.N * 2 } func main() { var s = &S{ // s はポインタ型 *S N: 2, } var a Doubler = s // s の静的型は Doubler, 動的型は *S f1 := a.Double s.N = 3 // 値を更新する f2 := a.Double fmt.Println(f1()) // 6 fmt.Println(f2()) // 6 }
https://play.golang.org/p/BvZD-mBGuZ1
変数の動的型がポインタ型でない場合は、interfaceを実装していない扱いとなりコンパイルが通りません。
変数 s
に対して s.Double()
を呼ぶことは出来ますが、これはあくまで (&s).Double()
の省略記法でしかなく、型Sのメソッドセットに Double
が含まれていないからです。(詳しくはMethod setsの説明等を参照ください)
type Doubler interface { Double() int } type S struct { N int } func (s *S) Double() int { return s.N * 2 } func main() { var s = S{ // s はポインタ型でない N: 2, } var a Doubler = s // compile error (型Sは、Doublerを実装していない)
https://play.golang.org/p/hcqF3aGNh-a
次が最後の難関です。
変数の動的型がポインタ型の場合、先ほどと異なり、結果は 6, 6
となります。
レシーバの型は先ほどと同じ S
であるにも関わらず、自動的に変数のdereferenceを行って値をコピーする挙動は発生しないようです。
type Doubler interface { Double() int } type S struct { N int } func (s S) Double() int { return s.N * 2 } func main() { var s = &S{ // s はポインタ型 N: 2, } var a Doubler = s f1 := a.Double s.N = 3 // 値を更新する f2 := a.Double fmt.Println(f1()) // 6 fmt.Println(f2()) // 6 }
この辺りで訳がわからなくなってくるので、もう少し丁寧に見てみましょう。
最後の難関
問題の箇所を抜き出して、比較しやすくした例です。
元の変数を v
、インタフェース型の変数を i
としています。
v.N
を書き換えた時に、全く同じ挙動をするように見えるメソッド値 fv1
, fi1
の結果は異なっていました。
type Doubler interface { Double() int } type S struct { N int } func (s S) Double() int { return s.N * 2 } func main() { var v = &S{ // s はポインタ型 N: 2, } var i Doubler = v fv1 := v.Double fi1 := i.Double v.N = 3 // 値を更新する fv2 := v.Double fi2 := i.Double fmt.Println(fv1()) // 4 fmt.Println(fi1()) // 6 fmt.Println(fv2()) // 6 fmt.Println(fi2()) // 6 }
https://play.golang.org/p/R4k1ZPehCHW
ここで、Specに メソッド値の取得時に式を評価した結果が保存される
ことが書かれていたのを思い出してみると、実はこの例は下記のように書くことも出来るのではないかと思い当たりました。
func main() { var v1 = &S{ // s はポインタ型 N: 2, } var i1 Doubler = v1 v2 := v1 // v1のコピーを作る i2 := i1 // i1のコピーを作る fv1 := v1.Double fi1 := i1.Double v1.N = 3 // 値を更新する fv2 := v2.Double fi2 := i2.Double fmt.Println(fv1()) // 4 fmt.Println(fi1()) // 6 fmt.Println(fv2()) // 6 fmt.Println(fi2()) // 6 }
https://play.golang.org/p/KYgciQm-i8D
v, i
を v1, i1
として、それをそのままコピーした v2, i2
を作り、そこから取得したメソッド値を fv2, fi2
として、先ほどと同じ内容を実行して、同じ結果が得られました。
かなり遠回りした感じがありますが、恐らく メソッド値のレシーバのコピーの挙動は、通常の変数のコピーの挙動と変わらない です。
これを意識した上でコードを読むように注意すれば、今回の問題のような挙動も読み解けるのではないかと思います。
とは言えこれはかなりしんどいので、インタフェース型の変数とメソッド値を組み合わせて使う場合は、難解な挙動があることを気を付けた上で使うようにしてみてください。
問題の解説はSkipしますが、ここまでの情報をヒントに読み直してみてください!