LoginSignup
22
13

More than 3 years have passed since last update.

Rust勉強中 - その20 -> クロージャ

Last updated at Posted at 2019-10-28

自己紹介

出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。

Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。

環境

新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.38.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell

前回

前回はユーティリティトレイトについて学びました。
Rust勉強中 - その19

クロージャ

Rustのクロージャは他の言語でいう無名関数式やlambda式です。
ただし、Rustのクロージャは高速で安全です。

クロージャのフォーマット

クロージャの書き方は以下のように書けます。
クロージャは引数や戻り値の型が自明であれば省略できます。

|arg: T| -> T {
    expr_1;
    ...
    expr_n
};

以下に例を示します。

fn main() {
    let c1 = |x| x+1;
    println!("{}", c1(10)); // 11
}

変数c1はクロージャを格納する変数です。クロージャには引数や戻り値の型を省略して書いています。また、{}も複数行の式でなければ省略できます。

クロージャにおける変数の移動と借用

クロージャは以下のように、クロージャ外の変数を参照することができます。

fn main() {
    ...
    let x = 1;
    let c1 = || println!("{}", x);
    c1(); // 1
}

変数xはクロージャを定義する前に、先に定義されています。その変数xをクロージャ内で参照し表示しました。クロージャはこの変数xの借用することができます。
もし、所有権を移動させたい場合は、moveを用います。

move |arg| expr;

混乱しそうだったので、クロージャでmoveありなしの動作を以下のようにして確認しました。もしかして、引数もmoveありなしで変わるのか?という疑問があったので引数についても確認しました。
まず、moveなしで引数に参照を渡した場合を確認します。

