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

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

新規登録して質問してみよう
ただいま回答率
85.48%
Rust

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

Q&A

解決済

1回答

584閲覧

クロージャ間で変数を共有できない場合にCellを使うのは適当か

trrk

総合スコア20

Rust

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

0グッド

0クリップ

投稿2022/10/02 04:23

前提

あるプログラムを作りました。
エラーが出てコンパイルできませんでしたが、Cellを使うとコンパイルできました。
動作はgetもsetも常に成功するようで問題なさそうです。

コンパイルはできましたが、この場面でCell、内部可変性を使うのが適当なのかわかっていません。
適当でしょうか?
適当ではない場合、Cellを使わないでよい方法はありますか?

実現したいこと

こういったコード

  • 入力がイテレータ(impl Iterator)
  • 入力イテレータが出す値を基本的には順番に別の関数へ渡し、その値の処理結果が得られるイテレータを得る
    • このイテレータが出す値を最終的にVecにまとめる
    • このイテレータが出す値によっては、入力イテレータが出す残りの要素を、特定の条件を満たすまで無視する

発生している問題・エラーメッセージ

Cellを使わないときのコンパイルエラー

error: captured variable cannot escape `FnMut` closure body --> src/main.rs:70:9 | 56 | let mut next_start = 0; // 無視する条件にかかわる変数 | -------------- variable defined here 57 | 58 | let main_result = iter::from_fn(|| { | - inferred to be a `FnMut` closure 59 | let start = std::mem::take(&mut next_start); | ---------- variable captured here ... 70 | Some(process_line(input_line).inspect(|(end, _)| { next_start = *end; })) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a reference to a captured variable which escapes the closure body | = note: `FnMut` closures only have access to their captured variables while they are executing... = note: ...therefore, they cannot allow references to captured variables to escape

該当のソースコード

元のコードより簡略化したものです。
Cellを使うメイン処理(コメントアウト)と使わないメイン処理の両方を書いてあります。
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=92ff444fb49504859dd6d767c9a575b9

試したこと

コンパイルが通るように色々試す

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

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

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

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

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

guest

回答1

0

ベストアンサー

クロージャ間で変数を共有できない場合にCellを使うのは適当か

Cellは実行時に僅かなオーバーヘッドがあるのと、 借用ルールに違反した場合はコンパイルエラーではなく実行時エラー(panic)になるので、 もし使わないですむなら使わないほうがいいです。(2022年10月5日 訂正:コメント欄でご指摘いただいたとおりCellがpanicするというのは誤りでした。Cellはpanicしないのが正しいです。RefCellと混同してしまいました)

しかし、今回の質問のようなコードで変数を共有するにはCellしかなさそうですので、Cellを使うのは適当だと思います。

余談ですが、ご質問のコードで試したところ、片方のクロージャーを構造体で置き換え、その構造体にIteratorを実装することで、 Cellを使わずに同じ機能を実現できました。簡略化前のコードに適用できるかはわかりませんが、参考になるかもしれません。

2022年10月5日 訂正:コメント欄でご指摘いただいたとおり、うまく動く理由はMyIter::next内でcollectしているためでした。collectせず、Iterator::Itemimpl Iteratorにすると、おっしゃる通り、inspectのクロージャーが &mut next_start をキャプチャ(捕捉)しようとするので、元のコードと同じ所有権がらみのエラーになってしまいました。

rust

