Closed10

Rustのメモ

フラワーフラワー

この辺を調査しておきたい

  • Rustのクロージャの使い分け(Fn, FnMut, FnOnce)と直接callする方法
  • iteratorでの所有権でおきがちなエラーとその対処
  • iteratortoolsの便利メソッド(batchingとか)の使い方
フラワーフラワー

Rustのクロージャの使い分け

Rustは以下の3つのクロージャが存在する。使い分けはキャプチャした値の所有権をどのようにするかで変わる。(キャプチャの挙動についてはここでは言及しない。)

  1. Fn
  2. FnMut
  3. FnOnce

FnはFnMutの、FnMutはFnOnceのサブトレイトである。(詳しくは実装を参照)
つまり、それぞれのトレイトの自由度は

FnOnce > FnMut > Fn

の順になる。

ここで、それぞれの挙動(キャプチャした値の使い方)を見ていく。

1. Fn

Fnはキャプチャした値を参照として受け取る型である。
キャプチャした値を変更も消費もしないので、何度でも同じクロージャを呼ぶことができるし、キャプチャした値を後で参照することができる(しかもキャプチャした値は不変)。

fn main() {
    let x = 1;
    let f = || x + 2;
    test_closure(f);
    test_closure(f);
    test_closure(f);
    assert_eq!(1, x);
}
fn test_closure<F: Fn() -> i32>(f: F) {
    assert_eq!(3, f());
}

もしキャプチャした値を変更 or 消費しようとするとコンパイルエラーになる。

  • キャプチャした値を変更しようとした場合
fn main() {
    let x = 1;
    let f = || x += 2;  // コンパイルエラー
    test_closure(f);
}
fn test_closure<F: Fn()>(f: F) {
    f();
}
  • キャプチャした値をmoveしようとした場合(CopyしないようにサンプルはStringにしている。)
fn main() {
    let x = String::from("a");
    let f = move || x;    // コンパイルエラー
    test_closure(f);
}
fn test_closure<F: Fn() -> String>(f: F) {
    assert_eq!("a", f());
}

※ クロージャのmoveセマンティクスはキャプチャした値の所有権をクロージャ内に移動させる。

フラワーフラワー

追加で以下もまとめる必要ありそう?

  • moveセマンティクスの挙動について
  • Copyトレイトの挙動について(Copyトレイトが実装されていると、move時にデータがコピーされる、primitiveな値は基本Copyトレイトが実装される。)
フラワーフラワー

2. FnMut

FnMutはキャプチャした値を可変参照として受け取る型である。
キャプチャしたデータを消費はしないが、データを編集することができる。

