今回もOnyxネタです。ストックがあるのでしばらく続きます。
(過去の記事はこちら)
TL; DR
- 値に対して破壊的変更を行う場合はポインタで渡すこと!
はじめに
先月から Crafting Interpreters をOnyxで写経している1のですが、破壊的メソッドが動作せずハマってしまいました。「Goとほぼ同じルールだろ」と高をくくっていたのが敗因です
そこで本記事では、備忘録も兼ねてオブジェクトに破壊的変更を加える場合の注意点をまとめました。
バージョン
- Onyx
0.1.8(執筆時点の最新版)
構造体を代入するとコピーされる
構造体は、別の変数へ代入した際にコピーされます。そのため、別変数側を変更しても元の変数へ影響はありません。
use core {printf}
Person :: struct {
name: str;
}
main :: () {
person1 := Person.{name="Taro"};
person2 := person1;
person2.name = "Hanako";
printf("{}\n", person2); // Person { name = "Hanako" }
// 変化なし
printf("{}\n", person1); // Person { name = "Taro" }
}
解決策: 共有したい場合はポインタを使う
ポインタを渡せば、参照先は同じなので破壊的変更を元の変数へ反映させることができます。
main :: () {
person1 := &Person.{name="Taro"};
person2 := person1;
// シンタックスシュガー: (*person2).name と書く必要はない
person2.name = "Hanako";
printf("{}\n", *person2); // Person { name = "Hanako" }
// 反映される
printf("{}\n", *person1); // Person { name = "Hanako" }
}
スライスも代入するとコピーされる
個人的な驚きポイントです。ポインタと同じ挙動になると思っていたのですが、別変数側を変更しても元の変数へ影響はありませんでした。
main :: () {
slice1 := i32.[1, 2, 3];
slice2 := slice1;
slice2[1] = 100;
printf("{}\n", slice2); // [ 1, 100, 3 ]
// 変化なし
printf("{}\n", slice1); // [ 1, 2, 3 ]
}
マップはコピーされない
一方、マップの場合は別変数側の変更が反映されました。(コピーしたい場合は、次バージョンで導入される map.copy を使うと良さそうです)
use core {printf}
use core.map
Person :: struct {
name: str;
}
main :: () {
map1 := map.literal(str, i32, .[
.{"foo", 1},
]);
map2 := map1;
map1->put("foo", 100);
printf("{}\n", map2); // { "foo" => 100 }
printf("{}\n", map1); // { "foo" => 100 }
}
関数の引数が値型の場合、実引数はコピーされる
破壊的変更を行う場合、仮引数をポインタ型にする必要があります。値型の場合引数がコピーされてしまい、変更が反映されません。
特にメソッドのレシーバは要注意です2。
Counter :: struct {
value: i32 = 0;
inc :: (c: Counter) {
c.value += 1;
}
}
main :: () {
counter := Counter.{};
printf("{}\n", counter); // Counter { value = 0 }
counter->inc();
printf("{}\n", counter); // Counter { value = 0 }
}
解決策: ポインタにする
Counter :: struct {
value: i32 = 0;
inc :: (c: &Counter) {
c.value += 1;
}
}
ポインタで渡すことで、想定通りの破壊的変更が反映されます。
main :: () {
counter := Counter.{};
printf("{}\n", counter); // Counter { value = 0 }
counter->inc();
printf("{}\n", counter); // Counter { value = 1 }
}
おわりに
以上、Onyxの破壊的変更の注意点でした。こういう「コンパイルは通るけど思ったのと違う挙動になる」タイプのミスは見つかりづらくてつらみを感じます...もっとOnyx力を上げていきたいですね。