Posted at

【Rust】ジェネリクスの取説

※Rustのバージョンは1.33.0 (stable)を使用しています。

Rustは所有権やライフタイム関連で詰まることが多い印象がありますが、私は同じくらい「型の指定方法」、特に「ジェネリクスでの型指定やトレイト境界指定」周りで詰まることが多いと感じています。

これは、Rustが特別複雑なことをしているわけではなく、網羅的なドキュメントや実例の載った解説が少ないためだと思います。最近は公式ドキュメントも充実してきましたが、それでも「これってどう書くの?」「そもそも出来るの?」という時になかなか情報が見つからないことが多々あります。(主に私の英語力が問題ということもありますが)

そこで、私の理解している範囲で、詰まりやすそうな部分とその周辺をまとめたいと思います。

※ジェネリクスやトレイトの導入部分は省略しています。the bookの該当部分に丁寧に書かれているので、そちらをお勧めします。


トレイト境界

トレイト境界(bounds)とは、ジェネリック型に対して「このトレイトを実装していなければならない」という制約を課すものであり、これにより、ジェネリック型はトレイト境界で指定されたトレイトのメソッド等を使用できるようになります。

トレイト境界の書き方は大きく分けて2通りありますが、どちらもType: Traitの形で記述します。


トレイト境界の記述場所


型引数宣言部に記述

fn my_fn<T: std::fmt::Debug>(val: T)  {

println!("{:?}", val); // Debugを使用できる
}


where節に記述

fn my_fn<T>(val: T) -> ()

where
T: std::fmt::Debug,
{
println!("{:?}", val); // Debugを使用できる
}

(where節の記述場所をわかりやすくするため、戻り値の空タプルを省略せず書いています。)


どちらに記述すべきか

基本的にはどちらでも同じです。

が、where節の場合のみ、型指定部に型引数以外の情報を記述できます。


NG

// これはエラー

fn my_fn<Option<T>: std::fmt::Debug>(val: T) {
println!("{:?}", Some(val));
}


OK

// これはOK

fn my_fn<T>(val: T)
where
Option<T>: std::fmt::Debug,
{
println!("{:?}", Some(val));
}

上記の例では、TではなくOption<T>に対するトレイト境界を設定しています。型引数宣言部で指定できる型は、型引数のみに限られるので、このようなことはwhere節でのみ可能です。

この記事では今後、基本的にはwhere節での方法を使って説明していきます。


トレイト境界を指定可能なアイテム

型引数を使うことのできるアイテムは、ほぼ全て、トレイト境界を指定できます。

// struct

struct MyStruct<T>
where
T: std::fmt::Debug,
{
val: T,
}

// "tupled" struct 正式には何て呼ぶの?
// where節がタプルの定義の後になることに注意!
struct MyTupledStruct<T>(T)
where
T: std::fmt::Debug;

// impl (struct) ※
impl<T> MyStruct<T>
where
T: std::fmt::Debug,
{
// method ※
fn my_method<U>(self, my_tupled_struct: MyTupledStruct<U>) -> (T, U)
where
U: std::fmt::Debug,
{
println!("{0:?}, {1:?}", self.val, my_tupled_struct.0);
(self.val, my_tupled_struct.0)
}
}

// trait
trait MyTrait<T>
where
T: std::fmt::Debug,
{
// method ※
fn my_trait_method<U>(self, my_tupled_struct: MyTupledStruct<U>) -> (T, U)
where
U: std::fmt::Debug;
}

// impl (trait) ※
impl<T> MyTrait<T> for MyStruct<T>
where
T: std::fmt::Debug,
{
// method ※
fn my_trait_method<U>(self, my_tupled_struct: MyTupledStruct<U>) -> (T, U)
where
U: std::fmt::Debug,
{
println!("{0:?}, {1:?}", self.val, my_tupled_struct.0);
(self.val, my_tupled_struct.0)
}
}

※ トレイト境界のある構造体やトレイトを使う場合、使う側で制約を満たすことを保証しなければならなりません。この場合、※のある場所はstd::fmt::Debugのトレイト境界を設定して、制約を満たさなければエラーとなります。(制約がわかっているなら、再度記述しなくてもいいような気がするが…なぜだめなのでしょう。)

再度記述しなくてもいいような仕組みとして、「関連型」というものがあります(トレイトに限りますが)。これについては後述します。


複数のトレイト境界の設定

