質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.42%
クロージャ

クロージャは、プログラミング言語における関数オブジェクトの一種です。 引数以外の変数を実行時の環境ではなく、 自身が定義された環境において解決することを特徴とします。

Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

Q&A

解決済

1回答

1973閲覧

Rust のクロージャにおける mut の効果範囲

KN2018

総合スコア18

クロージャ

クロージャは、プログラミング言語における関数オブジェクトの一種です。 引数以外の変数を実行時の環境ではなく、 自身が定義された環境において解決することを特徴とします。

Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

4グッド

3クリップ

投稿2021/05/02 08:21

下記コードの (A)、(B) の部分について、mut の効果範囲に疑問を感じています。
私の Rust に関する知識が浅く、疑問自体をうまく表現できていない状態で大変申し訳ありません。
以下のような考え方で合っているのかどうか、自分ではわからない状態で悩んでいます。
すみませんが、ご教授をいただけると本当にありがたいです。

(A) においては、rect の内容が変更されないから mut は不要。
right_bottom の参照先は書き換わるけれど、right_bottom を指すアドレス自体は変更されないため。

(B) においては、func でキャプチャされる変数が、rect だけでなく rb にも及ぶ。(lt はキャプチャされない)
そのためクロージャ生成時に、rect と rb を含む構造体が生成されて、move_x() 実行時に rb が変更されると判断されて、func に mut が必要となる。

#[derive(Debug)] struct Point { x: i32, y: i32, } impl Point { fn move_x(&mut self) { self.x += 10; } } #[derive(Debug)] struct Rectangle<'a> { left_top: Point, right_bottom: &'a mut Point, } fn main() { let lt = Point { x: 1, y: 1 }; let mut rb = Point { x: 5, y: 5 }; let rect = Rectangle { left_top: lt, right_bottom: &mut rb }; // (A) rect に mut が付与されていなくても move_x()は実行可能 rect.right_bottom.move_x(); println!("rect: {:?}", rect); // (B) func に mut を付与しないと、move_x()は実行不可能 let mut func = || { rect.right_bottom.move_x(); }; func(); println!("rect: {:?}", rect); }
sutonea, yskszk63, yumetodo, tatsuya6502👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

(A) においては、rect の内容が変更されないから mut は不要。
right_bottom の参照先は書き換わるけれど、right_bottom を指すアドレス自体は変更されないため。

その考え方で合っています。

(B) においては、func でキャプチャされる変数が、rect だけでなく rb にも及ぶ。(lt はキャプチャされない)
そのためクロージャ生成時に、rect と rb を含む構造体が生成されて、move_x() 実行時に rb が変更されると判断されて、func に mut が必要となる。

なんとなく合っているような気がしますが、できるだけ正確に説明してみます。

funcmutが必要になる直接的な理由は、func();のところでFnMutcall_mut(&mut self, ..)メソッドが使われるからです。&mut selfを得るためにはself(=func)がmutでなければなりません。これは&mut rbを得るためにlet mut rbにするのと同じ理由です。

ちなみに他のFnOnceFncall..(..)メソッドでは、順にself&selfを引数に取るためfuncmutを付ける必要はありません。

クロージャが定義されたときにFnMutなどのどのトレイトが実装されるかは、以下の優先順位で決定されるようです。

  1. FnFnMutFnOnceの全てを実装する
  2. FnMutFnOnceを実装する
  3. FnOnceだけを実装する

クロージャをコールするときにどのトレイトメソッドが使われるかは、コード側で指定(制約)がなければ以下の優先順位で決定されるようです。

  1. Fnを実装しているならcall(&self, ..)
  2. FnMutを実装しているならcall_mut(&mut self, ..)
  3. FnOncecall_once(self, ..)

コード側の指定というのは、たとえば 別の質問の回答で書いた ように、impl FnOnce()を引数に取る関数にクロージャを渡したときのようなことを指します。