fn main() {
    ...
    let num = vec![5];
    let arg = vec![6];
    println!("num: {:p}", &num);
    println!("arg: {:p}", &arg);
    let c2 = |arg| {
        println!("num: {:p}", &num);
        println!("arg: {:p}", arg);
    };
    c2(&arg);
    println!("num: {:p}", &num);
    println!("arg: {:p}", &arg);
num: 0xd2186ff348
arg: 0xd2186ff360
num: 0xd2186ff348
arg: 0xd2186ff360
num: 0xd2186ff348
arg: 0xd2186ff360

変数numは外部変数で、argが引数です。変数numと引数argのアドレスは変わりません。つまり借用されていることが分かります。

次にmoveなしで引数に値を渡した場合を確認します。

fn main() {
    ...
    let num = vec![5];
    let arg = vec![6];
    println!("num: {:p}", &num);
    println!("arg: {:p}", &arg);
    let c2 = |arg| {
        println!("num: {:p}", &num);
        println!("arg: {:p}", &arg);
    };
    c2(arg);
    println!("num: {:p}", &num);
    // println!("arg: {:p}", &arg); // Error
num: 0xe2ec7ef278
arg: 0xe2ec7ef290
num: 0xe2ec7ef278
arg: 0xe2ec7ef350
num: 0xe2ec7ef278

変数numは先ほどと同じでアドレスが変わらずです。argに関しては所有権の移動が発生するのでアドレスが変わっています。

次にmoveありで引数に参照を渡した場合を確認します。

fn main() {
    ...
    let num = vec![5];
    let arg = vec![6];
    println!("num: {:p}", &num);
    println!("arg: {:p}", &arg);
    let c2 = move |arg| {
        println!("num: {:p}", &num);
        println!("arg: {:p}", arg);
    };
    c2(&arg);
    // println!("num: {:p}", &num); // Error
    println!("arg: {:p}", &arg);
}
num: 0x2368d7f540
arg: 0x2368d7f558
num: 0x2368d7f610
arg: 0x2368d7f558
arg: 0x2368d7f558

moveありだと、変数numは所有権の移動が発生するのでアドレスが変化しています。引数argは参照を渡しているのでアドレスに変化はありません。

最後にmoveありの引数に値を渡した場合を確認します。

fn main() {
    let c0 = |x| x+1;
    println!("{}", c0(10));
    // move or not
    let x = 1;
    let c1 = || println!("{}", x);
    c1();
    let num = vec![5];
    let arg = vec![6];
    println!("num: {:p}", &num);
    println!("arg: {:p}", &arg);
    let c2 = move |arg| {
        println!("num: {:p}", &num);
        println!("arg: {:p}", &arg);
    };
    c2(arg);
    // println!("num: {:p}", &num);
    // println!("arg: {:p}", &arg);
num: 0xed5c3ff630
arg: 0xed5c3ff648
num: 0xed5c3ff700
arg: 0xed5c3ff720

この場合はどちらもアドレスに変化があり、所有権の移動が発生しています。

ダラダラと恐れ入ります。
以上をまとめますと、moveありなしで所有権の移動、借用が変化するのはクロージャ外の変数のみで、引数は関係ありませんでした。

移動が発生したときにその値がコピー可能であればコピーされますので、その後も変数を使用できます。

クロージャのトレイト

クロージャは以下のいずれかのトレイトを実装することになります。

trait Fn<Args> : FnMut<Args> {
    fn call(&self, args: Args) -> Self::Output;
}
trait FnMut<Args> : FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}
trait FnOnce<Args> {
    type Output;

    fn call_once(self, args: Args) -> Self::Output;
}

FnはFnMutを、FnMutはFnOnceを継承しています。

  • Fnトレイトはクロージャの参照を引数にとり、何度でも呼び出し可能
  • FnMutトレイトはクロージャの可変参照を引数にとり。何度でも呼び出し可能
  • FnOnceトレイトはクロージャの移動を行い、一度だけ呼び出し可能

関数もクロージャも値で型をもち、引数にしたり、変数に格納出来たりします。

fn f(a: i32) -> i32 {
    a 
}

fn fp1(a: i32, fp: fn(i32) -> i32) {
    println!("{}", fp(a));
}

fn main() {
    ...
    let c3 = |a: i32| -> i32 {a};
    fp1(5, f);  // 5
    fp1(5, c3); // 5
}

また、以下のようにトレイトオブジェクトや制約を用いて指定することもできます。

fn fp2(a: i32, fp: Box<dyn Fn(i32) -> i32>) {
    println!("{}", fp(a));
}

または

fn fp2<T>(a: i32, fp: T) where T: Fn(i32) -> i32 {
    println!("{}", fp(a));
}

fn main() {
    ...
    fp2(5, Box::new(f));  
    fp2(5, Box::new(c3));
}

FnOnce

もし、一度だけ呼び出されるようなクロージャを定義した場合、クロージャはFnOnceのみ実装します。以下の例を見てください。

fn call_twice<T>(c: T) where T: Fn() {
    c();
    c();
}

fn main() {
    ...
    let s = String::from("drop me!");
    let c5 = || {
        drop(s);
    };
    c5();
    // c5(); // Error: double drop
    // call_twice(c5); // Error: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
}

クロージャc5は変数sをドロップします。c5を1度呼び出すと、2度目には同じ値をドロップしようとするのでコンパイル時にエラーを吐きます。この時クロージャはFnOnceを実装しています。call_twice関数はFnを制約としています。なのでcall_twiceの引数にc5を渡すとFnを実装していないと怒られます。
また意図せずに値を消費していることがあるので注意が必要です。

fn main() {
    ...
    let v = vec![1];
    let c6 = move || v;
    c6();
    // c6(); // Error
}

FnMut

FnMutはmutな値を保持したmutなクロージャを実装します。

fn main() {
    ...
    let mut x = vec![5];
    let c7 = || {
        x.push(9);
    };
}

クロージャc7を例えば先ほどのcall_twiceに引数として入れると、FnMutを実装しており、Fnを実装していないためエラーを吐きます。
FnとFnMutのどちらも受け入れるようにするためには関数を以下のようにする必要があります。

fn call_twice_mut<T>(mut c: T) where T: FnMut() {
    c();
    c();
}

fn main() {
    ...
    let mut v = vec![5];
    let mut c7 = || {
        v.push(9);
    };
    // call_twice(c7); // Error: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut` 
    call_twice_mut(c7);  // FnMut
    println!("{:?}", v); // [5, 9, 9]
    call_twice_mut(c4);  // Fn
}

これは、制約の指定を親トレイトとなるFnMutにしてあげることで、許容範囲が広がるため実行できます。

ソース

fn get_type<T>(_: T) -> &'static str {
    std::any::type_name::<T>()
}

fn f(a: i32) -> i32 {
    a 
}

fn fp1(a: i32, fp: fn(i32) -> i32) {
    println!("{}", fp(a));
}

// fn fp2(a: i32, fp: Box<dyn Fn(i32) -> i32>) {
//     println!("{}", fp(a));
// }

fn fp2<T>(a: i32, fp: T) where T: Fn(i32) -> i32 {
    println!("{}", fp(a));
}


fn call_twice<T>(c: T) where T: Fn() {
    c();
    c();
}

fn call_twice_mut<T>(mut c: T) where T: FnMut() {
    c();
    c();
}

fn return_closure(arg: i32) -> Box<dyn Fn() -> i32> {
    Box::new(move || {arg})
}

fn main() {
    let c0 = |x| x+1;
    println!("{}", c0(10));
    // move or not
    let x = 1;
    let c1 = || println!("{}", x);
    c1();
    let num = vec![5];
    let arg = vec![6];
    println!("num: {:p}", &num);
    println!("arg: {:p}", &arg);
    let c2 = |arg| {
        println!("num: {:p}", &num);
        println!("arg: {:p}", &arg);
    };
    c2(arg);
    // println!("num: {:p}", &num);
    // println!("arg: {:p}", &arg);
    // type of closure
    let c3 = |a: i32| -> i32 {a};
    fp1(5, f);
    fp1(5, c3);
    fp2(5, Box::new(f));
    fp2(5, Box::new(c3));
    // Fn
    let c4 = || println!("{}", 5);
    call_twice(c4);
    // FnOnce
    let s = String::from("drop me!");
    let c5 = || {
        drop(s);
    };
    c5();
    // c5(); // Error: double drop
    // call_twice(c5); // Error: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
    let v = vec![1];
    let c6 = move || v;
    c6();
    // c6(); // Error: double drop
    // FnMut
    let mut v = vec![5];
    let c7 = || {
        v.push(9);
    };
    // call_twice(c7); // Error: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut` 
    call_twice_mut(c7);  // FnMut
    println!("{:?}", v); // [5, 9, 9]
    call_twice_mut(c4);  // Fn
    // return closure
    let mut closures: Vec<Box<dyn Fn()->i32>> = Vec::new();
    closures.push(return_closure(999));
    closures.push(return_closure(9));
    println!("{}", closures[0]());
    println!("{}", closures[1]());
}
11
1
num: 0x770672f578
arg: 0x770672f590
num: 0x770672f578
arg: 0x770672f650
5
5
5
5
5
5
[5, 9, 9]
5
5
999
9

今回はここまでー。
クロージャはよく見る文法だったのでここで学べて良かったです。高速で安全とのことなので積極的に使っていきたいですね!

22
13
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
22
13