LoginSignup
2
2

More than 1 year has passed since last update.

RUSTの型とメソッド呼び出し

Last updated at Posted at 2020-07-03

概要

半年ほど前にRUSTをかじった後放置していましたが,最近また触り始めて不変借用,可変借用,トレイトのメソッド呼び出しで混乱したのでサンプルコードを書いて動作を確認したことをまとめておく.ただし,理解が間違っているかもしれないのであしからず.

前提

RUSTでは,ある型Tとその借用型&T,可変借用型&mut Tは型として区別される.以下のコードでは,構造体Sself, &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, f3selfはパターン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)の実装を与えることで,SP両方の可変参照型に対して一気に実装を与えることができる((**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に実装したメソッドが呼ばれているが,tf3impl 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と同様に実体型のBtf1を呼び出すことはできない.また,tf3&Bは不変参照なので,実体型のBtf3を呼び出すことができない.ここでできるのは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 Stf2が呼ばれる.impl T for &Btf2を呼び出すには,(&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);
    }

型が書いてあったほうが,はっきりしていいと思う

間違い等あればご指摘ください.

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2