ご質問のコードでは使用するトレイトメソッドの指定はされておらず、またfuncmutが必要なことからcall_mut(&mut self, ..)が使われていることがわかります。ですから、そのクロージャはFnMutFnOnceを実装していると考えられます。(Fnは実装されていない)

Fnが実装されていない理由ですが、move_x()&mut self(ここでのselfPoint)を要求するからです。Fnが要求するメソッドはcall(&self, ..)ですが、&をとおして&mutを得ることができず、従ってFnを実装できません。&をとおして&mutを得られないことはご質問のコードに以下のコードを追加することで確認できます。

rust

1 // Rectangleの不変借用 2 let rect1 = &rect; 3 rect1.right_bottom.move_x(); // コンパイルエラーになる

エラーの内容は以下のとおりです。

console

1error[E0596]: cannot borrow `*rect1.right_bottom` as mutable, as it is behind a `&` reference 2 --> src/bin/main1.rs:30:5 3 | 429 | let rect1 = &rect; 5 | ----- help: consider changing this to be a mutable reference: `&mut rect` 630 | rect1.right_bottom.move_x(); 7 | ^^^^^^^^^^^^^^^^^^ `rect1` is a `&` reference, so the data it refers to cannot be borrowed as mutable

最後にキャプチャされる変数ですが、

func でキャプチャされる変数が、rect だけでなく rb にも及ぶ。(lt はキャプチャされない)

今回のケースでは以下の2種類のどちらかでキャプチャされると考えられます。

  • &mut rectをキャプチャする。(クロージャ内ではleft_topフィールドとright_bottomフィールドにアクセスできる)
  • &mut rect.right_bottomをキャプチャする。(クロージャ内ではright_bottomフィールドだけにアクセスできる)

実際のコードではright_bottomフィールドだけにアクセスするので後者が好ましく思えますが、こちらのissue を読んだ感じでは、現在のRustバージョンでは前者になっているようです。そのissueは前者から後者に変更するためのものですが、設計面で考慮することがいろいろとあり、まだ実現されてないようです。

今までの話をまとめますと、以下のようになります。

  1. funcmutが必要になる直接的な理由は、クロージャをコールするときにFnMutcall_mut(&mut self, ..)メソッドが使われるから
  2. call_mut(&mut self, ..)が使われる理由は、クロージャがFnMutFnOnceを実装しており、Fnは実装していないから
  3. クロージャがFnを実装できない理由はmove_x()メソッドが&mutを要求するから

最後に、余談ですが、2のところはコードで指定することでcall_mut(&mut self, ..)ではなく、call_once(self, ..)を使うことも可能です。

rust

1 // funcを「FnOnceを実装したトレイトオブジェクト」へと型強制することで 2 // `call_once()`を使うように仕向けられる。これによりfuncのmutは不要になるが 3 // 1回しか呼べなくなる 4 let func: Box<dyn FnOnce()> = Box::new(|| { 5 rect.right_bottom.move_x(); 6 }); 7 func(); 8 func(); // FnOnceなので2回目のコールはできない。コンパイルエラーになる

投稿2021/05/03 00:52

編集2021/05/03 01:00
tatsuya6502

総合スコア2035

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

KN2018

2021/05/03 06:03

前回させていただいた質問に引き続き、とても丁寧にご教授くださり、深く感謝しております。 前回に教えてくださったことを頭に入れて考えていたのですが、クロージャ生成による匿名構造体へのアクセスについて考えるのではなく、「& を通した参照では、&mut を取得できない」と考えるべき、ということがはっきり分かり、とても納得できました。 最後の余談の部分のコードも、初学者の私には大変参考になりました。トレイトオブジェクトが簡単にダウンキャストできたらいいのだろうけど、Rustはそういうのは難しいと聞いていますので、このように書く必要があるのかと、とても参考にさせていただいています。 クロージャというありきたりな単語でインターネット上を検索しても、知りたい情報にたどり着けず何時間もさまよってしまっていたので、的確に教えてくださり非常に深く感謝しております。 貴重なお時間をたくさん割いてくださり、本当に本当にありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.42%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問