Progrust Library.

// dictionary

impl Trait.

impl Traitは、具体的な型名の代わりに「このトレイトを実装した型」とだけ書く記法です。書けるのは関数の引数と戻り値の型の位置だけです1。externブロック内で宣言する外部関数には書けません2。

戻り値の位置に書くと、「そのトレイトを実装した具体的な型を1つ返す」という意味になります3。どの型を返すかを決めるのは関数側で、呼び出し側には型名が伏せられ、指定したトレイトが宣言するメソッドだけが使えます3。特に役立つのはクロージャとイテレータです。クロージャの型は書き表せる名前を持たず、イテレータはメソッドを重ねるほど型名が長く複雑になるためです4。

// 割引率から「価格に割引を適用するクロージャ」を作って返す
// クロージャの型には名前がないので、impl Fn で「呼び出せる何か」として返す
fn make_discount(rate: u32) -> impl Fn(u32) -> u32 {
    move |price| price * (100 - rate) / 100
}

// メソッドを重ねたイテレータの型は長くなる
// impl Iterator と書けば「u32を順に返すもの」とだけ公開できる
fn half_prices(prices: Vec<u32>) -> impl Iterator<Item = u32> {
    prices.into_iter().map(|price| price / 2)
}

fn main() {
    let apply_20off = make_discount(20);
    println!("1500円の2割引きは{}円", apply_20off(1500));

    for price in half_prices(vec![100, 150, 200]) {
        println!("半額は{}円", price);
    }
}
Playgroundで開く

引数位置との意味の違い

引数の位置のimpl Traitは、名前のない型引数を1つ宣言する糖衣構文で、実際の型を選ぶのは呼び出し側です5。戻り値の位置では逆に、型を選ぶのは関数側で、呼び出し側は型を指定できません6。同じ記法ですが、位置によって役割が入れ替わります。

観点 引数位置 fn f(x: impl Trait) 戻り値位置 fn f() -> impl Trait
具体的な型を決めるのは 呼び出し側5 関数側6
意味 名前のない型引数(トレイト境界付きの<T: Trait>の糖衣構文)5 具体的な型を伏せた抽象型3
呼び出しごとの型 渡した型ごとに変わる5 関数(とそのジェネリック引数)ごとに1つに固定される3

なお、ジェネリクスの型引数を戻り値に使ったfn f<T: Trait>() -> Tは、戻り値位置のimpl Traitとは別物です。前者は呼び出し側がTを決めて関数はその型を返すのに対し、後者は関数が型を決め「トレイトを実装していること」だけを約束します6。

返せる型は1つだけ

impl Traitの裏側には隠された具体的な型が1つあり、関数のどの経路を通っても同じ具体的な型に解決される必要があります3。そのため分岐で違う型を返すことはできず、エラー: E0308になります78。

trait Payment {
    fn pay(&self, amount: u32) -> String;
}

struct Cash;
struct CreditCard;

impl Payment for Cash {
    fn pay(&self, amount: u32) -> String {
        format!("現金で{}円を支払いました", amount)
    }
}

impl Payment for CreditCard {
    fn pay(&self, amount: u32) -> String {
        format!("カードで{}円を支払いました", amount)
    }
}

fn pick(use_card: bool) -> impl Payment {
    // エラー: E0308(ifとelseで返す型が違う)
    if use_card { CreditCard } else { Cash }
}

実行時に型を切り替えたい場合は、トレイトオブジェクトを使ってBox<dyn Payment>を返します7。

観点 -> impl Trait -> Box<dyn Trait>
返せる型 常に同じ1つの型3 実装した型のどれでもよい7
ヒープ確保 なし4 あり4
メソッド呼び出し 静的ディスパッチ4 動的ディスパッチ4
使えるトレイト 任意 dyn互換なトレイトのみ9

補足

呼び出し側にはトレイトのメソッドしか見えない

戻り値がimpl Traitだと、呼び出し側からは具体的な型が見えないため、そのトレイト(とスーパートレイト)が宣言するメソッド以外は呼べません3。下の例では範囲式1..=3が作るRangeInclusive<u16>を返していますが、impl Iteratorとしか宣言していないので、ExactSizeIteratorのlenは呼べずエラー: E0599になります。

fn steps() -> impl Iterator<Item = u16> {
    1..=3
}

fn main() {
    // エラー: E0599(不透明型にlenというメソッドはない)
    println!("{}", steps().len());
}

具体的な型を返すと決めているなら、-> std::ops::RangeInclusive<u16>のように書けば呼び出し側はlenも使えます。impl Traitは「公開する能力をトレイトの範囲に絞る」記法でもあります。

トレイトのメソッドの戻り値にも書ける(RPITIT)

トレイト宣言の中のメソッドの戻り値にもimpl Traitを書けます。これは匿名の関連型への糖衣構文として扱われ、実装側のシグネチャに書いた型がその関連型の中身になります10。

trait Shop {
    // 「名前を順に返すもの」とだけ決めて、実際の型は実装側に任せる
    fn items(&self) -> impl Iterator<Item = String>;
}

struct Bakery;

impl Shop for Bakery {
    fn items(&self) -> impl Iterator<Item = String> {
        ["食パン", "クロワッサン"].into_iter().map(String::from)
    }
}

