Rustのクロージャ:環境をキャプチャする匿名関数
クロージャとは、周囲の環境から変数をキャプチャできる匿名関数です。関数と同じように呼び出されますが、定義されたスコープ内の変数を「記憶」することができます。
正規関数を「テイクアウトメニューのセットメニュー」(材料はパラメータから得られる)に例えるなら、クロージャは「自分の冷蔵庫にある食材」(材料は環境から得られる)に例えることができます。クロージャは、定義された時点で存在していた変数にアクセスすることができます。これは、まるで冷蔵庫から食材を取り出して、すぐに料理を始めるようなものです。
1. 学習内容
- クロージャ構文の書き方
|params| exprおよび|params| { block } - 3つのキャプチャモード:
Fn(不変の借用)、FnMut(可変の借用)、FnOnce(所有権の消費) moveキーワード—キャプチャされた変数の所有権をクロージャに強制的に譲渡する- 関数引数としてのクロージャ:関数ポインタ
fnとFnトレイトの違い - クロージャとイテレータの組み合わせ:
map、filter、collectの連鎖呼び出し - 実世界のシナリオにおけるクロージャの使用パターン
2. 概念図
以下のMermaid図は、3種類のクロージャキャプチャ(Fn、FnMut、FnOnce)間の関係と相違点を示しています:
graph TB
A["Closure Closure"] --> B["Fn<br/>Immutable Borrowing &T"]
A --> C["FnMut<br/>Variable Borrowing &mut T"]
A --> D["FnOnce<br/>Consumer Ownership T"]
B --> E["let c = || println!("{ }", x);"]
B --> F["Can be called multiple times"]
B --> G["Environment variables are read-only and immutable"]
C --> H["let mut c = || x += 1;"]
C --> I["Can be called multiple times"]
C --> J["Environment variables can be modified"]
D --> K["let c = || drop(x);"]
D --> L["Can only be called once"]
D --> M["Ownership has been transferred to the closure"]
B -.-> |Automatic Inference| N["The compiler, based on the closure body<br/>Select the minimum requirement trait"]
C -.-> N
D -.-> N
N --> O["move Mandatory Keywords FnOnce"]
3. フードデリバリーの評価システムの背景
(1) 面倒な点:採点ルールごとに個別の関数を作成しなければならないこと
Mia(ミア)は、フードデリバリーの評価システムを開発しています。ユーザーはレストランを評価できますが、評価のルールはさまざまです:
- 一般ユーザー:1~5つの星で直接評価してください
- フードブロガー:評価 × 2(重み付けが高い)
- 衛生検査官:3未満のスコアは「是正が必要」と判定される
最初は、彼女はルールごとに関数を記述しました:
struct Review {
store: String,
rating: u32,
}
fn normal_score(r: &Review) -> u32 {
r.rating
}
fn blogger_score(r: &Review) -> u32 {
r.rating * 2
}
fn inspector_score(r: &Review) -> u32 {
if r.rating < 3 { 0 } else { r.rating }
}
fn main() {
let reviews = vec![
Review { store: "Pizzaplace".into(), rating: 4 },
Review { store: "BurgerKing".into(), rating: 2 },
Review { store: "SushiBar".into(), rating: 5 },
];
for r in &reviews {
println!("{}: normal={}, blogger={}, inspector={}",
r.store, normal_score(r), blogger_score(r), inspector_score(r));
}
}
コアロジックの違いはさておき、これら3つの関数の関数シグネチャはまったく同じです。もしプロダクトマネージャーが「スーパーメンバー向けのスコアリングルールをもう1つ追加してほしい」と言った場合、別の関数を記述する必要があります。さらに、これらのスコアリングルールは「動的に生成」することができません。例えば、「スコア × 係数」における係数は、関数内にハードコーディングされなければなりません。
(2) より複雑な要件:動的に変化する重み付け係数
プロダクトマネージャーは次のように述べた。「評価係数は(プロモーション活動に応じて)日々変化し、ユーザーはさまざまな評価戦略(スコアリング、重み付け、低評価へのペナルティ)を選択できる。これは標準関数では実現できない。関数のシグネチャはコンパイル時に固定されており、外部からの動的な係数を考慮に入れることができないからだ。」
// I want to use a function,However, the coefficient is dynamic.
let weight = 1.5; // Today's Weighting Coefficient
// fn weighted_score(r: &Review) -> u32 {
// (r.rating as f64 * weight) as u32 // ❌ Compilation Error:weight Outside the function's scope
// }
(3) Rustのクロージャに関する解決策
struct Review {
store: String,
rating: u32,
}
fn main() {
let reviews = vec![
Review { store: "Pizzaplace".into(), rating: 4 },
Review { store: "BurgerKing".into(), rating: 2 },
Review { store: "SushiBar".into(), rating: 5 },
];
// Closure 1: normal score (no capture)
let normal = |r: &Review| r.rating;
// Closure 2: weighted score (captures `weight` from environment)
let weight: f64 = 1.5;
let weighted = |r: &Review| (r.rating as f64 * weight) as u32;
// Closure 3: inspector (captures `threshold` from environment)
let threshold = 3;
let inspector = |r: &Review| if r.rating < threshold { 0 } else { r.rating };
for r in &reviews {
println!("{}: normal={}, weighted={}, inspector={}",
r.store, normal(r), weighted(r), inspector(r));
}
}
出力:
Pizzaplace: normal=4, weighted=6, inspector=4
BurgerKing: normal=2, weighted=3, inspector=0
SushiBar: normal=5, weighted=7, inspector=5
クロージャ
|r| r.rating * weightは外部変数weightをキャプチャしますが、これは通常の関数では不可能なことです。weightやthresholdは動的に変化する可能性があり(たとえば、ユーザーの入力や設定ファイルに基づいて)、クロージャはそれらの現在の値を自動的にキャプチャします。これがクロージャの核心的な価値です:周囲の変数を「盗み出し」、それらを利用できる関数。
4. 基本概念
(1) 閉鎖システムの概要
graph TB
A[Rust Closures] --> B[Syntax]
A --> C[Capture Modes]
A --> D[move Keyword]
A --> E[As Parameters]
A --> F[With Iterators]
B --> B1["|param1, param2| expr"]
B --> B2["|param| { multiple; statements; }"]
C --> C1["Fn: immutable borrow (&T)"]
C --> C2["FnMut: mutable borrow (&mut T)"]
C --> C3["FnOnce: ownership (T)"]
D --> D1["let c = move || x;"]
D --> D2["Forces ownership transfer"]
E --> E1["fn pointer: fn(T) -> U"]
E --> E2["Fn trait: impl Fn(T) -> U"]
E --> E3["FnMut / FnOnce trait bounds"]
F --> F1["iter().map(|x| x + 1)"]
F --> F2["iter().filter(|x| x > 0)"]
F --> F3["Chained: map().filter().collect()"]
(2) 3つのキャプチャモードの比較
| 特性 | 取得方法 | 呼び出し回数 | 環境を変更可能か? | 複数回呼び出し可能か? |
|---|---|---|---|---|
FnOnce |
所有権を消費する(移動) | 1回 | 環境変数を消費できる | いいえ(所有権はすでに譲渡済み) |
FnMut |
変数の借用 | 複数回 | 環境変数を変更可能 | はい |
Fn |
不変の借用 | 複数回 | 環境変数は変更できない | はい |
(3) 関数ポインタ fn 対 クロージャ Fn
| 特集 | 関数ポインタ fn |
クロージャ Fn トレイト |
|---|---|---|
| 環境変数を取得しますか? | いいえ—パラメータのみ使用可能です | はい—環境変数を取得できます |
| パラメータとして使用できますか? | はい | はい(より一般的な場合) |
| クロージャーは扱えますか? | いいえ | はい |
| タイプサイズ | fn(T) -> U (ポインタサイズ) |
クロージャごとにサイズが異なる |
| パフォーマンス | 判定済み | コンパイラによるインライン化可能 |
| 構文 | fn foo(x: i32) -> i32 |
|x| x + 1 |
(4) クロージャーの取得手法の比較
| 取得方法 | 構文例 | 所有権に関する考慮事項 | クロージャの呼び出し可能回数 | 適用可能なシナリオ |
|---|---|---|---|---|
| 不変の借用 | |x| x + var |
借用 &T |
複数 | 環境変数への読み取り専用アクセス |
| 可変借用 | |x| { var += 1; ... } |
借用 &mut T |
複数(排他的) | 環境変数を変更する必要がある |
| 所有権の移動 | move |x| x + var |
T を取得 |
1回限り(消費された場合) | クロージャは参照よりも長く存続しなければならない |
| キャプチャなし | |x| x + 1 |
なし | 複数 | 関数ポインタに相当 |
5. クロージャの例
(1) ▶ サンプル:クロージャーの基本構文 — フードデリバリーの評価 (難易度 ⭐)
// ============================================
// Closure basics: syntax, type inference, calling
// ============================================
struct Review {
store: String,
rating: u32,
}
fn main() {
let reviews = vec![
Review { store: String::from("Pizzaplace"), rating: 4 },
Review { store: String::from("BurgerKing"), rating: 2 },
Review { store: String::from("SushiBar"), rating: 5 },
Review { store: String::from("NoodleHouse"), rating: 3 },
];
// --- Syntax variation 1: single expression ---
let double = |r: &Review| r.rating * 2;
// --- Syntax variation 2: block body ---
let triple = |r: &Review| {
let result = r.rating * 3;
println!(" (triple: {} -> {})", r.rating, result);
result
};
// --- Syntax variation 3: type inference ---
// Rust infers parameter and return types from usage
let half = |r| r.rating / 2; // type inferred as &Review -> u32
println!("--- Rating Calculations ---");
for r in &reviews {
println!("{} (rating: {}):", r.store, r.rating);
println!(" double={}, half={}", double(r), half(r));
let _ = triple(r);
}
// --- Closures that capture variables ---
let min_rating = 3;
let min_rating2 = 4;
// Capture `min_rating` from the surrounding scope
let is_pass = |r: &Review| r.rating >= min_rating;
let is_good = |r: &Review| r.rating >= min_rating2;
println!("\n--- Pass Check (min: {}) ---", min_rating);
for r in &reviews {
println!("{}: {}", r.store, if is_pass(r) { "PASS" } else { "FAIL" });
}
println!("\n--- Good Check (min: {}) ---", min_rating2);
for r in &reviews {
println!("{}: {}", r.store, if is_good(r) { "GOOD" } else { "OK" });
}
// --- Using closures as function arguments ---
fn check_reviews(reviews: &[Review], label: &str, scorer: impl Fn(&Review) -> u32) {
print!("{}: ", label);
for r in reviews {
print!("{}->{} ", r.store, scorer(r));
}
println!();
}
let weight = 2;
let weighted_scorer = |r: &Review| r.rating * weight;
check_reviews(&reviews, "Weighted(×2)", weighted_scorer);
check_reviews(&reviews, "Normal", |r| r.rating);
check_reviews(&reviews, "Bonus", |r| if r.rating >= 4 { r.rating + 1 } else { r.rating });
}
出力:
--- Rating Calculations ---
Pizzaplace (rating: 4):
double=8, half=2
(triple: 4 -> 12)
BurgerKing (rating: 2):
double=4, half=1
(triple: 2 -> 6)
SushiBar (rating: 5):
double=10, half=2
(triple: 5 -> 15)
NoodleHouse (rating: 3):
double=6, half=1
(triple: 3 -> 9)
--- Pass Check (min: 3) ---
Pizzaplace: PASS
BurgerKing: FAIL
SushiBar: PASS
NoodleHouse: PASS
--- Good Check (min: 4) ---
Pizzaplace: GOOD
BurgerKing: OK
SushiBar: GOOD
NoodleHouse: OK
--- Weighted(×2) ---
Pizzaplace->8 BurgerKing->4 SushiBar->10 NoodleHouse->6
Normal: Pizzaplace->4 BurgerKing->2 SushiBar->5 NoodleHouse->3
Bonus: Pizzaplace->5 BurgerKing->2 SushiBar->6 NoodleHouse->3
クロージャの構文には、単一式
|x| expr、ブロック|x| { stmt; expr }、および型推論|x| x+1(文脈から型が推論される)の3つのスタイルがあります。環境変数のキャプチャはクロージャー独自の機能です。is_passはmin_ratingをキャプチャし、weighted_scorerはweightをキャプチャします。check_reviews関数はimpl Fn(&Review) -> u32を受け入れます。つまり、Fnトレイトを実装する任意のクロージャーをこの関数に渡すことができます。
(2) ▶ サンプル:キャプチャモード — Fn / FnMut / FnOnce (難易度 ⭐⭐)
// ============================================
// Three capture modes: Fn, FnMut, FnOnce
// ============================================
fn main() {
println!("=== Fn: Immutable Borrow ===");
let scores = vec![10, 20, 30];
// Fn closure: only reads captured variables (immutable borrow)
let print_scores = || {
println!("Scores: {:?}", scores); // &Vec<i32>
};
print_scores(); // Can be called multiple times
print_scores();
println!("Still accessible: {:?}", scores); // scores not moved
println!("\n=== FnMut: Mutable Borrow ===");
let mut counters = vec![0, 0, 0];
// FnMut closure: can mutate captured variables
let mut increment = || {
for c in &mut counters {
*c += 1;
}
};
increment(); // Can be called multiple times
increment();
increment();
println!("Counters: {:?}", counters); // [3, 3, 3]
println!("\n=== FnOnce: Ownership Consumed ===");
let name = String::from("Pizzaplace");
// FnOnce closure: consumes the captured variable
let consume = || {
println!("Consuming: {}", name);
drop(name); // Explicitly drop (consumes ownership)
};
consume();
// consume(); // ❌ Compile error: closure can only be called once
// println!("{}", name); // ❌ Compile error: name was moved
println!("\n=== FnOnce with move ===");
let data = vec![1, 2, 3, 4, 5];
// `move` forces the closure to take ownership of `data`
let compute = move || {
let sum: i32 = data.iter().sum();
println!("Sum of data: {}", sum);
// data is dropped here at end of closure
};
compute();
// println!("{:?}", data); // ❌ Compile error: data was moved into the closure
// compute(); // ❌ Could also fail if the closure consumed data
println!("\n=== Practical: Closure Type Selection ===");
let items = vec!["apple", "banana", "cherry"];
// Fn: read-only
let count = || items.len();
println!("Count (Fn): {}", count());
// FnMut: modify captured variable
let mut store = String::new();
let mut append = |item: &str| {
if !store.is_empty() { store.push_str(", "); }
store.push_str(item);
};
for item in &items {
append(item);
}
println!("Store (FnMut modified): {}", store);
let store_len = store.len();
println!("Store length: {}", store_len);
}
出力:
=== Fn: Immutable Borrow ===
Scores: [10, 20, 30]
Scores: [10, 20, 30]
Still accessible: [10, 20, 30]
=== FnMut: Mutable Borrow ===
Counters: [3, 3, 3]
=== FnOnce: Ownership Consumed ===
Consuming: Pizzaplace
=== FnOnce with move ===
Sum of data: 15
=== Practical: Closure Type Selection ===
Count (Fn): 3
Store (FnMut modified): apple, banana, cherry
Store length: 20
コンパイラは、クロージャの本体がキャプチャされた変数にどのようにアクセスするか based on に基づいて、使用するトレイトを自動的に推論します:読み取り専用アクセス ➔
Fn、変更可能なアクセス ➔FnMut(この場合、クロージャはmutで宣言されている必要があります)、所有権の消費 ➔FnOnce(1回のみ呼び出し可能)。キーワードmoveは、所有権をクロージャへ強制的に譲渡します。これはマルチスレッドのシナリオで一般的に使用されます(thread::spawnには'staticライフタイムが必要です)。
(3) ▶ サンプル:引数としてのクロージャー――関数ポインタと Fn トレイトの比較 (難易度 ⭐⭐)
// ============================================
// Closures as parameters: fn pointer vs Fn trait
// ============================================
// --- Version 1: Accepts ONLY function pointers ---
// fn(T) -> U is a function pointer type (no environment capture)
fn apply_fn_pointer(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
// --- Version 2: Accepts ANY Fn trait (including closures) ---
// impl Fn(T) -> U accepts both fn pointers and closures
fn apply_fn<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
// --- Version 3: FnMut parameter ---
fn apply_twice<F>(mut f: F, x: i32) -> i32
where
F: FnMut(i32) -> i32,
{
f(f(x))
}
// --- Version 4: FnOnce parameter ---
fn apply_once<F>(f: F, x: i32) -> i32
where
F: FnOnce(i32) -> i32,
{
f(x)
}
// A regular function (can be used as fn pointer)
fn square(x: i32) -> i32 {
x * x
}
fn main() {
// --- Regular functions as fn pointers ---
println!("--- Function Pointers ---");
// `square` is a function, can be passed as fn(i32) -> i32
println!("square(5) via fn ptr: {}", apply_fn_pointer(square, 5));
// Annotated closure (no capture) can be coerced to fn pointer
let triple = |x: i32| x * 3;
println!("triple(5) via fn ptr: {}", apply_fn_pointer(triple, 5));
// --- Closures with Fn trait ---
println!("\n--- Fn Trait (generic) ---");
let base = 10;
// This closure captures `base`, so it CANNOT be a fn pointer
let add_base = |x: i32| x + base;
println!("add_base(5): {}", apply_fn(add_base, 5));
// Both fn pointers and closures work with Fn trait
println!("square(6) via Fn: {}", apply_fn(square, 6));
// --- FnMut in action ---
println!("\n--- FnMut ---");
let mut accum = 0;
let mut accumulate = |x: i32| {
accum += x;
accum
};
println!("accumulate(5): {}", apply_twice(&mut accumulate, 5));
// After apply_twice: accum = 5 + 5 = 10, then 10 + 5 = 15
println!("Final accum: {}", accum);
// --- FnOnce in action ---
println!("\n--- FnOnce ---");
let owned = String::from("value: ");
let describe = |x: i32| {
println!("{} {}", owned, x);
x
};
println!("describe(42): {}", apply_once(describe, 42));
// describe was consumed (FnOnce), cannot call again
// --- Practical: scoring system with different strategies ---
println!("\n--- Practical: Scoring Strategies ---");
fn run_strategy<F>(reviews: &[u32], label: &str, strategy: F)
where
F: Fn(u32) -> u32,
{
let scores: Vec<u32> = reviews.iter().map(|&r| strategy(r)).collect();
println!("{}: {:?}", label, scores);
}
let ratings = [4, 2, 5, 3, 1];
let bonus_threshold = 4;
let bonus_points = 1;
// Different scoring strategies, all as closures
run_strategy(&ratings, "Normal", |r| r);
run_strategy(&ratings, "Double", |r| r * 2);
run_strategy(&ratings, "Bonus", |r| if r >= bonus_threshold { r + bonus_points } else { r });
run_strategy(&ratings, "Penalty", |r| if r < 3 { 0 } else { r });
}
出力:
--- Function Pointers ---
square(5) via fn ptr: 25
triple(5) via fn ptr: 15
--- Fn Trait (generic) ---
add_base(5): 15
square(6) via Fn: 36
--- FnMut ---
accumulate(5): 15
Final accum: 15
--- FnOnce ---
value: 42
describe(42): 42
--- Practical: Scoring Strategies ---
Normal: [4, 2, 5, 3, 1]
Double: [8, 4, 10, 6, 2]
Bonus: [5, 2, 6, 3, 1]
Penalty: [4, 0, 5, 3, 0]
関数ポインタ
fn(i32) -> i32は、通常の関数および環境をキャプチャしないクロージャのみを受け入れることができます。Fnトレイト(ジェネリック)は、あらゆるクロージャ(環境をキャプチャするものを含む)を受け入れることができます。選択ルール:FFI の場合や、固定の関数型を格納する必要がある場合はfnポインタを使用し、環境をキャプチャするクロージャが必要なシナリオではFn/FnMut/FnOnceトレイトを受け入れる。run_strategy関数は、impl Fn(u32) -> u32(4種類の異なるスコアリング戦略)をクロージャとして受け入れます。これには、bonus_thresholdおよびbonus_pointsをキャプチャするボーナス戦略も含まれます。
(4) ▶ サンプル:クロージャとイテレータの組み合わせ — map / filter / collect (難易度 ⭐⭐⭐)
// ============================================
// Closures + Iterator combinators: map, filter, collect
// ============================================
#[derive(Debug)]
struct Order {
store: String,
items: Vec<String>,
rating: u32,
}
fn main() {
let orders = vec![
Order {
store: String::from("Pizzaplace"),
items: vec!["Margherita".into(), "Cola".into()],
rating: 4,
},
Order {
store: String::from("BurgerKing"),
items: vec!["Whopper".into(), "Fries".into(), "Shake".into()],
rating: 2,
},
Order {
store: String::from("SushiBar"),
items: vec!["Salmon".into(), "Tuna".into()],
rating: 5,
},
Order {
store: String::from("NoodleHouse"),
items: vec!["Ramen".into(), "Gyoza".into(), "Tea".into()],
rating: 3,
},
];
// --- map: transform each element ---
println!("--- map: store names uppercase ---");
let store_names: Vec<String> = orders
.iter()
.map(|o| o.store.to_uppercase())
.collect();
println!("{:?}", store_names);
// --- filter: keep elements matching a condition ---
println!("\n--- filter: orders with rating >= 4 ---");
let good_orders: Vec<&Order> = orders
.iter()
.filter(|o| o.rating >= 4)
.collect();
for o in &good_orders {
println!("{:?}", o);
}
// --- filter + map: chained ---
println!("\n--- filter + map: good store names ---");
let good_stores: Vec<String> = orders
.iter()
.filter(|o| o.rating >= 4)
.map(|o| format!("★ {} (rating: {})", o.store, o.rating))
.collect();
for s in &good_stores {
println!("{}", s);
}
// --- map + filter: compute and refine ---
println!("\n--- map + filter: average rating per item ---");
let avg_ratings: Vec<String> = orders
.iter()
.map(|o| {
// Compute average rating per item
let item_count = o.items.len() as f64;
let avg = o.rating as f64 / item_count;
(o.store.clone(), avg, o.items.len())
})
.filter(|(_, avg, _)| *avg > 1.5) // Only stores with high per-item rating
.map(|(store, avg, count)| format!("{}: {:.2}/item ({} items)", store, avg, count))
.collect();
for s in &avg_ratings {
println!("{}", s);
}
// --- Advanced: closures capturing external state ---
println!("\n--- Advanced: dynamic threshold filtering ---");
let min_rating = 3;
let category = "Premium";
let premium_stores: Vec<String> = orders
.iter()
.filter(|o| o.rating >= min_rating)
.map(|o| format!("[{}] {} (rating: {})", category, o.store, o.rating))
.collect();
for s in &premium_stores {
println!("{}", s);
}
// --- All items from high-rated stores ---
println!("\n--- Flat items from high-rated stores ---");
let threshold = 3;
let recommended_items: Vec<&String> = orders
.iter()
.filter(|o| o.rating > threshold)
.flat_map(|o| o.items.iter())
.collect();
println!("Recommended: {:?}", recommended_items);
// --- Custom scoring with filter ---
println!("\n--- Custom filter: weighted score ---");
let weight = 1.2;
let min_score = 4.0;
let top_orders: Vec<&Order> = orders
.iter()
.filter(|o| (o.rating as f64 * weight) >= min_score)
.collect();
for o in &top_orders {
let weighted = o.rating as f64 * weight;
println!("{}: raw={}, weighted={:.1}, pass={}", o.store, o.rating, weighted, weighted >= min_score);
}
// --- count, any, all ---
println!("\n--- Aggregate checks ---");
let high_rated_count = orders.iter().filter(|o| o.rating >= 4).count();
println!("Orders with rating >= 4: {}", high_rated_count);
let has_bad_order = orders.iter().any(|o| o.rating <= 1);
println!("Has any bad order (rating <= 1): {}", has_bad_order);
let all_rated = orders.iter().all(|o| o.rating >= 1);
println!("All orders rated at least 1: {}", all_rated);
}
出力:
--- map: store names uppercase ---
["PIZZAPLACE", "BURGERKING", "SUSHIBAR", "NOODLEHOUSE"]
--- filter: orders with rating >= 4 ---
Order { store: "Pizzaplace", items: ["Margherita", "Cola"], rating: 4 }
Order { store: "SushiBar", items: ["Salmon", "Tuna"], rating: 5 }
--- filter + map: good store names ---
★ Pizzaplace (rating: 4)
★ SushiBar (rating: 5)
--- map + filter: average rating per item ---
Pizzaplace: 2.00/item (2 items)
SushiBar: 2.50/item (2 items)
--- Advanced: dynamic threshold filtering ---
[Premium] Pizzaplace (rating: 4)
[Premium] SushiBar (rating: 5)
[Premium] NoodleHouse (rating: 3)
--- Flat items from high-rated stores ---
Recommended: ["Margherita", "Cola", "Salmon", "Tuna"]
--- Custom filter: weighted score ---
Pizzaplace: raw=4, weighted=4.8, pass=true
SushiBar: raw=5, weighted=6.0, pass=true
--- Aggregate checks ---
Orders with rating >= 4: 2
Has any bad order (rating <= 1): false
All orders rated at least 1: true
イテレータコンビネータは、クロージャの最も強力な応用例の一つです:
map(各要素の変換)、filter(条件に基づく要素の保持)、flat_map(ネストされたイテレータの平坦化)、any/all(集計チェック)、count(カウント)。ここでのクロージャの価値は、外部の動的値を捕捉する能力(min_rating、weight、categoryなど)にあり、これによりデータ処理ロジックと設定を分離できます。連鎖呼び出しiter().filter().map().collect()は、Rust において最も表現力豊かなイディオムの一つです。
(5) ▶ サンプル:総合演習 — クロージャを用いた設定駆動型データパイプラインの実装(難易度 ⭐⭐⭐)
// ============================================
// Comprehensive Example: Closure Capture + Higher-Order Functions + Iterator Chain
// ============================================
struct DataPipeline<'a, T> {
data: Vec<T>,
filters: Vec<Box<dyn Fn(&T) -> bool + 'a>>,
transforms: Vec<Box<dyn Fn(T) -> T + 'a>>,
}
impl<'a, T: Clone + 'a> DataPipeline<'a, T> {
fn new(data: Vec<T>) -> Self {
DataPipeline { data, filters: Vec::new(), transforms: Vec::new() }
}
fn filter<F: Fn(&T) -> bool + 'a>(mut self, f: F) -> Self {
self.filters.push(Box::new(f));
self
}
fn map<F: Fn(T) -> T + 'a>(mut self, f: F) -> Self {
self.transforms.push(Box::new(f));
self
}
fn execute(self) -> Vec<T> {
let mut result = self.data;
for f in &self.filters {
result.retain(f);
}
for t in &self.transforms {
result = result.into_iter().map(t).collect();
}
result
}
}
fn main() {
let min_score = 60;
let bonus = 10;
let max_score = 100;
let pipeline = DataPipeline::new(vec![85, 42, 95, 58, 73, 30, 88])
.filter(move |&&x| x >= min_score)
.filter(|&&x| x < max_score)
.map(move |x| x + bonus);
let result = pipeline.execute();
println!("Filter+ After the bonus points are added: {:?}", result);
let multiplier = 2;
let adjusted: Vec<i32> = result.into_iter()
.map(|x| x * multiplier)
.filter(|x| *x < 200)
.collect();
println!("Double+ Upper Limit Filtering: {:?}", adjusted);
let threshold = 150;
let has_high = adjusted.iter().any(|&x| x > threshold);
let all_above_100 = adjusted.iter().all(|&x| x > 100);
println!("Has values greater than {}: {}, All > 100: {}", threshold, has_high, all_above_100);
}
出力:
Filter+ After the bonus points are added: [95, 83, 98]
Double+ Upper Limit Filtering: [190, 166, 196]
Has values greater than 150: true, All > 100: true
DataPipelineはBox<dyn Fn>を使用してクロージャを格納し、moveは外部の設定値 (min_score、bonus) を取得し、filter().map()への連鎖呼び出しによって処理パイプラインが構築されます。クロージャにより、「設定とロジックを分離する」ことが可能になります。
❓ よくある質問
move キーワードはいつ使用すべきですか?fn(T) -> U とクロージャートレート Fn(T) -> U は互換性がありますか?impl Fn、Box<dyn Fn>、それとも汎用的な F: Fn のどれを選ぶべきでしょうか?F: Fn はパフォーマンスが最も優れており(静的ディスパッチ)、Box<dyn Fn> は最も柔軟性が高く(動的ディスパッチ)、impl Fn はジェネリックのシンタックスシュガーです。📖 まとめ
- クロージャ構文
|params| exprまたは|params| { block };パラメータ型および戻り値型は通常、推論される - 3つのキャプチャモード:
Fn(不変の借用、複数回呼び出し可能)、FnMut(可変の借用、複数回呼び出し可能)、FnOnce(所有権を消費、1回のみ呼び出し可能) moveこのキーワードは、キャプチャされた変数の所有権をクロージャに移すように強制します。これは、マルチスレッドプログラミングにおける標準的な手法です。- 関数のパラメータはクロージャを受け入れることができます:
fnポインタ(キャプチャなし)またはFn/FnMut/FnOnceトレイト(ジェネリック) - クロージャとイテレータの組み合わせ (
map,filter,flat_map,any,all) は、Rust の最も強力なイディオムの一つです - Rustのクロージャはゼロコストの抽象化であり、コンパイル時にインライン化されるため、実行時のオーバーヘッドはゼロです
📝 練習問題
-
難易度 ⭐:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]を含むVec<i32>があります。変数threshold(値は 5)をキャプチャするクロージャを作成し、filterを使ってthresholdより大きい数値をフィルタリングし、mapを使ってそれらを 2 倍にし、最後にcollectを使って新しい Vec に追加し、結果を出力してください。 -
難易度 ⭐⭐: 各要素に変換関数を適用する関数
fn transform<F>(data: &[i32], f: F) -> Vec<i32> where F: Fn(i32) -> i32を作成してください。関数main内で、3つのクロージャを作成してください:|x| x * 2(double型)、変数addをキャプチャする加算クロージャ、および変数max_valをキャプチャする切り捨てクロージャ(値がmax_valを超える場合はmax_valを返すもの)。それぞれについてtransformを呼び出し、結果を出力してください。 -
難易度 ⭐⭐⭐:
struct Product { name: String, price: f64, category: String }という名前の構造体があります。6つの商品インスタンスを作成し、それらをベクトルに格納してください。関数fn analyze_products<F1, F2>(products: &[Product], category_filter: F1, price_adjuster: F2) where F1: Fn(&&Product) -> bool, F2: Fn(f64) -> f64を記述し、まずfilterを使用してカテゴリでフィルタリングし、次にmapを使用して価格を調整し、最後に調整後の価格順に商品を並べ替えて、最も高価な商品の上位3つを出力するようにしてください。main関数内で、異なるフィルタリングおよび価格調整の戦略を用いて、これを少なくとも2回テストしてください。



