概要
半年ほど前にRUSTをかじった後放置していましたが,最近また触り始めて不変借用,可変借用,トレイトのメソッド呼び出しで混乱したのでサンプルコードを書いて動作を確認したことをまとめておく.ただし,理解が間違っているかもしれないのであしからず.
前提
RUSTでは,ある型T
とその借用型&T
,可変借用型&mut T
は型として区別される.以下のコードでは,構造体S
にself
, &self
, &mut self
のメソッドを定義し,呼び出したときにどのメソッドが呼び出されるのか,またはコンパイルエラーになるのかなどを確かめていく.なお,説明の都合上,参照型(&T)に対してその値の型(T)を実体型と表記する
pub fn get_type<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
struct S { val: i32 }
impl S {
fn new(ival: i32) -> Self {
S { val: ival }
}
fn f1(self) {
println!("val = {}", self.val);
println!("t1 type = {}", get_type(self)); // reftest::S
}
fn f2(&self) {
println!("val = {}", self.val);
println!("t2 type = {}", get_type(self)); // &reftest::S
}
fn f3(&mut self) {
self.val += 1;
println!("val = {}", self.val);
println!("t3 type = {}", get_type(self)); // &mut reftest::S
}
}
パターン1:実体型を使った呼び出し
fn test1() {
let mut s = S::new(10); // mutにしないとf3が呼べない.sはS型
s.f3(); // &mut reftest::S
s.f2(); // &reftest::S
s.f1(); // reftest::S
//s.f1(); //2回目は呼べない
}
このコードを実行すると,以下の結果を得る.
val = 11
t3 type = &mut reftrtest::S
val = 11
t2 type = &reftrtest::S
val = 11
t1 type = reftrtest::S
実体型であるs
を使って呼び出すと,それぞれ引数の型に応じてself
が実体,不変参照,可変参照として呼び出される.なお,f1
の引数は実体型self
を受け取るため,メソッド呼び出しで所有権が移動し,2回は呼び出すことはできない.
パターン2:参照型を使った呼び出し
fn test2() {
let mut s = S::new(10); // mutにしないとf3が呼べない.sはS型
let refs: &mut S = &mut s; // &mut reftest::S
refs.f3(); // &mut reftest::S
refs.f2(); // &reftest::S
//refs.f1(); // SがCopyトレイトを実装していないので,デリファレンスできない
}
これを実行すると以下の結果を得る
val = 11
t3 type = &mut reftrtest::S
val = 11
t2 type = &reftrtest::S
参照型を使った場合でも,f2
, f3
のself
はパターン1の実体型を使った場合と変化がない.ただし,refs.f1()
の呼び出しはコンパイルエラーとなる.これは,f1
は引数に実体を必要としているのに,参照型では所有権を移動できないためだと思われる.
こう考えると,参照型でメソッドを呼び出す意味はあまりないのかもしれない.
パターン3: トレイトを定義して実装する
以下のトレイトTを定義し,Sに対して実装してみる
trait T {
fn tf1(self);
fn tf2(&self);
fn tf3(&mut self);
}
impl T for S {
fn tf1(self) {
println!("tf1 val = {}", self.val);
println!("{}", get_type(self));
}
fn tf2(&self) {
println!("tf2 val = {}", self.val);
println!("{}", get_type(self));
}
fn tf3(&mut self) {
self.val += 1;
println!("tf3 val = {}", self.val);
println!("{}", get_type(self));
}
}
この状態で,実体を使って呼び出してみる
fn test3() {
println!("test3");
let mut s = S::new(10);
s.tf3(); // &mut reftest::S
s.tf2(); // &reftest::S
s.tf1(); // reftest::S
//s.tf1(); // 2回は呼べない
}
すると,以下の結果を得る
test3
tf3 val = 11
&mut reftrtest::S
tf2 val = 11
&reftrtest::S
tf1 val = 11
reftrtest::S
構造体に定義したメソッドと同様,それぞれ引数の型に応じてself
が実体,不変参照,可変参照として呼び出されている.
パターン4:&mut Tの実装を追加して参照型を使って呼び出し
RUSTの標準APIの実装でしばしば見受けられる,T
の可変参照&mut T
に対してトレイトの実装を追加する.
impl<B: T> T for &mut B {
fn tf1(self) {
println!("tf1 in &mut B");
println!("--> {}", get_type(self));
//(*self).tf1(); // コンパイルエラー
}
fn tf2(&self) {
println!("tf2 in &mut B");
println!("--> {}", get_type(self));
(**self).tf2();
}
fn tf3(&mut self) {
println!("tf3 in &mut B");
//println!("--> {}", get_type(self)); //コメントを外すとコンパイルエラーになる
(**self).tf3();
}
}
こうした実装を最初見たときには意味がわからなかったけれど,これはある型T
をトレイト境界に持つ任意のB
型に対し,&mut B
型にT
を実装する,と読む.この説明はたぶん正しいのだけれど,具体的にどういうことか分かりづらい.パターン3でS
に対してトレイトT
の実装を追加した.そしてS
だけでなく&mut S
に対してもこのトレイトを実装したい状況を考える.ここで大事なのが,S
, &S
, &mut S
は全く別の型であるということである.したがって,&mut S
に対してトレイトT
の実装を加えるならば,
impl T for &mut S {...}
とすればいいのだが,こうするとS
ではない他の型P
に対してトレイトT
を実装したとき,&mut P
にも実装するとなると同じ記述(imple T for &mut P
)をすることになる.そして,多くの場合,&mut S
でもS
と同じ振る舞いにしたい,という場合がほとんどだと思われる.そこで,
impl<B: T> T for &mut B {..}
と定義することで,T
を満たす任意の型B
の可変参照型(&mut B
)の実装を与えることで,S
とP
両方の可変参照型に対して一気に実装を与えることができる((**self).xx()
の呼び出しがそれであるが,後述).もちろん,個別の実装を与えたければ個別の実装を与える必要がある.
この状態で,以下のように可変参照型を使って以下のコードを実行してみる
println!("test4");
let mut s = S::new(10);
let mut rs = &mut s; // rsは&mut S
rs.tf3(); //&mut &mut reftrtest::S
rs.tf2(); // &&mut reftrtest::S
rs.tf1(); // &mut reftrtest::S
すると,以下の結果を得る
tf3 val = 11
&mut reftrtest::S -->(*)ここが予想を裏切る
tf2 in &mut B
--> &&mut reftrtest::S -> 二重参照型&&
tf2 val = 11
&reftrtest::S
tf1 in &mut B
--> &mut reftrtest::S
これまでの実行結果と異なり,tf3の呼び出しで直感に反した振る舞いをする.tf2
, tf1
は&mut B
に実装したメソッドが呼ばれているが,tf3
はimpl T for S
で定義したものが呼び出されている.また,tf2
の結果を見ると,self
は&&mut S
となっている.
つまり,参照型(&mut s
)を使って参照型を引数に取る(&self
)メソッド(tf2
とかtf3
)の中で,self
は二重参照型となっている.よって,&mut B
のトレイト実装で(**self).tf2()
とかしているのは,2回デリファレンスすることで&&B
からB
まで戻り,B
で定義したメソッドを呼び出してその結果を返す,という意味になる.こうすることで,任意のB
に対し,&mut B
にも同じ実装を与えている(絶対に分かりづらいと思うんだけどどうなんでしょう..).
さて,次に実体型を引数に取るtf1
で
//(*self).tf1(); // コンパイルエラー
と書いた箇所があるが,&self
が二重参照となるならself
は一重参照なので,呼び出せそうな気もするが,これは先の例と同じ,参照からは所有権を剥奪できないので,呼び出すことができない.
最後に,予想を裏切ったtf3
はどうすれば呼び出されるようになるかというと,
let mut s = S::new(10);
let mut rs = &mut s; // rsは&mut S
(&mut rs).tf3(); //&mut &mut reftrtest::S --> ここ
こんなふうに,可変参照のrs
をさらに可変参照でキャスト(&mut rs
)すると呼び出されるようになる.そして,&mut B
のトレイと実装でself
は&mut &mut S
となる.標準APIでこのような実装を見かけるが,こんな面倒くさい呼び出し方いつするのか不明..暗黙のうちに行われてるのでしょうかね...
パターン5:&Tの実装を追加して参照型を使って呼び出し
トレイトT
に対し,&mut B
の実装をしたが,&B
の実装もできるはずである(別の型なので).そこで,以下を追加する
impl<B: T> T for & B {
fn tf1(self) {
println!("tf1 in &B");
println!("**> {}", get_type(self));
}
fn tf2(&self) {
println!("tf2 in &B");
println!("**> {}", get_type(self));
(**self).tf2();
}
fn tf3(&mut self) {
println!("tf3 in &B");
//println!("--> {}", get_type(self));
//(**self).tf3(); // できない.&Bなのでmutじゃないから
}
}
もちろん実装を与えることができるが,tf1
ではパターン4と同様に実体型のB
のtf1
を呼び出すことはできない.また,tf3
も&B
は不変参照なので,実体型のB
のtf3
を呼び出すことができない.ここでできるのはtf2
の呼び出しだけである.
以下のコードを実行する
let s = S::new(10);
let rs = &s;
s.tf2(); --> impl T for Sが呼ばれる
println!("---");
(&rs).tf2(); --> impl T for &Bが呼ばれる
すると,以下の結果を得る
tf2 val = 10
&reftrtest::S
---
tf2 in &B
**> &&reftrtest::S
tf2 val = 10
&reftrtest::S
参照型(&s)でtf2
を呼び出してもimpl T for S
のtf2
が呼ばれる.impl T for &B
のtf2
を呼び出すには,(&rs).tf2()
と参照でキャストする必要がある.
結論
トレイト実装を任意の型についてその参照にもまとめて与えたい場合,可変参照に対する実装には意味があるが,不変参照の実装にはあまり意味がない(気がする).また,こんなことを毎回考えて実装する必要はたぶんなく,基本は所有権ベースで考えて実体型,参照型を選び,さらに値を書き換えるかどうかで可変参照か不変参照かを選べば良いと思う.
P.S. あるT
の可変参照&mut T
のトレイト実装があるのは,そのオブジェクトの可変参照を引数で渡し,その中でそのオブジェクトのメソッドを呼び出すことがあるからなのかも..と思いついたりしました.
追加
ある型T
のトレイト実装に対し,その可変参照への実装が必要な理由を教えていただきました.
以下のように,メソッドf1
の引数はimpl T
であり,トレイトTを実装していれば引数として与えることができる.このとき,S
でも&mut S
でも関係なくf1を呼び出したい.このとき,&mut S
の実装がなければ呼び出すことができない.
fn f1(mut v: impl T) {
v.tf3();
}
let s = S::new(20);
f1(s);
let mut s1 = S::new(30);
f1(&mut s1);
}
追記2:メソッドやトレイトのselfについて
RUSTでは,構造体に関連付ける=メソッドとして関数を定義するためにself
を用いる.そして,self
を不変実体,不変借用,可変実体,可変借用で用いる,以下の4パターンがありうる.
struct Person {
name: String,
age: u32,
}
impl Person {
// 不変実体
fn hello(self) -> () {
println!("Hello");
}
// 不変借用
fn hello2(&self) -> () {
println!("I am {} year(s) old", self.age);
}
// 可変実体
fn hello3(mut self) -> () {
println!("Hello 3");
self.age = 100;
println!("I am {} year(s) old", self.age);
}
// 可変借用
fn hello4(&mut self ) {
println!("Hello 4");
self.age = 200;
println!("I am {} year(s) old", self.age);
}
}
fn impl_test() {
let p = Person{
name: String::from("Taro"),
age: 20,
};
p.say_name().say_age();
p.hello();
//p.hello(); // 2回目はエラー.-> move セマンティクス
let mut p2 = Person::new("Jiro", 20);
p2.hello2();
p2.hello4();
p2.hello3(); // mutable move
//p2.hello4(); // moveしたのでエラー
}
self
に関しては関数の引数に型を書かないが,これはシンタックスシュガーで,型まで書くと以下のようになる.
impl Person {
fn hello(self: Self) -> () {
println!("Hello");
}
fn hello2(self: &Self) -> () {
println!("I am {} year(s) old", self.age);
}
fn hello3(mut self : Self) -> () {
println!("Hello 3");
self.age = 100;
println!("I am {} year(s) old", self.age);
}
fn hello4(self : &mut Self) { // == selfの前のmutはつけなくてもいい
println!("Hello 4");
self.age = 200;
println!("I am {} year(s) old", self.age);
}
型が書いてあったほうが,はっきりしていいと思う
間違い等あればご指摘ください.