[Xiaojia で Rust プログラミングを学ぶ] 13、関数型言語の機能: イテレータとクロージャ

シリーズ記事ディレクトリ

[XiaojiaでRustプログラミングを学ぶ] 1. Rustプログラミングの基礎
[XiaojiaでRustプログラミングを学ぶ] 2. Rustパッケージ管理ツールの使用
[XiaojiaでRustプログラミングを学ぶ] 3. Rustの基本的なプログラム概念
[XiaojiaでRustプログラミングを学ぶ] 】4. Rustの所有権の概念を理解する
【XiaojiaからRustプログラミングを学ぶ】5. 構造体を使用して構造化データを関連付ける
【XiaojiaからRustプログラミングを学ぶ】6. 列挙とパターンマッチング
【XiaojiaからRustプログラミングを学ぶ】7. パッケージを使用する(プロジェクトを管理するためのパッケージ)、ユニットパッケージ (Crate)、およびモジュール (Module)
[Xiaojia で Rust プログラミングを学ぶ] 8. 共通コレクション
[Xiaojia で Rust プログラミングを学ぶ] 9. エラー処理 (Error Handling)
[Xiao Jia が Rust プログラミングを学ぶをフォローする] 11. 自動テストを作成する
[Xiao Jia で Rust プログラミングを学ぶ] 12. コマンド ライン プログラムを構築する
[Xiao Jia で Rust プログラミングを学ぶ] 13. 関数型言語の機能: イテレータとクロージャ

序文

Rust の設計は多くの既存の言語やテクノロジーからインスピレーションを受けており、注目すべき影響の 1 つは関数型プログラミングです。関数型プログラミング スタイルには、多くの場合、パラメーター値または他の関数の戻り値として関数が含まれ、後で実行するために変数に値を割り当てます。

主な教材は「The Rust Programming Language」を参照


1. クロージャ

1.1. クロージャ

Rust のクロージャは、変数に格納したり、引数として他の関数に渡すことができる匿名関数です。クロージャは 1 か所で作成し、別のコンテキストで実行できます。関数とは異なり、クロージャでは呼び出しスコープ内の値をキャプチャできます。
例えば:

  • 関数をパラメータとして渡す
  • 関数を関数の戻り値として使用する
  • 関数を変数に代入する

1.2、Rustのクロージャー構文

1.2.1、クロージャ構文形式

Rust クロージャは Smalltalk と Ruby の形式を借用していますが、関数との最大の違いはパラメータが |param| の形式で宣言されていることです。

例: クロージャ構文フォーム

|param1,param2,....|{
    
    
	语句1;
	语句2...
	返回表达式
}

1.2.1. クロージャの簡略化された形式

return 式が 1 つだけの場合は、次の形式に簡略化できます。

|param|返回表达式

1.3. クロージャの型推定

Rustは静的言語であるため、すべての変数には型がありますが、コンパイラの強力な型推論機能のおかげで、多くの場合、明示的に型を宣言する必要はありませんが、関数はすべてのパラメータと戻り値の型を指定する必要があります。

コードの可読性を高めるために、型を明示的にマークすることがありますが、同じ目的で、クロージャの型をマークすることもできます。

let sum = |x:i32, y:32| -> 32{
    
    
	x + y
}

型推論は非常に便利ですが、ジェネリックではないため、コンパイラは型を推論するときに常にその型を使用します。

1.4. 構造内のクロージャ

struct Cacher<T> where T: Fn(u32) -> u32 {
    
    
	query: T,
	value: Optional<u32>
}

このとき、クエリはクロージャで、その型はFn(u32)です -> u32はフィーチャであり、Tがクロージャの型であることを示すために使用されます。

1.5、環境内の値を取得する

1.5.1、環境の価値を捉える

クロージャは環境内の値をキャプチャできます