fn main() {
    let mut x = 1;
    let f = || x += 2;
    test_closure(f);
    assert_eq!(3, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

当然だが、キャプチャする値はmutな参照なので、同じスコープ内で重複してキャプチャすることはできない。

fn main() {
    let mut x = 1;
    let f = || x += 2;
    let g = || x += 3; // コンパイルエラー
    test_closure(f);
    test_closure(g);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

上記のケースでコンパイルを通したいなら、NLLを意識した実装にする必要がある。(NLLについての説明は割愛)

fn main() {
    let mut x = 1;
    let f = || x += 2;
    test_closure(f);

    let g = || x += 3;
    test_closure(g);

    assert_eq!(6, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

これも当然だが、クロージャの型にFnを指定するとコンパイルエラーになる。

fn main() {
    let mut x = 1;
    let f = || x += 2;  // コンパイルエラー
    test_closure(f);
    assert_eq!(6, x);
}
fn test_closure<F: Fn()>(mut f: F) {
    f();
}

キャプチャした値を変更しないようなクロージャ(要するにFn)だとコンパイルエラーは出ない。

fn main() {
    let mut x = 1;
    let f = || println!("{x:?}");
    test_closure(f);
    assert_eq!(6, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

キャプチャした値をmoveする場合はエラーになる。

fn main() {
    let mut s = String::from("x");
    let f = move || s;  // コンパイルエラー
    test_closure(f);
}
fn test_closure<F: FnMut() -> String>(mut f: F) {
    f();
}

フラワーフラワー

FnMutの実験をしていて気づいたのだが、以下の場合はコンパイルが通らない。
クロージャ内でキャプチャした値を破壊的変更する場合、クロージャがCopyでなくなるためにクロージャのcopyが出来ずmoveしてしまうのが原因らしい。

fn main() {
    let mut x = 1;
    let f = || x += 2;
    test_closure(f);
    test_closure(f); // コンパイルエラー
    assert_eq!(5, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

これをコンパイル通すには、クロージャを&mutな参照にする必要がある。
コンパイラの指摘通りに愚直に修正すると以下の通りとなる。

fn main() {
    let mut x = 1;
    let mut f = || x += 2;
    test_closure(&mut f);
    test_closure(&mut f);
    assert_eq!(5, x);
}
fn test_closure<F: FnMut()>(f: &mut F) {
    f();
}

ちなみに、キャプチャした値に対して破壊的変更を行わない場合はコンパイルエラーにはならない。
(この場合はクロージャがCopyとして扱われる)

fn main() {
    let x = 1;
    let f = || println!("{x:?}");
    test_closure(f);
    test_closure(f);
    assert_eq!(5, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

個人的には、Rustのクロージャは型を持った一つの値として扱うのが良いと思う。
moveもcopyも参照やmut参照もできるので。

フラワーフラワー

FnOnceはいつ使うのか分かりづらいところあると思うので、その辺もまとめたい。

フラワーフラワー

FnOnceはキャプチャしたデータをmoveする。
キャプチャしたデータの所有権がクロージャにmoveするので、データに変更を加えたり消費(drop)してもエラーが発生しない。

  • 変更する場合
fn main() {
    let mut s = String::from("x");
    let f = move || s = "y".to_string();
    test_closure(f);
}

fn test_closure<F: FnOnce()>(f: F) {
    f();
}

  • moveする場合
fn main() {
    let s = String::from("x");
    let f = move || s;
    test_closure(f);
}

fn test_closure<F: FnOnce() -> String>(f: F) {
    f();
}

  • 消費する場合
fn main() {
    let s = String::from("x");
    let f = || drop(s);
    test_closure(f);
}

fn test_closure<F: FnOnce()>(f: F) {
    f();
}

当然ながら、値をmoveするのでキャプチャした値は後から参照できない。

fn main() {
    let s = String::from("x");
    let f = move || s;
    test_closure(f);
    assert_eq!("x".to_string(), s);  // コンパイルエラー
}

fn test_closure<F: FnOnce() -> String>(f: F) {
    f();
}

クロージャを2回以上呼ぶこともできない。(以下の場合はsがmoveされているのでfはCopyが動かない)

fn main() {
    let s = String::from("x");
    let f = move || s;
    test_closure(f);
    test_closure(f);  // コンパイルエラー
}

fn test_closure<F: FnOnce() -> String>(f: F) {
    f();
}

フラワーフラワー

FnOnceを使うケースは、以下の3パターンがあると考えられる。

1.並行処理でクロージャを渡す時
2. クロージャの使用を一回だけにしたい時
3. キャプチャしたデータを消費するような処理を行いたい時

おそらく、一番多いケースは1.のパターンだと思われる。
以下のドキュメントにもある通り、Rustの並行処理はFnOnceなクロージャをspawnすることで実行される。(これはtokioでも同様)
https://doc.rust-lang.org/std/thread/fn.spawn.html

フラワーフラワー

Option<T>のmethodは基本的にレシーバをmoveするので注意。(mapとか)
immutableな参照&ならCopy Traitを実装しているので、moveしても参照をコピーする。
なので、move後も普通に参照できる。

#[derive(Debug, PartialEq, Eq)]
struct Sample<'a, T>(Option<&'a T>);

fn main() {
    let binding = 1;
    let sample = Sample(Some(&binding));
    let sample2 = sample.0.map(|s| *s + 1);

    assert_eq!(sample, Sample(Some(&1)));
    assert_eq!(sample2, Some(2));
}

しかし、mutableな参照はCopy traitを実装していないので、mapとかでmoveするとその後から参照できなくなる。

#[derive(Debug, PartialEq, Eq)]
struct Sample<'a, T>(Option<&'a mut T>);

fn main() {
    let mut binding = 1;
    let mut sample = Sample(Some(&mut binding));
    let sample2 = sample.0.map(|s| *s + 1);

    assert_eq!(sample, Sample(None));
    assert_eq!(sample2, Some(2));
}

エラー内容

error[E0382]: borrow of partially moved value: `sample`
   --> src/main.rs:9:5
    |
7   |     let sample2 = sample.0.map(|s| *s + 1);
    |                   -------- --------------- `sample.0` partially moved due to this method call
    |                   |
    |                   help: consider calling `.as_ref()` or `.as_mut()` to borrow the type's contents
8   |
9   |     assert_eq!(sample, Sample(None));
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ value borrowed here after partial move
    |
note: this function takes ownership of the receiver `self`, which moves `sample.0`
   --> /Users/hanaokachiiku/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/option.rs:919:28
    |
919 |     pub const fn map<U, F>(self, f: F) -> Option<U>
    |                            ^^^^
    = note: partial move occurs because `sample.0` has type `std::option::Option<&mut i32>`, which does not implement the `Copy` trait
    = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

対処方法はケースバイケースだが、例えばtakeを使用するとレシーバをNoneに変えてくれる。(当然レシーバはmutにしなければならない。)
Optionを&mutで受け取っているが、moveのような使い方をしたい時には便利。

#[derive(Debug, PartialEq, Eq)]
struct Sample<'a, T>(Option<&'a mut T>);

fn main() {
    let mut binding = 1;
    let mut sample = Sample(Some(&mut binding));
    let sample2 = sample.0.take().map(|s| *s + 1);

    assert_eq!(sample, Sample(None));
    assert_eq!(sample2, Some(2));
}

このスクラップは2023/12/03にクローズされました