高度な関数とクロージャ
最後に関数とクロージャに関連する高度な機能の一部を探究し、これには関数ポインタとクロージャの返却が含まれます。
関数ポインタ
クロージャを関数に渡す方法について語りました; 普通の関数を関数に渡すこともできるのです!
新しいクロージャを定義するのではなく、既に定義した関数を渡したい時にこのテクニックは有用です。
これを関数ポインタで行うと、関数を引数として他の関数に渡して使用できます。関数は、型fn
(小文字のfです)に型強制されます。
Fn
クロージャトレイトと混同すべきではありません。fn
型は、関数ポインタと呼ばれます。
引数が関数ポインタであると指定する記法は、クロージャのものと似ています。リスト19-35のように。
ファイル名: src/main.rs
fn add_one(x: i32) -> i32 { x + 1 } fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { let answer = do_twice(add_one, 5); // 答えは{} println!("The answer is: {}", answer); }
このコードは、The answer is: 12
と出力します。do_twice
の引数f
は、型i32
の1つの引数を取り、
i32
を返すfn
と指定しています。それから、do_twice
の本体でf
を呼び出すことができます。
main
では、関数名のadd_one
を最初の引数としてdo_twice
に渡せます。
クロージャと異なり、fn
はトレイトではなく型なので、トレイト境界としてFn
トレイトの1つでジェネリックな型引数を宣言するのではなく、
直接fn
を引数の型として指定します。
関数ポインタは、クロージャトレイト3つ全て(Fn
、FnMut
、FnOnce
)を実装するので、常に関数ポインタを引数として、
クロージャを期待する関数に渡すことができます。関数が関数とクロージャどちらも受け入れられるように、
ジェネリックな型とクロージャトレイトの1つを使用して関数を書くのが最善です。
クロージャではなくfn
だけを受け入れたくなる箇所の一例は、クロージャのない外部コードとのインターフェイスです:
C関数は引数として関数を受け入れられますが、Cにはクロージャがありません。
インラインでクロージャが定義されるか、名前付きの関数を使用できるであろう箇所の例として、map
の使用に目を向けましょう。
map
関数を使用して数字のベクタを文字列のベクタに変換するには、このようにクロージャを使用できるでしょう:
#![allow(unused)] fn main() { let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers .iter() .map(|i| i.to_string()) .collect(); }
あるいは、このようにクロージャの代わりにmap
に引数として関数を名指しできるでしょう:
#![allow(unused)] fn main() { let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers .iter() .map(ToString::to_string) .collect(); }
先ほど「高度なトレイト」節で語ったフルパス記法を使わなければならないことに注意してください。
というのも、to_string
という利用可能な関数は複数あるからです。ここでは、
ToString
トレイトで定義されたto_string
関数を使用していて、このトレイトは標準ライブラリが、
Display
を実装するあらゆる型に実装しています。
このスタイルを好む方もいますし、クロージャを使うのを好む方もいます。どちらも結果的に同じコードにコンパイルされるので、 どちらでも、自分にとって明確な方を使用してください。
クロージャを返却する
クロージャはトレイトによって表現されます。つまり、クロージャを直接は返却できないのです。
トレイトを返却したい可能性のあるほとんどの場合、代わりにトレイトを実装する具体的な型を関数の戻り値として使用できます。
ですが、クロージャではそれはできません。返却可能な具体的な型がないからです; 例えば、
関数ポインタのfn
を戻り値の型として使うことは許容されていません。
以下のコードは、クロージャを直接返そうとしていますが、コンパイルできません:
fn returns_closure() -> Fn(i32) -> i32 {
|x| x + 1
}
コンパイルエラーは以下の通りです:
error[E0277]: the trait bound `std::ops::Fn(i32) -> i32 + 'static:
std::marker::Sized` is not satisfied
-->
|
1 | fn returns_closure() -> Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^ `std::ops::Fn(i32) -> i32 + 'static`
does not have a constant size known at compile-time
|
= help: the trait `std::marker::Sized` is not implemented for
`std::ops::Fn(i32) -> i32 + 'static`
= note: the return type of a function must have a statically known size
エラーは、再度Sized
トレイトを参照しています!コンパイラには、クロージャを格納するのに必要なスペースがどれくらいかわからないのです。
この問題の解決策は先ほど見かけました。トレイトオブジェクトを使えます:
#![allow(unused)] fn main() { fn returns_closure() -> Box<Fn(i32) -> i32> { Box::new(|x| x + 1) } }
このコードは、問題なくコンパイルできます。トレイトオブジェクトについて詳しくは、 第17章の「トレイトオブジェクトで異なる型の値を許容する」節を参照してください。
次は、マクロを見てみましょう!