Rustの参照と所有権難しいですよね。今回はRustの「暗黙的な参照変換」について解説します。
下記のコードを見てください。よく考えると一貫性がないように見えませんか?
struct Foo { name: String }
impl Foo {
fn bar(&self) {
println!("{}", self.name);
}
}
fn baz(r: &String) {
println!("{}", r);
}
fn main() {
let foo: Foo = Foo { name: "some_string".to_string() };
let foo_ref: &Foo = &foo;
// (1) YES - コンパイル成功
foo.bar();
// (2) NO - コンパイルエラー
baz(foo_ref.name);
// (3) NO - コンパイルエラー
let name = foo_ref.name;
println!("{}", name);
// (4) YES - コンパイル成功
println!("{}", foo_ref.name);
// (5) YES - コンパイル成功
if "hello".to_string() < foo_ref.name {
println!("x")
} else {
println!("y")
}
}
ドット演算子の自動参照・自動逆参照
Rustにおいて、ドット演算子(.
)は自動参照と自動逆参照という機能を持っています。この機能は 左側 のオペランドに対してのみ働きます。
ケース(1)のfoo.bar()
では、foo
はFoo
型ですが、bar
メソッドは&self
(Foo
への参照)を期待しています。ドット演算子の自動参照機能により、foo
は自動的に&foo
に変換されます。
普段意識しないかもしれませんが、ドット演算子は左側にある値を必要に応じて参照したり、逆参照したりしているんですね。
参照と値の取得
2番目と3番目の例でコンパイルエラーが発生する理由はすぐにわかると思います。foo_ref.name
がString
型の値を返すからですね。
// (2) NO - コンパイルエラー
baz(foo_ref.name);
しかし、よく考えてみましょう。 先程の自動参照機能で暗黙的に変換してくれそうな気もしませんか?
自動変換はドットの右側の値には適用されないのです。
この場合、baz
関数は&String
型の参照を期待しているので、明示的に参照を取得する必要があります:
// これなら動作します
baz(&foo_ref.name);
同様に、3番目の例もname
変数にString
型の値を代入しようとしていますが、実際には参照を通して値をコピーしようとしているため、コンパイルエラーになります。
マクロの特殊な振る舞い
ここで4番目の例をみて 「おや?!」 と思いませんか?
(3)のprintln!("{}", name);
はコンパイルできないのになぜprintln!("{}", foo_ref.name);
は通るんでしょうか?
これが成功するのは、println!
マクロの特殊な振る舞いによるものです。
println!
はマクロであり、通常の関数とは異なる振る舞いをします。println!
マクロにはフォーマットの過程で必要に応じて参照演算子を追加する処理が備わっているため、コンパイルエラーが発生しません。
比較演算子の動作
5番目の例が成功する理由も、比較演算子の特殊な内部実装にあります。
// (5) YES - コンパイル成功
if "hello".to_string() < foo_ref.name {
println!("x")
} else {
println!("y")
}
Rustでは比較演算子を使う際に、内部的にオペランドに対して自動的に参照が取られる仕組みになっています。これにより、String
型同士の比較として処理され、コンパイルが成功します。
まとめ
- ドット演算子(
.
)は左側の値に対して自動的に参照・逆参照を行いますが、右側の値(フィールドやメソッドの結果)には適用されません -
println!
などのマクロは特殊な振る舞いをすることがあり、一般的な関数呼び出しとは異なる参照処理を行います - 比較演算子などの演算子も、内部的に特別な参照処理を行います
以上、知らなくても困らないであろうRustの小ネタでした