【Rust】非同期クロージャ(async || {})の書き方・完全ガイド【2026/2024 Edition対応】

目次

結論

Rust 2024 Edition(1.85以降)で非同期クロージャが安定化しました。これまで面倒だった「クロージャ内でawaitしたい」ケースが、直感的に書けるようになります。

// 新しい書き方(Rust 2024 Edition)
let urls = vec!["https://example.com", "https://rust-lang.org"];
let fetcher = async |url: &str| {
    let resp = reqwest::get(url).await;
    resp.text().await
};

// AsyncFnトレイト境界もシンプル
async fn process<T, F>(items: Vec<T>, f: F)
where
    F: AsyncFn(&T) -> String,
{
    for item in &items {
        let result = f(item).await;
        println!("{}", result);
    }
}

これがどう便利なのか、旧来の書き方と比較しながら見ていきましょう。

非同期クロージャとは

非同期クロージャ(Async Closure)は、asyncキーワードをクロージャに直接付与できる構文です。次の2つの問題を一気に解決しました。これは嬉しい。

  1. 借用したキャプチャをawait越しに保持できない問題
  2. 高階な非同期関数シグネチャ(HRTB + async)が書けない問題

旧来の書き方(2024 Edition以前)

これまでは、クロージャ内でawaitしたい場合、|| async {} という形で書く必要がありました。「クロージャがasyncブロックを返す」という、ちょっと回りくどい書き方ですね。

// 古い書き方:クロージャがFutureを返す
let old_style = |url: &str| async move {
    let resp = reqwest::get(url).await;
    resp.text().await
};

この書き方には大きな制約があります。async move を使うとキャプチャがムーブされてしまい、await後に借用したデータを使い回せません。実際に書いてみると結構ストレスなんですよね。

新しい書き方(2024 Edition以降)

// 新しい書き方:非同期クロージャ
let new_style = async |url: &str| {
    let resp = reqwest::get(url).await;
    let body = resp.text().await;
    body.len() // ここでurlの借用が生きている!
};

async || {} と書くだけで、借用した値をawaitを越えて保持できます。むしろ最初からこう書ければよかったのにと思いました。

AsyncFnトレイトの使い方

非同期クロージャの型を表現するのが AsyncFn・AsyncFnMut・AsyncFnOnce トレイトです。通常のFn・FnMut・FnOnceに対応する非同期版で、直感的に使えます。

// AsyncFnとして受け取る
async fn retry<T, F>(
    times: usize,
    f: F,
) -> Vec<T>
where
    F: AsyncFn(usize) -> T,
{
    let mut results = vec![];

    for i in 0..times {
        results.push(f(i).await);
    }

    results
}

#[tokio::main]
async fn main() {
    let results = retry(3, async |i| {
        do_something(i).await
    }).await;

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

ポイントは以下の3つです。どれも押さえておきたいところですね。

  • async Fn() 境界:非同期で呼び出せるクロージャの型を表現
  • 既存の Fn() -> impl Future も自動実装:後方互換性あり。移行のハードルが低いのはありがたい
  • 3つのバリエーション:AsyncFn(不変借用)、AsyncFnMut(可変借用)、AsyncFnOnce(消費)

実践ユースケース

1. イテレータと非同期処理の組み合わせ

今まではイテレータ内でawaitするのに一工夫必要でしたが、もうそんな心配は不要です。

use futures::future::join_all;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = vec![
        "https://api.example.com/users",
        "https://api.example.com/posts",
        "https://api.example.com/comments",
    ];

    let fetch = async |url: &str| {
        let resp = reqwest::get(url).await?;
        let text = resp.text().await?;
        Ok::<_, reqwest::Error>(text)
    };

    let futures: Vec<_> = urls.iter().map(|u| fetch(u)).collect();
    let results = join_all(futures).await;

    for result in &results {
        if let Ok(body) = result {
            println!("Fetched {} bytes", body.len());
        }
    }

    Ok(())
}

2. ミドルウェアパターン

処理の前後に決まった処理を挟みたいケース。タイミングの計測なんかが典型的です。

use std::time::Instant;

async fn with_timing<F, T>(
    label: &str,
    f: F,
) -> T
where
    F: AsyncFn() -> T,
{
    let start = Instant::now();
    let result = f().await;
    let elapsed = start.elapsed();
    println!("[Timing] {}: {:?}", label, elapsed);
    result
}

#[tokio::main]
async fn main() {
    let data = with_timing("fetch_data", async || {
        fetch_from_database().await
    }).await;

    println!("Got data: {:?}", data);
}

3. エラーハンドリングとリトライ

リトライ処理といえば昔からある定番パターンですが、非同期クロージャを使うとこんなにスッキリ!

use std::time::Duration;
use tokio::time::sleep;

async fn retry_with_backoff<F, T, E>(
    max_retries: u32,
    f: F,
) -> Result<T, E>
where
    F: AsyncFn() -> Result<T, E>,
    E: std::fmt::Display,
{
    let mut attempt = 0;

    loop {
        match f().await {
            Ok(value) => return Ok(value),
            Err(err) => {
                attempt += 1;
                if attempt >= max_retries {
                    return Err(err);
                }
                eprintln!("Attempt {} failed: {}", attempt, err);
                sleep(Duration::from_millis(100 * u64::from(attempt))).await;
            },
        }
    }
}

#[tokio::main]
async fn main() {
    let result = retry_with_backoff(3, async || {
        unreliable_api_call().await
    }).await;

    match result {
        Ok(data) => println!("Success: {}", data),
        Err(e)  => println!("All retries failed: {}", e),
    }
}

移行ガイド

Rust 2024 Editionへの移行はそれほど難しくありません。既存の || async {}async || {} に書き換えていくイメージです。

// Before: クロージャ+asyncブロック
let before = |x: &str| async move {
    process(x).await
};

// After: 非同期クロージャ(借用を維持)
let after = async |x: &str| {
    process(x).await
};

Cargo.tomlのeditionも忘れずに更新しておきましょう。

[package]
name = "your-project"
version = "0.1.0"
edition = "2024"  # 2021 → 2024

注意点として、|| async {} から async || {} への書き換えはClippyの提案レベルなので、手動で確認する必要があります。cargo fix --edition で大半の互換性問題は自動修正されるので、まずはそれを実行してみてくださいね。

ところで、Rustの開発環境を整えるなら独自ドメインの取得も検討してみてください。ブログやポートフォリオに独自ドメインを設定すると、学習成果の発表や技術記事の発信が格段にしやすくなります。

まとめ

Rust 2024 Editionで安定化された非同期クロージャのメリットをまとめます。

項目旧方式 (|| async {})非同期クロージャ (async || {})
借用の保持async moveでムーブ借用を維持できる
構文の簡潔さやや冗長直感的
高階関数境界HRTBで煩雑AsyncFnでシンプル
型推論手動指定が必要自動推論

非同期Rustのコードは同期コードと比べて複雑になりがちでしたが、非同期クロージャの登場でそのギャップだいぶ縮まりました。特にmapやfilterのようなイテレータ処理と非同期処理の組み合わせが、違和感なく書けるようになったのは大きいですね。

まだ async || {} に慣れていない方は、ぜひ既存の || async {} を書き換えてみてください。コードの見通しが格段に良くなりますよ。

【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次