1#[derive(Debug, Clone, PartialEq, Eq)] 2enum Line2 { 3 A, 4 B, 5} 6 7// クロージャーとiter::from_fn()で作っていたイテレーターを構造体で置き換える 8struct MyIter<I1, F> { 9 lines: I1, 10 process_line: F, 11 next_start: i32, 12} 13 14impl<I1, F, I2> Iterator for MyIter<I1, F> 15where 16 I1: Iterator<Item = i32>, // linesイテレーター 17 F: FnMut(i32) -> I2, // process_lineクロージャー 18 I2: Iterator<Item = (i32, Line2)>, // process_lineの戻り値(イテレーター) 19{ 20 type Item = Vec<(i32, Line2)>; 21 22 fn next(&mut self) -> Option<Self::Item> { 23 let input_line = loop { 24 let input_line = self.lines.next()?; 25 if input_line >= self.next_start { 26 break input_line; 27 } 28 println!("{}: skipped", input_line); 29 }; 30 println!("{}: processed", input_line); 31 32 Some((self.process_line)(input_line) 33 .inspect(|(end, _)| { self.next_start = *end; }) 34 .collect() 35 ) 36 } 37} 38 39fn main() { 40 // process_line の出力用 41 let mut process_line_results = [ 42 (0, &[(1, Line2::A), (1, Line2::B)][..]), 43 (1, &[]), 44 (2, &[(4, Line2::B)]), 45 // 3 はなし、4 まで飛ばすので飛ばされる 46 (4, &[]), 47 (5, &[(6, Line2::A)]), 48 ] 49 .into_iter(); 50 51 // 行の内容を処理して結果を返す処理 52 // ここでは process_line_results から順番に返すようにしている 53 // ここではクロージャだが、実際は関数 54 // 実際も戻り値は impl Iterator 55 let process_line = |i| { 56 // 計5回呼ばれる 57 let (expected_i, line_items) = process_line_results.next().unwrap(); 58 assert_eq!(i, expected_i); 59 line_items.into_iter().cloned() // impl Iterator<Item=(i32, Line2)> 60 }; 61 62 // 入力データ 63 // ここでは単なる数値 [0,1,2,3,4,5] 64 // 実際も impl Iterator 65 let lines = 0..6_i32; 66 67 // ★メイン処理 68 let main_result = MyIter { 69 lines, 70 process_line, 71 next_start: 0, 72 } 73 .flatten() 74 .collect::<Vec<_>>(); 75 76 // 処理結果出力 77 println!("main_result: {:?}", main_result); 78 assert_eq!( 79 main_result, 80 vec![(1, Line2::A), (1, Line2::B), (4, Line2::B), (6, Line2::A),] 81 ); 82}

2022年10月5日 追記

所有権の問題を回避するために、MyIter::nextがイテレーター(impl Iterator<Item=(i32, Line2)>)を返すのではなく、(i32, Line2)を返す実装にしてみました。

ただ、ここまでするなら、MyIterをイテレーターにしてmaincollectするよりも、イテレーターにせずにVec<(i32, Line2)>を返すメソッドを作る方が簡単かもしれません。

rust

1#[derive(Debug, Clone, PartialEq, Eq)] 2enum Line2 { 3 A, 4 B, 5} 6 7// クロージャーとiter::from_fn()で作っていたイテレーターを構造体で置き換える 8struct MyIter<I1, F, I2> { 9 input_lines_iter: I1, 10 process_line: F, 11 processed_lines_iter: Option<I2>, 12 next_start: i32, 13} 14 15impl<I1, F, I2> MyIter<I1, F, I2> { 16 fn new(input_lines_iter: I1, process_line: F) -> Self { 17 MyIter { 18 input_lines_iter, 19 process_line, 20 processed_lines_iter: None, 21 next_start: 0, 22 } 23 } 24} 25 26impl<I1, F, I2> Iterator for MyIter<I1, F, I2> 27where 28 I1: Iterator<Item = i32>, // input_linesイテレーター 29 F: FnMut(i32) -> I2, // process_lineクロージャー 30 I2: Iterator<Item = (i32, Line2)>, // process_lineの戻り値 31{ 32 type Item = (i32, Line2); // このイテレーターの戻り値型 33 34 fn next(&mut self) -> Option<Self::Item> { 35 loop { 36 // process_lineの戻り値のイテレーターがないなら、process_lineを呼び出して 37 // イテレーターを作る 38 if self.processed_lines_iter.is_none() { 39 let input_line = loop { 40 let input_line = self.input_lines_iter.next()?; 41 if input_line >= self.next_start { 42 break input_line; 43 } 44 println!("{}: skipped", input_line); 45 }; 46 println!("{}: processed", input_line); 47 48 self.processed_lines_iter = Some((self.process_line)(input_line)); 49 } 50 51 if let Some(pl_iter) = &mut self.processed_lines_iter { 52 // process_lineの戻り値のイテレーターから次の値を取り出す 53 if let Some(pl @ (end, _)) = pl_iter.next() { 54 self.next_start = end; 55 return Some(pl); 56 } else { 57 // イテレーターが終了していたら、最初からやり直す 58 self.processed_lines_iter = None; 59 continue; 60 } 61 } else { 62 // 全ての行を処理したので終了する 63 return None; 64 } 65 } 66 } 67} 68 69fn main() { 70 // process_line の出力用 71 let mut process_line_results = [ 72 (0, &[(1, Line2::A), (1, Line2::B)][..]), 73 (1, &[]), 74 (2, &[(4, Line2::B)]), 75 // 3 はなし、4 まで飛ばすので飛ばされる 76 (4, &[]), 77 (5, &[(6, Line2::A)]), 78 ] 79 .into_iter(); 80 81 // 行の内容を処理して結果を返す処理 82 // ここでは process_line_results から順番に返すようにしている 83 // ここではクロージャだが、実際は関数 84 // 実際も戻り値は impl Iterator 85 let process_line = |i| { 86 // 計5回呼ばれる 87 let (expected_i, line_items) = process_line_results.next().unwrap(); 88 assert_eq!(i, expected_i); 89 line_items.into_iter().cloned() // impl Iterator<Item=(i32, Line2)> 90 }; 91 92 // 入力データ 93 // ここでは単なる数値 [0,1,2,3,4,5] 94 // 実際も impl Iterator 95 let lines = 0..6_i32; 96 97 // ★メイン処理 98 // flattenが不要になった 99 let main_result = MyIter::new(lines, process_line).collect::<Vec<_>>(); 100 101 // 処理結果出力 102 println!("main_result: {:?}", main_result); 103 assert_eq!( 104 main_result, 105 vec![(1, Line2::A), (1, Line2::B), (4, Line2::B), (6, Line2::A),] 106 ); 107}

