はじめに
高度な関数とクロージャへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、関数ポインタやクロージャの返却など、関数とクロージャの高度な機能を探ります。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
高度な関数とクロージャへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、関数ポインタやクロージャの返却など、関数とクロージャの高度な機能を探ります。
このセクションでは、関数ポインタやクロージャの返却など、関数とクロージャに関連するいくつかの高度な機能を探ります。
クロージャを関数に渡す方法については既に説明しましたが、通常の関数も関数に渡すことができます!この技術は、既に定義した関数を渡したい場合、新しいクロージャを定義する代わりに便利です。関数は、大文字の F
ではなく、小文字の f
を持つ fn
型に暗黙的に変換されます。これは、Fn
クロージャトレイトと混同しないようにしてください。fn
型は、関数ポインタ と呼ばれます。関数ポインタを使って関数を渡すことで、関数を他の関数の引数として使うことができます。
パラメータが関数ポインタであることを指定する構文は、クロージャの構文と似ています。例えば、add_one
という関数を定義しており、この関数は引数に 1 を加えるものとします。do_twice
関数は 2 つのパラメータを持ちます。i32
型のパラメータを取り、i32
を返す任意の関数への関数ポインタと、1 つの i32
型の値です。do_twice
関数は、関数 f
に arg
値を渡して 2 回呼び出し、その 2 つの関数呼び出しの結果を加えます。main
関数は、add_one
と 5
を引数に do_twice
を呼び出します。
ファイル名: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}");
}
リスト 19-27: fn
型を使って関数ポインタを引数として受け取る
このコードは 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
のみを受け付け、クロージャを受け付けない場合の 1 つの例は、クロージャがない外部コードとのインターフェイスを行う場合です。C 言語の関数は関数を引数として受け付けることができますが、C にはクロージャはありません。
インラインで定義したクロージャまたは名前付き関数のどちらを使うかの例として、標準ライブラリの Iterator
トレイトによって提供される map
メソッドの使い方を見てみましょう。数値のベクタを文字列のベクタに変換するために map
関数を使う場合、クロージャを使うことができます。
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
の引数として指定することもできます。
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
を実装する任意の型に対して標準ライブラリによって実装されています。
「列挙型の値」で思い出してください。定義した各列挙型のバリアントの名前も初期化関数になります。これらの初期化関数は、クロージャトレイトを実装する関数ポインタとして使うことができます。これは、初期化関数をクロージャを取るメソッドの引数として指定できることを意味します。
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20)
.map(Status::Value)
.collect();
ここでは、map
が呼び出される範囲内の各 u32
値を使って、Status::Value
の初期化関数を使って Status::Value
インスタンスを作成しています。このスタイルが好きな人もいれば、クロージャを使う人もいます。どちらのコードも同じコードにコンパイルされるので、どちらのスタイルが分かりやすいか使ってください。
クロージャはトレイトによって表されるため、直接クロージャを返すことはできません。トレイトを返したい場合のほとんどのケースでは、代わりにトレイトを実装する具体的な型を関数の返却値として使うことができます。しかし、クロージャの場合はそれができません。なぜなら、返却可能な具体的な型がないからです。例えば、関数ポインタ fn
を返却型として使うことはできません。
次のコードは、直接クロージャを返そうとしていますが、コンパイルされません。
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
コンパイラのエラーは次の通りです。
error[E0746]: return type cannot have an unboxed trait object
--> src/lib.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at
compile-time
|
= note: for information on `impl Trait`, see
<https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-
implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of
type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ~~~~~~~~~~~~~~~~~~~
エラーは再び Sized
トレイトを参照しています!Rust は、クロージャを格納するのにどれだけのスペースが必要かを知りません。この問題の解決策を前に見たことがあります。トレイトオブジェクトを使うことができます。
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
このコードは正常にコンパイルされます。トレイトオブジェクトに関する詳細は、「異なる型の値を許容するトレイトオブジェクトの使用」を参照してください。
次に、マクロを見てみましょう!
おめでとうございます!高度な関数とクロージャの実験を完了しました。LabEx でさらに多くの実験を行って、スキルを向上させることができます。