一つの型に複数のトレイト境界を設定する場合は、Type: Trait1 + Trait2のように+で区切ります。

複数の型にトレイト境界を設定する場合はType1: Trait1, Type2: Trait2のように,で区切ります。

fn my_fn<T, U>(a: T, b: T)

where
T: std::fmt::Display + std::fmt::Debug,
U: std::fmt::Display + std::fmt::Debug,
{
println!("{0} {1}", a, b);
println!("{0:?} {1:?}", a, b);
}

ちなみに、Rustでカンマ区切りの記述は、最後の要素にもカンマを付けて構いません(付けなくても問題なし)。rustfmtで整形した場合は、最後にカンマを付けるようです。


デフォルトの型引数

Rustでは「デフォルト引数」は今のところ無いのですが、「デフォルト型引数」はあります。

デフォルト型引数の指定のある型引数が省略された場合は、デフォルト型が採用されます。

書き方は、ジェネリック型引数宣言部に<型引数 = 型>と書きます。もし、宣言部にトレイト境界を記述する場合は、<型引数:トレイト境界 = 型>と書きます。


構造体

struct MyStruct<T: Default = i64> {

val: T,
}

impl MyStruct {
fn my_method(&mut self) {
self.val = i64::default()
}
}


列挙型

enum MyEnum<T: Default = i64> {

Empty,
Value(T),
}

impl MyEnum {
fn my_method(&mut self) {
if let MyEnum::Value(ref mut x) = self {
*x = i64::default();
}
}
}


トレイト

デフォルト引数にはSelfも指定できます。

trait MyTrait<T = Self>

where
T: std::ops::Add,
{
fn my_method(self) -> T;
}

impl MyTrait for i64 {
fn my_method(self) -> i64 {
self + self
}
}

ちなみに、関数にはデフォルト型引数は指定できません。


型引数を使うデフォルト型引数

デフォルト型引数に型引数を使った型を指定することもできます。

trait MyTrait<T, U = Option<T>> {

fn my_method(self) -> U;
}

impl<T> MyTrait<T> for T {
fn my_method(self) -> Option<T> {
Some(self)
}
}


関連型

トレイトには、関連型(associated type)と呼ばれる、型引数と似たようなジェネリックスの指定方法があります。

trait MyTrait {

type MyType; // 関連型
fn my_method(self) -> Self::MyType;
}

impl<T> MyTrait for T {
type MyType = i64; // 実装時に関連型へ具体型を指定する
fn my_method(self) -> i64 {
0
}
}

これは、ジェネリック型引数と同じく、使用する側が具体型を決められるような場所のみを提供します。具体的な型は、トレイト実装時に指定します。

関連型を使用する場合は、関連関数と同様にトレイトの実装型::関連型(上記例ではSelf::MyType部分)と記述します。


関連型へのトレイト境界

関連型へも、ジェネリック型引数と同様に、トレイト境界を指定可能です。

以下の書き方が一般的なようです。

trait MyTrait {

// type 関連型: トレイト境界
type MyType: std::fmt::Debug + std::fmt::Display;
fn my_method(self) -> Self::MyType;
}

トレイト境界の書き方は、前述のトレイト境界の書き方と同様です。

また、where節に記述することも可能です。

trait MyTrait

where
Self::MyType: std::fmt::Debug + std::fmt::Display,
{
type MyType;
fn my_method(self) -> Self::MyType;
}

どちらの書き方でも、おそらく動作に違いはありません。(何か違いを知っている方は教えてください。)

違う点としては、where節では関連型を使った型にトレイト境界を指定できますが、関連型に直接記述する方法では今のところ指定できません。


NG

trait MyTrait {

type MyType;
type Option<MyType>: std::fmt::Debug; // これは「今のところ」NG
fn my_method(self) -> Self::MyType;
}


エラーメッセージ