fn main() {
    
    
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

関数を使用して実装すると、コンパイラは動的環境で値を取得できないことを通知します。

1.5.2、メモリに対するクロージャの影響

クロージャは環境から値を取得すると、それらの値を保存するためにメモリを割り当てます。一部のシナリオでは、この追加のメモリ割り当てが負担になる可能性があります。対照的に、関数はこれらの環境値をキャプチャしないため、関数の定義と使用にこのようなメモリの負担はかかりません。

1.5.2. 3 つの Fn 特性

クロージャが環境変数を取得するには 3 つの方法があり、これらは関数パラメータを渡す 3 つの方法 (所有権の転送、変数の借用、および不変の借用) に対応するため、Fn Trait には 3 つのタイプがあります。

1.5.2.1、FnOnce

このタイプのクロージャは、キャプチャされた変数の所有権を取得します。クロージャーは 1 回のみ実行できます。

fn fn_once<F>(func: F)
where
    F: FnOnce(usize) -> bool,
{
    
    
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    
    
    let x = vec![1, 2, 3];
    fn_once(|z|{
    
    z == x.len()})
}

この時点で、コンパイラは、所有権を失ったクロージャ変数に対して 2 回目の呼び出しを行うことができないため、エラーを報告します。エラー メッセージは、F が Copy Trait を実装していないためエラーを報告し、制約を追加して Copy のクロージャを実装しようとすることを示しています。

fn fn_once<F>(func: F)
where
    F: FnOnce(usize) -> bool + Copy,// 改动在这里
{
    
    
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    
    
    let x = vec![1, 2, 3];
    fn_once(|z|{
    
    z == x.len()})
}

クロージャにキャプチャされた変数の所有権を強制的に取得させたい場合は、パラメータ リストの前に move キーワードを追加できます。この使用法は通常、クロージャのライフ サイクルがキャプチャされた変数のライフ サイクルよりも長い場合に使用されます。 、クロージャを返すか、クロージャを別のスレッドに移動するなど。

use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    
    
    println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();

1.5.2.2、FnMut

これは、環境内の値を取得して変更できるようにする可変の借用です。

fn main() {
    
    
    let mut s = String::new();

    let mut update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}

複雑な形

fn main() {
    
    
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);

    exec(update_string);

    println!("{:?}",s);
}

fn exec<'a, F: FnMut(&'a str)>(mut f: F)  {
    
    
    f("hello")
}

1.5.2.3、Fnストローク

環境内の値を不変借用の形で取得します。上記のコードの F 型を Fn に変更しましょう。

fn main() {
    
    
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);

    exec(update_string);

    println!("{:?}",s);
}

fn exec<'a, F: Fn(&'a str)>(mut f: F)  {
    
    
    f("hello")
}

エラー レポートから、クロージャが FnMut 機能を実装しており、変数の借用が必要であることは明らかですが、実行時に Fn 機能でマークされているため、不一致があります。正しいものを見てみましょう。借用方法を変更します。

fn main() {
    
    
    let s = "hello, ".to_string();

    let update_string =  |str| println!("{},{}",s,str);

    exec(update_string);

    println!("{:?}",s);
}

fn exec<'a, F: Fn(String) -> ()>(f: F)  {
    
    
    f("world".to_string())
}

1.5.3、移動とFn

上で、FnOnce に対する move キーワードの重要性を説明しましたが、実際には、move を使用するクロージャは Fn および
Fn Mut 機能を実装している可能性があります。

なぜなら、クロージャがどの Fn トレイトを実装するかは、クロージャが変数をキャプチャする方法ではなく、キャプチャされた変数をクロージャがどのように使用するかによって決まるからです。move 自体は後者、つまりクロージャが変数をキャプチャする方法を強調しています。

1.5.4、3つのFnの関係

実際、クロージャは Fn トレイトを実装するだけでなく、ルールは次のとおりです。

  • すべてのクロージャは FnOnce トレイトを自動的に実装するため、どのクロージャでも少なくとも 1 回は呼び出すことができます。
  • キャプチャされたすべての変数の所有権を削除しないクロージャは、FnMut トレイトを自動的に実装します。
  • キャプチャされた変数への変更を必要としないクロージャは自動的に無効化されます Fn Trait