投稿2022/10/03 02:54

編集2022/10/05 09:01
tatsuya6502

総合スコア2035

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

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

trrk

2022/10/03 12:49

回答ありがとうございます! 僅かなオーバーヘッドがあるため`Cell`を使わないですむなら使わないほうがよいが、今回のようなケースでは使うのが妥当と考えられる、と理解しました。 panic周りは、こう認識しています。 ・`Cell`はpanicしない ・`RefCell`は実行時に借用ルールの検査があり、panicしうる これが正しいと考えているのですが、いただいた回答と食い違っており、この点は気になっています。 (もしかしたら`Cell`と`RefCell`を混同されただけかもしれません。) 余談もありがとうございます。簡略化前のコードにも適用できました。 また、構造体を使ったIteratorの実装方法など、参考になりました。 余談です。 ただ、コンパイルできるようになったのはinspectの後に一度collectを挟んだためのようでした。 元のコードでも、同じようにcollectをするとコンパイルできました。 collectせずにできないか試したのですが、これは無理そうでした。 MyIterのItemをimpl Iteratorにするのはunstableで、これを有効化してもnextからMyIter自身へのライフタイム?(inspectのクロージャからnext_startへアクセスするために参照が必要?)を含む戻り値を返せずに詰みました。そのため、Cellを使う形で落ち着きそうです。 https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=2f5d66c46e43315a4dd3b281a8017f2d
tatsuya6502

2022/10/05 09:06

返信が遅くなりました。おっしゃる通り、私の回答が間違っていました。正しくは、(1) Cellはpanicしない、(2) 余談のコードがうまくいくのはcollectのおかげ、となります。 MyIterのItemをimpl Iteratorにするのを、私の方でも試してみましたが、やはり所有権がらみのエラーになり、詰みました。 一応、さらに余談として、MyIter::nextがイテレーターを返すのではなく、(i32, Line2)を返すようにした例を追記しました。MyIterの中で、process_lineが返したイテレーターを直接操ることで所有権の問題を回避しています。
trrk

2022/10/06 15:27

いえ、編集してくださってありがとうございます。誤りだったのですね。 Itemを(i32, Line2)としたIteratorを試すと、簡略化前のコードでも有効でした! この実装は思いつきませんでした。確かにcollectを使ったほうが簡単そうですが、個人的になるべくcollectしたくありませんでしたし、collectもCellも使わない実装方法が知れてよかったです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問