あっぽログ
← 記事一覧に戻る

RustのAsyncトレイトを使いこなす:trait内でasync fnを定義して非同期処理を抽象化する

はじめに

Rustで非同期プログラミングを進めていくと、「トレイトの中でも async fn を使いたい」という場面に必ず出会います。たとえば、複数のデータソース(データベース・API・ファイル)を同じインターフェースで扱いたいときなどです。

しかし、Rustの標準トレイトでは長らく async fn の定義が制限されており、工夫が必要でした。本記事では、その背景と解決策、そして実践的な使い方を順を追って解説します。


なぜトレイトでasync fnが難しいのか

Rustの async fn は、内部的に Future トレイトを実装した匿名型を返します。通常の関数であれば問題ありませんが、トレイトでは戻り値の型がコンパイル時に確定している必要があるため、各実装ごとに異なる匿名型を返す async fn がそのままでは使えませんでした。

// これはRust 1.74以前ではコンパイルできなかった
trait Fetcher {
    async fn fetch(&self, url: &str) -> String; // エラー!
}

方法①:async-trait crateを使う

最もよく使われてきた解決策が、async-trait クレートです。

セットアップ

# Cargo.toml
[dependencies]
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }

基本的な使い方

use async_trait::async_trait;

#[async_trait]
trait Fetcher {
    async fn fetch(&self, url: &str) -> String;
}

struct HttpFetcher;

#[async_trait]
impl Fetcher for HttpFetcher {
    async fn fetch(&self, url: &str) -> String {
        // 実際にはreqwestなどを使うが、ここでは簡略化
        format!("Response from {}", url)
    }
}

#[tokio::main]
async fn main() {
    let fetcher = HttpFetcher;
    let result = fetcher.fetch("https://example.com").await;
    println!("{}", result);
}

#[async_trait] マクロをトレイト定義と impl ブロックの両方に付けるだけで動作します。内部的には Pin<Box<dyn Future + Send>> に変換されています。


方法②:Rust 1.75以降のネイティブサポート

Rust 1.75(2023年12月リリース)から、impl Trait in return position for traits(RPITIT)と合わせて、トレイト内での async fn がネイティブサポートされるようになりました。

trait Fetcher {
    async fn fetch(&self, url: &str) -> String;
}

struct HttpFetcher;

impl Fetcher for HttpFetcher {
    async fn fetch(&self, url: &str) -> String {
        format!("Response from {}", url)
    }
}

async-trait クレートなしでそのまま書けます。ただし、dyn トレイトオブジェクトとして使う場合には制限があるため、後述します。


実践:複数の実装を切り替える

async traitの真価は、実装を差し替えられる点にあります。たとえばテスト用のモック実装と本番実装を同じトレイトで扱う例を見てみましょう。

use async_trait::async_trait;

#[async_trait]
trait UserRepository {
    async fn find_by_id(&self, id: u64) -> Option<String>;
}

// 本番実装(DBなどを想定)
struct DbUserRepository;

#[async_trait]
impl UserRepository for DbUserRepository {
    async fn find_by_id(&self, id: u64) -> Option<String> {
        // 実際はDBクエリ
        Some(format!("User#{}", id))
    }
}

// テスト用モック実装
struct MockUserRepository;

#[async_trait]
impl UserRepository for MockUserRepository {
    async fn find_by_id(&self, id: u64) -> Option<String> {
        if id == 1 {
            Some("Alice".to_string())
        } else {
            None
        }
    }
}

// dynトレイトオブジェクトとして受け取る
async fn greet_user(repo: &dyn UserRepository, id: u64) {
    match repo.find_by_id(id).await {
        Some(name) => println!("Hello, {}!", name),
        None => println!("User not found."),
    }
}

#[tokio::main]
async fn main() {
    let db_repo = DbUserRepository;
    greet_user(&db_repo, 42).await;

    let mock_repo = MockUserRepository;
    greet_user(&mock_repo, 1).await;
    greet_user(&mock_repo, 99).await;
}

出力:

Hello, User#42!
Hello, Alice!
User not found.

async_trait を使えば dyn UserRepository として関数に渡すことができ、依存性の注入(DI)パターンにも応用できます。


ネイティブasync fnとdyn Traitの注意点

Rust 1.75以降のネイティブサポートでは、dyn Trait として使う際にまだ制限があります。

// これはエラーになる(ネイティブサポートのみの場合)
async fn greet_user(repo: &dyn Fetcher, id: u64) { ... }

dyn Trait で使いたい場合は、引き続き async-trait クレートを使うか、trait_variant クレートを検討してください。静的ディスパッチ(ジェネリクスの impl Trait)であればネイティブでも問題なく動作します。

// ジェネリクスならネイティブでOK
async fn greet_user<R: Fetcher>(repo: &R, url: &str) {
    let result = repo.fetch(url).await;
    println!("{}", result);
}

まとめ

方法dyn Trait外部クレート推奨シーン
async-trait クレート✅ 可能必要dyn使用・安定重視
ネイティブ async fn⚠️ 制限あり不要静的ディスパッチ
  • async-trait クレートdyn Trait が必要なときや、幅広い安定性が求められるときに最適
  • ネイティブ async fn:ジェネリクスで静的ディスパッチを使う場合はシンプルに書ける

非同期トレイトをうまく使いこなすことで、テストしやすく・拡張しやすいRustコードを書けるようになります。ぜひ実際のプロジェクトで試してみてください!

← 記事一覧に戻る