pub trait Fn<Args> : FnMut<Args> {
    
    
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Args> : FnOnce<Args> {
    
    
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait FnOnce<Args> {
    
    
    type Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

1.6. 関数の戻り値としてのクロージャ

fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
    
    
    let num = 5;

    if x > 1{
    
    
        Box::new(move |x| x + num)
    } else {
    
    
        Box::new(move |x| x - num)
    }
}

2. イテレータ

2.1. イテレータ

イテレータ パターン: 一連の項目に対して何らかのアクションを実行します。イテレータは各項目を走査して、シーケンスがいつ完了するかを決定します。Rust のイテレータは遅延型であり、イテレータを使用するメソッドが呼び出されない限り、イテレータ自体は何の効果もありません。

イテレータを使用すると、配列、ベクトル、ハッシュマップなどの連続したコレクションを反復処理できます。

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    
    
    println!("{}", val);
}

2.2、イテレータ特性

2.2.1、イテレータ特性

すべての反復子は、標準ライブラリで定義されている Iterator 特性を実装します。定義はおおよそ以下の通りです。

pub trait Iterator {
    
    
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // 省略其余有默认实现的方法
}

2.2.2、次の方法

イテレータ内の項目が返されるたびに、返された結果は Some でラップされ、反復の最後には None が返されます。

fn main() {
    
    
    let arr = [1, 2, 3];
    let mut arr_iter = arr.into_iter();

    assert_eq!(arr_iter.next(), Some(1));
    assert_eq!(arr_iter.next(), Some(2));
    assert_eq!(arr_iter.next(), Some(3));
    assert_eq!(arr_iter.next(), None);
}

2.3. 反復法

iter: 不変参照に対するイテレータを作成する
into_iter: 作成されたイテレータが所有権を取得します
iter_mut: 変更可能な参照を反復処理します

2.4. イテレータの消費方法

2.4.1、合計方法

次のメソッドを呼び出すメソッドは、消費アダプターと呼ばれます。例: Sum メソッド: イテレータの所有権を取得します。

fn main() {
    
    
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);

    // v1_iter 是借用了 v1,因此 v1 可以照常使用
    println!("{:?}",v1);

    // 以下代码会报错,因为 `sum` 拿到了迭代器 `v1_iter` 的所有权
    // println!("{:?}",v1_iter);
}

2.4.2、収集メソッド

use std::collections::HashMap;
fn main() {
    
    
    let names = ["sunface", "sunfei"];
    let ages = [18, 18];
    let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();

    println!("{:?}",folks);
}

zip はイテレータ アダプタです。その機能は、2 つのイテレータの内容を一緒に圧縮して、Iterator<Item=(ValueFromA, ValueFromB)> などの新しいイテレータを形成することです。ここでは [(name1, age1), (名前2、年齢2)]。

次に、collect を使用して、新しいイテレータ内の (K, V) の形式で値を HashMap<K, V> に収集します。同様に、型はここで明示的に宣言する必要があり、HashMap 内の KV 型は次のようになります。最終的に、コンパイラーは HashMap<&str, i32> を推定しますが、これは完全に正しいです。

2.5. イテレータアダプター

2.5.1、地図

コンシューマ アダプタはイテレータを消費し、値を返すためです。次に、イテレータ アダプタは、名前が示すように、新しいイテレータを返します。これは、連鎖メソッド呼び出しを実装するための鍵です: v.iter().map().filter()…。

コンシューマ アダプタとは異なり、イテレータ アダプタは遅延的です。つまり、コンシューマ アダプタがラップして最終的にイテレータを具体的な値に変換する必要があります。

let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

2.5.2、zip

2.5.3、フィルター

2.5.4、列挙する

使用 enumerate 方法可以获取迭代时的索引。
let v = vec![1u64, 2, 3, 4, 5, 6];
for (i,v) in v.iter().enumerate() {
    
    
    println!("第{}个值是{}",i,v)
}

2.6、カスタム反復子

自定义迭代器很简单,我们只需要实现 Iterator 特征 以及next 方法即可。实际上  Iterator 之中还有其他方法,其他方法都有默认实现,无需手动去实现。
impl Iterator for Counter {
    
    
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
    
    
        if self.count < 5 {
    
    
            self.count += 1;
            Some(self.count)
        } else {
    
    
            None
        }
    }
}

4. パフォーマンスの比較: ループ VS イテレータ

イテレータは Rust のゼロコスト抽象化の 1 つであり、この抽象化により実行時のオーバーヘッドが発生しないことを意味します。

要約する

それが今日のすべてです

おすすめ

転載: blog.csdn.net/fj_Author/article/details/132222779