fn main() {
    for item in Bakery.items() {
        println!("{}", item);
    }
}
Playgroundで開く

ただし戻り値位置にimpl Traitを持つメソッドはdyn互換の条件を満たさないため、そのままではdyn Shopにできません9。

ジェネリック引数の捕捉とuse<..>

impl Traitが隠している具体的な型が、関数の型引数やライフタイムを含むことがあります。そのため戻り値位置のimpl Traitは、スコープにあるジェネリック引数(型・const・ライフタイム)をすべて自動的に捕捉します11。

捕捉する対象はuse<..>境界で明示的に絞れます12。たとえばuse<>(何も捕捉しない)と書くと、引数の参照とは無関係な戻り値であることを表明できます。

// use<> により「引数の参照を捕捉しない」ことを明示する
fn to_prices(text: &str) -> impl Iterator<Item = u32> + use<> {
    let prices: Vec<u32> = text.split(',').filter_map(|s| s.parse().ok()).collect();
    prices.into_iter()
}

fn main() {
    let input = String::from("150,980,120");
    let prices = to_prices(&input);
    drop(input); // 捕捉していないので、元のStringを先に破棄してよい

    for price in prices {
        println!("{}円", price);
    }
}
Playgroundで開く
書けない場所

impl Traitは変数の型注釈や構造体のフィールドの型には書けず、書くとエラー: E0562になります12。型エイリアス(type Shown = impl Display;)も安定版では使えず、こちらはエラー: E0658(unstable)になります。

use std::fmt::Display;

fn main() {
    // エラー: E0562(変数束縛の型にimpl Traitは書けない)
    let price: impl Display = 1500;
    println!("{}", price);
}

Footnotes

  1. The Rust Reference - Impl trait limitations “impl Trait can only appear as a parameter or return type of a non-extern function. It cannot be the type of a let binding, field type, or appear inside a type alias.” ↩ ↩2

  2. Error code E0562 “impl Trait is only allowed as a function return and argument type.” ↩ ↩2

  3. The Rust Reference - Abstract return types “Functions can use impl Trait to return an abstract return type. These types stand in for another concrete type where the caller may only use the methods declared by the specified Trait.” / “Each possible return value from the function must resolve to the same concrete type.” ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  4. The Rust Reference - Abstract return types “impl Trait in return position allows a function to return an unboxed abstract type. This is particularly useful with closures and iterators. For example, closures have a unique, un-writable type.” / “This could incur performance penalties from heap allocation and dynamic dispatch.” / “which also avoids the drawbacks of using a boxed trait object.” / “Similarly, the concrete types of iterators could become very complex, incorporating the types of all previous iterators in a chain.” ↩ ↩2 ↩3 ↩4 ↩5

  5. The Rust Reference - Anonymous type parameters “impl Trait in argument position is syntactic sugar for a generic type parameter like <T: Trait>, except that the type is anonymous and doesn’t appear in the GenericParams list.” / “The caller must provide a type that satisfies the bounds declared by the anonymous type parameter.” ↩ ↩2 ↩3 ↩4

  6. The Rust Reference - Differences between generics and impl Trait in return position “In argument position, impl Trait is very similar in semantics to a generic type parameter. However, there are significant differences between the two in return position. With impl Trait, unlike with a generic type parameter, the function chooses the return type, and the caller cannot choose the return type.” / “doesn’t allow the caller to determine the return type. Instead, the function chooses the return type, but only promises that it will implement Trait.” ↩ ↩2 ↩3

  7. The Rust Programming Language - Returning Types That Implement Traits “However, you can only use impl Trait if you’re returning a single type.” / “Returning either a NewsArticle or a SocialPost isn’t allowed due to restrictions around how the impl Trait syntax is implemented in the compiler.” / “We’ll cover how to write a function with this behavior in the "Using Trait Objects to Abstract over Shared Behavior" section of Chapter 18.” ↩ ↩2 ↩3

  8. Error code E0308 “Expected type did not match the received type.” ↩

  9. The Rust Reference - Dyn compatibility “Dispatchable functions must: … Not have an opaque return type” ↩ ↩2

  10. The Rust Reference - Return-position impl Trait in traits and trait implementations “Every impl Trait in the return type of an associated function in a trait is desugared to an anonymous associated type. The return type that appears in the implementation’s function signature is used to determine the value of the associated type.” ↩

  11. The Rust Reference - Automatic capturing “Return-position impl Trait abstract types automatically capture all in-scope generic parameters, including generic type, const, and lifetime parameters (including higher-ranked ones).” / “2024 Edition differences: Before the 2024 edition, on free functions and on associated functions and methods of inherent impls, generic lifetime parameters that do not appear in the bounds of the abstract return type are not automatically captured.” ↩ ↩2

  12. The Rust Reference - Precise capturing “The set of generic parameters captured by a return-position impl Trait abstract type may be explicitly controlled with a use<..> bound. If present, only the generic parameters listed in the use<..> bound will be captured.” ↩

この辞書が使われているページ

backlinks 5

  1. 辞書トレイトオブジェクト
  2. 辞書トレイト境界
  3. 辞書関連型
  4. 本動かして学ぶRustプログラミング問題集 › 第20章 トレイト境界とimpl Trait
  5. 本動かして学ぶRustプログラミング問題集 › 第21章 トレイトオブジェクトと関連型