generic associated types are unstable (see issue #44265) rustc(E0658)



OK

trait MyTrait

where
Option<Self::MyType>: std::fmt::Debug, // これはOK
{
type MyType;
fn my_method(self) -> Self::MyType;
}


ただし、(後述の内容を少し先取りする形になりますが)関連型のあるトレイトをトレイト境界として指定した場合、関連型へのトレイト境界は省略出来るのですが、下記のような「関連型を使った型」は、再度トレイト境界を明示しないといけません。

trait MyTrait

where
Self::MyType: std::fmt::Debug,
{
type MyType;
fn my_method(self) -> Self::MyType;
}

fn my_fn<T>(val: T)
where
T: MyTrait,
// T::MyType: std::fmt::Debug は省略できる
{
println!("{:?}", val.my_method());
}

trait MyTrait

where
Option<Self::MyType>: std::fmt::Debug,
{
type MyType;
fn my_method(self) -> Self::MyType;
}

fn my_fn<T>(val: T)
where
T: MyTrait,
Option<T::MyType>: std::fmt::Debug, //は省略できない
{
println!("{:?}", Some(val.my_method()));
}

また、ジェネリック関連型については、RFCとして公開されており、将来的に可能になるかもしれません。


関連型のメリット


型注釈を省略できる

ジェネリック型引数を使用した場合は、トレイトの実装型が決まっても、トレイトの型引数の型は未定なので、型(ジェネリック型)を明記しなければなりません。しかし、関連型は、トレイトの実装型が決まれば、自動的に関連型も定まるため、明記する必要はありません。特に関連型を具体型に限定しない状態での記述が楽になるかと思います。


ジェネリック型引数

trait MyTrait<T> {

fn my_method(&self) -> T;
}

// 型引数が2つ必要
fn my_fn<T, U>(val: T) -> U
where
T: MyTrait<U>,
{
val.my_method()
}



関連型

trait MyTrait {

type MyType;
fn my_method(&self) -> Self::MyType;
}

// 型引数が1つで済む
fn my_fn<T>(val: T) -> T::MyType
where
T: MyTrait,
{
val.my_method()
}



関連型へのトレイト境界を再度明示しなくてもよい

関連型へのトレイト境界も、使用時に再度記述する必要はありません。


ジェネリック型引数

trait MyTrait<T>

where
T: std::fmt::Debug,
{
fn my_method(&self) -> T;
}

fn my_fn<T, U>(val: T)
where
T: MyTrait<U>,
U: std::fmt::Debug, // ジェネリック型引数のトレイト境界は再度記述しなければならない
{
println!("{:?}", val.my_method());
}



関連型

trait MyTrait {

type MyType: std::fmt::Debug;
fn my_method(&self) -> Self::MyType;
}

fn my_fn<T>(val: T)
where
T: MyTrait, // 関連型のトレイト境界は再度記述しなくて良い
{
println!("{:?}", val.my_method());
}



関連型のデメリット


一つの型の実装に対して複数パターンの関連型は指定できない

ある特定の型に対して、ある特定のトレイトは一つしか定義できません。しかし、型引数の場合は、一つの型が決まってもジェネリック型は決定しないため、ジェネリック型引数の具体型がかぶらない限り、複数のジェネリック型への実装を定義できます。


ジェネリック型引数

trait MyTrait<T> {

fn my_method(&self) -> T;
}

// i32に対してMyTrait<T = i32>を実装
impl MyTrait<i32> for i32 {
fn my_method(&self) -> i32 {
self + 1
}
}

// i32に対してMyTrait<T = Option<u32>>を実装
impl MyTrait<Option<u32>> for i32 {
fn my_method(&self) -> Option<u32> {
match (*self as i64) + 1 {
a if a < 0 => Some(a as u32),
_ => None,
}
}
}


対して、関連型はメリットにも書いたとおり、実装する型ごとに関連型が一意に決まるため、複数のパターンは実装できません。


関連型

trait MyTrait {

type MyType;
fn my_method(&self) -> Self::MyType;
}

// i32に対してMyTrait<MyType = i32>を実装
impl MyTrait for i32 {
type MyType = i32;
fn my_method(&self) -> i32 {
self + 1
}
}

// i32に対する実装が競合するためエラー
impl MyTrait for i32 {
type MyType = Option<u32>;
fn my_method(&self) -> Option<u32> {
match (*self as i64) + 1 {
a if a < 0 => Some(a as u32),
_ => None,
}
}
}


この違いが、型引数と関連型の最も基本的な違いです。どちらを選ぶかは、「この型は実装型に対して必ず一意に決まるものか」を考えて判断するといいでしょう。


関連型にはデフォルト型は指定できない

ジェネリック型引数は、前述の通りデフォルトの型を決めることができますが、関連型には現在そのような機能はありません。

trait MyTrait {

type MyType = i32; // デフォルト型は定義できないため、エラー
fn my_method(&self) -> Self::MyType;
}


エラーメッセージ

associated type defaults are unstable (see issue #29661) rustc(E0658)


ただし、デフォルト関連型については、RFCとして公開されており、nightly版で#![feature(associated_type_defaults)]を指定すると上記の書き方はエラーにはならないようです(うまくは動かないですが)。前向きに検討されているようには見えます。


関連型をトレイト境界で使用する

関連型が標準ライブラリで使われている例として、演算子があります。

std::opsにあるAdd(加算)やSub(減算)等の演算子の多くにはOutputという関連型が定義されていて、演算結果の型として使われています。例えばAddの定義は以下のようになっています。


pub trait Add<RHS=Self> {

type Output;

#[must_use]
fn add(self, rhs: RHS) -> Self::Output;
}


各型は、『Add::Outputの型 = Addを実装している型 + RHSの型』のように使用されます。

このAddトレイトをトレイト境界に設定し、引数同士の加算が出来るようにしてみます。


関連型に型を指定する

関連型の型を指定するには、トレイト<関連型 = 型>と記述します。

fn my_fn<T>(x: T, y: T) -> T

where
T: std::ops::Add<Output = T>, // T = T + Tが可能
{
x + y
}

RHS=Self=Tなので、T = T + T、全てT型の演算になります。


関連型にトレイト境界を指定する

関連型にトレイト境界を指定するには関連型: トレイト境界と記述します。

このとき、「関連型が実装されているトレイトの型」(下記例ではT::)を指定しなければならない点には注意です。

fn my_fn<T>(x: T, y: T) -> T::Output

where
T: std::ops::Add, // T::Output = T + T
T::Output: std::fmt::Debug, // T::Output (T + T) はDebugを実装している
{
let a = x + y;
println!("{:?}", a);
a
}

AddTに実装されているので、その関連型OutputT::Outputと指定します。

先ほどと違い、Outputに特定の型としてTを指定していないので、T + TTではなくT::Outputのままとなります。

ちなみに、std::ops::Add::Outputと書いても文法的には正しいのですが、何に対する実装かわからないため、この場合はエラーとなります。後述しますが、T::Outputは省略せずに書くと<T as std::ops::Add>::Outputとなります。


トレイトの継承

トレイトは、他言語で「インターフェイス」と呼ばれるものに似ています。そして、大体の言語ではインターフェイスを他のインターフェイスに継承出来るように、トレイトも他のトレイトに継承っぽいことが可能です1。ただ、ちょっと使い勝手が違って「思てたんと違ーう!」となるかもしれません。

トレイトの継承の記述は、継承先のトレイトの定義部分でtrait SubTrait: SuperTraitと書きます。

trait MySuperTrait {

fn my_super_method(&self) -> i64;
}

// MySubTraitはMySuperTraitを継承する
trait MySubTrait: MySuperTrait {
fn my_sub_method(&self) -> u64;
}


サブトレイトの実装

上記のMySubTraitを実装の中に、MySuperTraitのメソッドmy_super_methodの実装をまとめることはできません。


NG

impl MySubTrait for i32 {

// エラー my_super_methodはMySubTraitのメソッドではない!
fn my_super_method(&self) -> i64 {
0i64
}

fn my_sub_method(&self) -> u64 {
0u64
}
}



エラーメッセージ

the trait bound `i32: MySuperTrait` is not satisfied

the trait `MySuperTrait` is not implemented for `i32` rustc(E0277)

method `my_super_method` is not a member of trait `MySubTrait`
not a member of trait `MySubTrait` rustc(E0407)


実際のところ、MySubTraitMySuperTraitのメソッドを持っていません。全く別のトレイトして扱います。なので、MySuperTraitで定義したメソッドはMySuperTraitの実装として記述しなければなりません。


OK

impl MySuperTrait for i32 {

// MySuperTraitのメソッドはMySuperTraitで定義する
fn my_super_method(&self) -> i64 {
0i64
}
}

impl MySubTrait for i32 {
// MySubTraitのメソッドはMySubTraitで定義する
fn my_sub_method(&self) -> u64 {
0u64
}
}



トレイトの継承=Selfへのトレイト境界

MySubTraitMySuperTraitのメソッドを持っていないことは、以下のようにトレイトを明確化することでも確認できます。(トレイトの明確化の詳細は後述します。


NG

fn my_fn<T>(val: T)

where
T: MySubTrait,
{
// エラー TをMySubTraitとして見た時に、my_super_methodは存在しない
let _a = <T as MySubTrait>::my_super_method(&val);
}


エラーメッセージ

cannot find method or associated constant `my_super_method` in trait `MySubTrait`

help: a method with a similar name exists: `my_sub_method` rustc(E0576)

ちなみに以下の記述では、どのやり方でもトレイトが推論によってMySuperTraitに決まるため、問題なく動きます。


OK

fn my_fn<T>(val: T)

where
T: MySubTrait,
{
let _a = val.my_super_method();
let _a = MySuperTrait::my_super_method(&val);
let _a = MySubTrait::my_super_method(&val);
let _a = T::my_super_method(&val);
}

このように、Rustにおける継承と呼ばれている機能は、サブトレイトを実装する型はスーパートレイトを実装していなければならないという制約を課すものと言えるでしょう。つまり、「Selfに対して、トレイト境界を設定する」ものと考えた方がしっくりきます。

実際、以下の記述でも同じ動きをします。

trait MySuperTrait {

fn my_super_method(&self) -> i64;
}

// where節で記述しても同じ
trait MySubTrait
where
Self: MySuperTrait,
{
fn my_sub_method(&self) -> u64;
}


Sizedトレイト

大抵の型はコンパイル時に既にサイズが決まっています。例えばi32は4バイト、boolは1バイトです。自分で定義したstructenumも一部例外を除いて、コンパイル時にサイズが決まっています。


サイズ不定型

ほぼ全ての型はコンパイル時にサイズが決まっていますが、サイズが決まっていない型も少しだけ存在します。以下の2つが代表的です。


  • スライス([T]str)

  • トレイトオブジェクト(dyn Trait)

[T]strは、要素数がわからないため、また、トレイトオブジェクトは実装型がわからないため、サイズが不定となります。


NG

// Tには自動的にSizedトレイトの制約(T: Sized)がつく

fn my_fn<T>(val: T) -> *const T {
&val
}

fn main() {
let a: &str = "aaa";
let b = my_fn(*a); // エラー TはSizedを要求している
}



サイズ不定では出来ないこと

サイズ不定の型は、出来ないことが多いため、そのままでは扱いにくいです。そのため、通常は参照やポインタ、Box等(全てSizedになる2)を通して扱います。


変数の型にはなれない

let _a: str; // NG

let _a: &str; // OK


関数の引数、戻り値の型にはなれない

// NG

fn my_fn_ng(val: dyn std::fmt::Debug) -> dyn std::fmt::Debug {
println!("{:?}", val);
val
}

// OK
fn my_fn_ok(val: Box<dyn std::fmt::Debug>) -> Box<dyn std::fmt::Debug> {
println!("{:?}", val);
val
}

(TDebugを実装している場合、Box<T>Debugを実装していることが保証されています)


列挙型の要素にはなれない

// NG

enum MyEnumNg {
A(i32),
B([i32]),
}

// OK
enum MyEnumOk<'a> {
A(i32),
B(&'a [i32]),
}


構造体やタプルの末尾を除く要素にはなれない

// NG

struct MyStructNg {
a: [i32],
b: i32,
}

// OK (この構造体はサイズ不定となる)
struct MyStructOk {
a: i32,
b: [i32],
}

ちなみに、OKと書いたMyStructOkですが、これは確かにコンパイルは通りますが、実際にまともに作成する手段が無いためほぼ使えません。使うとしたら、unsafeコードを大量に書いて、スライスのような処理を自作する感じでしょうか。普段はあまり気にしなくても良いでしょう。


Sizedトレイトは暗黙的に実装されている

Rustには、コンパイル時にサイズが決まっていることを表現するため、Sizedというトレイトが用意されています。Sizedトレイト自体には、メソッドなどの機能はありません。Sizedを実装している型はサイズが決定しているという「マーカー」の役割を持っています。


サイズが決まっている型には、自動的にSizedトレイトが実装される

サイズが決まっている型は全てSizedトレイトを実装しています。自分で作った型も、コンパイル時にサイズが決まっていれば、自動的にSizedトレイトが実装されます。これを意図的に外すことは不可能です。


ジェネリック型引数や関連型には、自動的にSizedトレイトの境界が設定される

ジェネリック型引数や関連型を定義した時、何も明示しなくてもSizedトレイトの境界が設定されます。

// T: Sized

// MyStruct<T>: Sized
#[derive(Debug)]
struct MyStruct<T>(T);

fn main() {
// MyStructはSizedを実装しているため、Tになれる
my_fn(&MyStruct(0i32));
}

// T: Sized
fn my_fn<T>(x: &T)
where
T: std::fmt::Debug,
{
println!("{:?}", x);
}


?Sized

型引数や関連型には、自動的にSizedの制約が付きますが、これではサイズ不定型を扱いたい場合に困ります。そのため、自動的に付くSizedの制約を取り除く方法が提供されています。トレイト境界に?Sizedと記述することで、サイズ不定型を扱えるようになります。


NG

fn main() {

// エラー T = str はサイズ不定型
my_fn("foo");
}

fn my_fn<T>(x: &T)
where
T: std::fmt::Debug,
{
println!("{:?}", x);
}



OK

fn main() {

my_fn("foo");
}

fn my_fn<T>(x: &T)
where
T: std::fmt::Debug + ?Sized, // Tにサイズ不定型を許可する
{
println!("{:?}", x);
}



トレイト宣言時のSelfSizedではない

例外として、トレイトのSelfには暗黙的にSizedトレイト境界は設定されません。そのため、str[i32]等のサイズ不定型への実装が可能となっています。

trait MyTrait {

fn my_method_ref(&self);
fn my_method_mut(&mut self);
}

// サイズ不定型への実装が可能
impl MyTrait for str {
fn my_method_ref(&self) {}
fn my_method_mut(&mut self) {}
}

また、Sizedとは限らないため、&self&mut selfは引数に設定できても、selfを引数にしたデフォルトメソッドは定義できません(Sizedかどうかは実装する型依存のため、メソッド定義自体は可能です)。

trait MyTraitNg {

fn my_method_ref(&self) {}
fn my_method_mut(&mut self) {}
fn my_method_own(self) {} // NG selfはsizedではないため、引数に設定できない
}

trait MyTraitOk {
fn my_method_ref(&self);
fn my_method_mut(&mut self);
fn my_method_own(self); // OK メソッドの実体を作らず、定義のみなら可能
}

Sizedトレイト境界を明示すれば、selfを引数にしたデフォルトメソッドも定義できます。

trait MyTrait: Sized {

fn my_method_ref(&self) {}
fn my_method_mut(&mut self) {}
fn my_method_own(self) {} // OK selfはsizedに限定されたため、引数に出来る
}


トレイト・型の明確化(フルパス記法)

Rustでは、型やトレイトを明示しなくても、推論によって自動的に決定してくれます。

しかし、推論によって一つに決まらない場合は、プログラマが型やトレイトを明示しなくてはなりません。


値・関数実行時の型明確化

値作成時や、関数実行時にジェネリック型を明確にしたい場合は、型or関数::<型引数の型>と記述します。

(この::<>のことをturbofishと呼ぶらしい)


構造体

struct MyStruct<T>(T);

fn main() {
// _aは全てMyStruct<i64>
let _a = MyStruct(0i64);
let _a = MyStruct::<i64>(0);
}


列挙型

enum MyEnum<T> {

Empty,
Value(T)
}

fn main() {
// _aは全てMyEnum<i64>
let _a = MyEnum::Value(1i64);
let _a = MyEnum::Value::<i64>(1);
let _a = MyEnum::Empty::<i64>;
}


関数

fn my_fn<T>(val: T) -> T {

val
}

fn main() {
// _aは全てOption<i64>
let _a = my_fn(Some(0i64));
let _a = my_fn(None::<i64>);
let _a = my_fn::<Option<i64>>(None);
}

この辺の情報は、The Rust Reference内のPathsに少なくとも載っているのですが、それ以外にどこに載っているのかわからないです。the bookのジェネリクス関係の説明にあって欲しいと思っています。


トレイトの明確化

ある型に実装されているトレイトを明確化する場合は、<型 as トレイト>::メソッドとかと記述します。(フルパス記法と言うそうです。)

この書き方はキャスト(v as Type)に似ており、意味もまた似ています。しかし、キャストは実行時に型を変換するのに対し、トレイトの明確化は何の処理も実行しません<Type as Trait>::method()は、Type::method()Trait::method()を省略せずに書いただけです。


明確化が必要になる具体例


名前競合を回避する

関連型をトレイト境界で使用する」で説明したAddトレイト(加算)を使った例に、Subトレイト(減算)のトレイト境界を追加してみます。Subトレイトは、Addトレイトと似たような定義となっていて、両方共にOutputという関連型を持っています。(API Document参照)


NG

// エラー T::OutputはAddトレイトのOutputか、SubトレイトのOutputかわからない

fn my_fn<T>(x: T, y: T) -> T::Output
where
T: Copy + std::ops::Add + std::ops::Sub,
T::Output: std::fmt::Debug, // エラー 上に同じ
{
println!("{:?}", x + y);
x - y
}

上記コードはコンパイルエラーとなります。何故かと言うとTAddトレイトとSubトレイトを実装しているため、T::Outputと書いた場合、どちらのOutputを指すか決めることができないためです。


エラーメッセージ

ambiguous associated type `Output` in bounds of `T`

ambiguous associated type `Output`
note: associated type `T` could derive from `std::ops::Sub`
note: associated type `T` could derive from `std::ops::Add` rustc(E0221)

この手のエラーは「ambiguous(あいまいな)」関連型などと言われるので、言われたらトレイトを明確にしましょう。上記コードでは、加算結果にDebugを使用し、減算結果を戻り値に使用しているため、以下のようにします。


OK

fn my_fn<T>(x: T, y: T) -> <T as std::ops::Sub>::Output

where
T: Copy + std::ops::Add + std::ops::Sub,
<T as std::ops::Add>::Output: std::fmt::Debug,
{
println!("{:?}", x + y);
x - y
}


循環を回避する

ここでは、ジェネリック型引数を持つトレイトをスーパートレイトに持つサブトレイトを定義してみます。その際、そのジェネリック型をサブトレイトの関連型としてみましょう。

特に意味はないですが、ジェネリック型引数を持つトレイトとしてBorrowを使ってみます。(詳細はAPI Document参照)


NG

// エラー 定義がループしている

trait MyTrait: std::borrow::Borrow<Self::MyType> {
type MyType;
}

上記コードはコンパイルエラーとなります。なぜでしょう。

エラーメッセージは以下のようになります。


エラーメッセージ

cycle detected when computing the supertraits of `MyTrait`

note: ...which again requires computing the supertraits of `MyTrait`, completing the cycle
note: cycle used when collecting item types in top-level module rustc(E0391)

MyTraitのスーパートレイト(Borrow)の計算時にループが検出されたと言っています。これはSelfの定義には、スーパートレイトのBorrowの定義が必要で、Borrowの定義にはSelfが必要で…とループしています。名前は競合していないので、大丈夫な気もしますが、エラーとなるものはしょうがないのです。

どうすればいいかというと、MyTypeMyTraitの関連型であることを明記し、Borrowを参照させなくします。


OK

trait MyTrait: std::borrow::Borrow<<Self as MyTrait>::MyType> {

type MyType;
}


その他推論がうまく動かない場合

ここまで、T::Outputのように、型::関連型のような記法を使ってきましたが、この型に記述して推論が動く型は限られているようです。型引数T::Self::等はトレイトを記述しなくても推論されますが、Enum<T>::T::Assoc::等はトレイトを明記しなければなりません。パスの表現が曖昧になるからでしょうか?

例えば、以下の例は戻り値でトレイトを明確化しなければなりません。


NG

fn my_fn<T>(x: Option<T>, y: Option<T>) -> Option<T>::Output

where
Option<T>: std::ops::Add,
{
x + y
}


OK

fn my_fn<T>(x: Option<T>, y: Option<T>) -> <Option<T> as std::ops::Add>::Output

where
Option<T>: std::ops::Add,
{
x + y
}

この辺は深く考えず、コンパイラに怒られたらフルパス記法を試してみる程度でいいかもしれません。


最後に

本当はもう少し書く予定でしたが、思っていた以上に記述量が多くなり、力尽きました。

嘘・大げさ・紛らわしい等の指摘や要望があれば、コメント等でお伝え下さい。


参考にしたページ


英語


日本語





  1. 古い"the book"では"継承"と呼ばれていますが、最新の"the book"では"継承"という表現は使われていないようです。 



  2. サイズ不定型へのポインタは、通常のポインタよりもサイズが大きくなります。これは、参照するアドレス以外のデータ(スライスの要素数等)を持つためです。 



  • 関連型
  • トレイトの継承
  • Sizedトレイト
  • トレイト・型の明確化(フルパス記法)
  • 最後に