3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

メソッドにおける (&mut self) と (self) -> self の使い分け

Posted at

内容

構造体のフィールドを変更するメソッドを定義したい時、以下のどちらでも動作するように見えます。

  1. &mut self を引数とし、可変参照を受け取って処理する
  2. self を引数とし、変数の所有権を受け取って処理し、selfを返す

どちらの実装を選ぶべきなのか?という話です。

場面例

何らかのユーザ情報構造体 User が levelフィールドを持っていて、それを1増やす increase_level() メソッドを追加したい場面を考えます。
1,2両方のパターンとも メソッド呼び出しとその後の処理を記述できますが、適切なのはどちらか、というものです。

struct User {
    level : i32
}

1.のパターンでの実装

impl User {
    pub fn increase_level(&mut self) {
        self.level += 1;
    }
}

fn main () {
    let mut user = User{ level : 0 };
    user.increase_level();
    // 以降 userへ何らかの処理...
}

2.のパターンでの実装

impl User {
    pub fn increase_level(mut self) -> User {
        self.level += 1;
        self
    } 
}

fn main () {
    let user = User{ level : 0 };
    let user = user.increase_level();
    // 以降 userへ何らかの処理...
}

結論

上記場面例のように「引き続き同じインスタンスを使う場合」は1.のパターンが望ましいです。

  1. &mut self を引数とし、可変参照を受け取って処理する

メソッドを呼んだ後も引き続き同じインスタンスが使われる想定なら、1.のパターンで可変参照をとるのが適切です。
逆に「メソッド呼び出し以降そのインスタンスを使ってはいけない場合」は、2.のパターンで所有権を奪うのが適切です。

理由

The Rust Programming Language 5.3. メソッド記法
に記載のある下記の通りになります。

メソッドの一部でメソッドを呼び出したインスタンスを変更したかったら、第1引数に&mut selfを使用するでしょう。 selfだけを第1引数にしてインスタンスの所有権を奪うメソッドを定義することは稀です; このテクニックは通常、 メソッドがselfを何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用できないようにしたい場合に使用されます。

そもそも、構造体の内部状態を少し変更するだけで、所有権を移動・処理・戻す、といった処理を毎回行うのは煩わしく、実行コストもかかります。
このことは 4.1. 所有権とは? の最後でも触れられています。

ただ、私はこの文章だけでは使い分けをイメージできなかったので、説明の続きを以下に記載します。

詳細説明

今回の場合例

重要になってくるのは、以下の一文です。

selfだけを第1引数にしてインスタンスの所有権を奪うメソッドを定義することは稀です;
このテクニックは通常、 メソッドがselfを何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用できないようにしたい場合に使用されます。

今回の例では、 increase_level() を呼び出した後のUserインスタンスをどうしたいか、という点になります。

今回はメソッドの処理後も引き続き同じインスタンスを使い続け、userの名前やIDを参照したり、他の更新処理を行うことが想定されます。
となると、所有権を奪って(名前は同じでも)別物のインスタンスとして変数へ格納する2.の処理はふさわしくない、と考えられます。

これが「ユーザを削除する処理なので、以降同一インスタンスを使わないでほしい」という場合なら、所有権を奪うのが適切かもしれません。
(その場合、返り値もResult<()>など他の型に変更するのが望ましい)

標準ライブラリでの例

別の例として、標準ライブラリにおけるVecのメソッドを見てみます。
Struct std::vec::Vec

self を引数とする・所有権を奪うようなメソッドは、
leakinto_iter のような元のVecを使えないようにして全く別のインスタンスに変換する処理でしか見られません。

  • leak
    • Vec の所有権を奪い、 [T] の可変参照に変換する
pub fn leak<'a>(self) -> &'a mut [T]
where
    A: 'a, 
  • into_iter
    • Vec の所有権を奪い、イテレータへ変換する
fn into_iter(self) -> IterMut<'a, T>

逆に pushtruncate など、
同じインスタンスのまま内部状態に変更が加わるのみのメソッドは &mut self を引数に取るよう実装されています。

所有権の概念も踏まえつつ、メソッドを呼んだ後「以降そのインスタンスを使ってはいけないか」「引き続き同じインスタンスを使うか」という点を意識すると納得しやすいと思います。

そもそもの経緯

なぜこんな記事を書き出したかです。

クラスメソッドを self を引数にとるような冒頭の 2. の形でメソッドの実装を書いていたところ、
インスタンスをVecに入れループ回そうとした際にエラーが出てしまいました。

  • コード概形
/* 
確認環境
$ cargo --version
cargo 1.56.0 (4ed5d137b 2021-10-04)
$ rustc -V
rustc 1.56.1 (59eed8a2a 2021-11-01)
*/

struct User {
    level : i32
}

impl User {
    pub fn increase_level(mut self) -> User {
        self.level += 1;
        self
    }
}

struct UserTeam {
    users: Vec<User>
}

impl UserTeam {

    fn increase_members_level(mut self) -> UserTeam {
        for user in self.users.iter_mut() {
            *user = user.increase_level(); // <--------------!error!
        }
        self
    }
}

fn main () {
    let mut team  = UserTeam {
        users : vec![ User{ level : 0 } , User{ level : 10 } ]
    };
    team.increase_members_level();
}
  • エラー
error[E0507]: cannot move out of `*user` which is behind a mutable reference
  --> src/main.rs:26:21
   |
26 |             *user = user.increase_level();
   |                     ^^^^^----------------
   |                     |    |
   |                     |    `*user` moved due to this method call
   |                     move occurs because `*user` has type `User`, which does not implement the `Copy` trait
   |
note: this function takes ownership of the receiver `self`, which moves `*user`

iter_mut() で得られるイテレータは要素への可変参照です。ところが、increase_level() の引数には mut self を渡す必要があります。
Copyトレイトが実装されていればコピーしたインスタンスを渡せばいいのですが、それが実装されていないのでエラーとなる訳です。

確かにCopyトレイトを実装すればエラーは出なくなりそうですが、今回行いたいのは1インスタンス内でフィールドを変更する処理です。
他へのコピーやムーブは必要ない操作を想定しているのに、なぜこのようなエラーが出るのか?本当にCopyトレイトが必要なのか?と調べたのがきっかけです。
もっと早い段階で気づきましょう

以上になります。同じような状況になってエラー文で検索かけた方の助けになれば幸いです。

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?