トレイトについて理解する

rust

この記事で書くこと

  • トレイトとは
  • 引数としてトレイトを使う
  • 戻り値としてトレイトを使う

トレイト(trait)とは

traitは特性という意味の英語です。

rustにおいては、トレイトとは「メソッドを集めたもの」です。このトレイトを構造体に付与することで、構造体にその特性(メソッド)を与えることができます。他の言語でいうインターフェースに似ています。

トレイトを実装する

例えば以下のように、液体についてのトレイトを実装してみます。

trait Liquid {
    fn amount(&self) -> usize;
}

struct Oil {
    amount: usize,
}

impl Liquid for Oil {
    fn amount(&self) -> usize {
        self.amount
    }
}

fn main() {
    let oil = Oil { amount: 10 };
    println!("{}",oil.amount());
}

実行結果

10

コード解説

traitを作成

まずは液体の特性だということを表すために、trait Liquidを使います。その中に液体についての特性を書いていきます。例えば液体には「量」を持つという特性を持ちます。このことをamountメソッドとして書きます。

implで特性を構造体に与える

トレイトを構造体に対して実装するにはimplキーワードを使います。ここで、Liquidの特性であるamonutを具体的に実装します。これでOilにはLiquidの特性が実装されました。

デフォルト実装

先ほどの例では、Oil構造体でamountを実装しました。traitには、trait内でメソッドを実装できる、デフォルト実装ができます。デフォルト実装は以下のように書きます。

trait Liquid {
    fn amount(&self) -> usize;
    fn double_amount(&self) -> usize{
        self.amount() * 2
    }
}

struct Oil {
    amount: usize,
}

impl Liquid for Oil {
    fn amount(&self) -> usize {
        self.amount
    }
}

fn main() {
    let oil = Oil { amount: 10 };
    println!("{}",oil.amount());
    println!("{}",oil.double_amount());
} 

実行結果

10
20

先ほどの例に、double_amountメソッドを追加しました。しかし、Oil構造体でLiquidトレイトを実装する時にはdouble_amountメソッドは実装していません。しかしながら、double_amountはOil構造体で使うことができます。traitの実装をそのまま引き継ぐような実装をデフォルト実装と言います。

オーバーライド

デフォルト実装があっても、構造体に同じ名前のメソッドを実装できます。すると、構造体で実装したメソッドが使用されます。これをオーバーライドと言います。(実装が適当だけど許して〜)

trait Liquid {
    fn amount(&self) -> usize;
    fn double_amount(&self) -> usize{
        self.amount() * 2
    }
}

struct Oil {
    amount: usize,
}

impl Liquid for Oil {
    fn amount(&self) -> usize {
        self.amount
    }
    fn double_amount(&self) -> usize {
        self.amount * 3
    }
}

fn main() {
    let oil = Oil { amount: 10 };
    println!("{}",oil.amount());
    println!("{}",oil.double_amount());
}

実行結果

10
30

スーパートレイトの実装を前提として、トレイトを実装する

あるトレイトの実装を前提として、トレイトを実装することができます。前提とされているトレイトのことをスーパートレイトと言います。以下の例では、LiquidがDrinkのスーパートレイトになっています。つまり、DrinkはLiquidが実装されていることを前提とします。

trait Liquid {
    fn amount(&self) -> usize;
    fn double_amount(&self) -> usize {
        self.amount() * 2
    }
}

trait Drink : Liquid {
    fn drink(&self) {
        println!("drink {}ml!",self.double_amount());
    }
}

struct Water {
    amount:usize,
}

impl Liquid for Water{
    fn amount(&self) -> usize {
        self.amount
    }
}

impl Drink for Water {
}

fn main() {
    let water = Water{amount:100};
    println!("{}", water.amount());
    println!("{}", water.double_amount());
    water.drink();
}

実行結果

100
200
drink 200ml!

traitの関係を図にすると以下のようになります。

スーパートレイトが実装されていないとエラーになる

DrinkはLiquidが実装されていることを前提として実装されてるので、Liquidを実装していない構造体に実装しようとするエラーになります。例えば、先ほどの例で、WaterにLiquidを実装していない場合、コンパイルできません。

// Err
trait Liquid {
    fn amount(&self) -> usize;
    fn double_amount(&self) -> usize {
        self.amount() * 2
    }
}

trait Drink : Liquid {
    fn drink(&self) {
        println!("drink {}ml!",self.double_amount());
    }
}

struct Water {
    amount:usize,
}

impl Drink for Water {
}

fn main() {
    let water = Water{amount:100};
    println!("{}", water.amount());
    println!("{}", water.double_amount());
    water.drink();
}

引数としてトレイトを使う(トレイト境界)

引数としてトレイトを使うと、「このトレイトを実装した引数」と指定することができます。これをジェネリクスと組み合わせて使用します。ジェネリクスは任意の型を受け取る方法ですが、これに制限を加える形でトレイトを使用します。このような使い方をトレイト境界(trait bound)と言います。ちなみに、トレイト境界は指定し忘れてコンパイラから注意されることが多いので、trait boundという単語は覚えた方がいいです笑。

ジェネリクスについては、別に記事を書いていますのでよろしければご覧ください。

トレイト境界には、書き方が3パターンあります。

パターン1

fn all_drink_for_you<T:Drink>(d: &T) {
println!("{}ml drink for you!", d.amount());
}

スタンダードな書き方です。ジェネリクスの書き方と同じです。ジェネリクスに制限をかけることを表すために「T : Drink」と書きます。

パターン2

fn all_drink_for_you<T>(d: &T)
where
    T: Drink,
{
    println!("{}ml drink for you!", d.amount());
}

whereを使って、トレイト境界を書くこともできます。この書き方はトレイト境界が長くなる際に使われます。トレイト境界が複数必要な場合、「T : Drink + Hoge + Fuga」のように+を使って複数指定します。この場合、関数が長く見辛くなってしまうので、このような書き方をします。

パターン3

fn all_drink_for_you(d: &impl Drink) {
println!("{}ml drink for you!", d.amount());
}

パターン1のシンタックスシュガーとして用意されています。ジェネリクスを明示的に書く必要がないのでシンプルにかけます。

戻り値としてトレイトを使う

あるトレイトを実装した型を戻り値とすることができます。ここではsomeliquid関数からLiquidを実装した型を返しています。

trait Liquid {
    fn amount(&self) -> usize;
    fn double_amount(&self) -> usize {
        self.amount() * 2
    }
}

trait Drink: Liquid {
    fn drink(&self) {
        println!("drink {}ml!", self.double_amount());
    }
}

struct Water {
    amount: usize,
}

impl Liquid for Water {
    fn amount(&self) -> usize {
        self.amount
    }
}

impl Drink for Water {}

fn some_liquid() -> impl Liquid{
    Water { amount: 10 }
}
fn main() {
    let liquid = some_liquid();
    println!("{}",liquid.amount());
}

ここで、ちょっと注意が必要なことがあります。main関数内で、waterのdrinkメソッドを実行しようとすると、コンパイルエラーになります。返している型はあくまでLiquid型なので、some_liquid関数でWaterを返していても、使えるメソッドはLiquidのメソッドのみです。

fn main() {
    let liquid = some_liquid();
    println!("{}",liquid.amount());
    liquid.drink(); // <- Err
}

また、実行時に型が決まらない場合にも注意が必要です。以下はどちらもLiquidを実装していますが、コンパイルできません。コンパイル時には、「Liquidを実装しているどの型か」がわかっている必要があります。

fn some_liquid(var: &str) -> impl Liquid {
    if var == "water" {
        Water { amount: 10 }
    } else {
        Oil { amount: 10 }
    }
}

このコードを通したい場合には、Boxとトレイトオブジェクトを使用する必要があります。

fn some_liquid(var: &str) -> Box<dyn Liquid> {
    if var == "water" {
        Box::new(Water { amount: 10 })
    } else {
        Box::new(Oil { amount: 10 })
    }
}

最後に

トレイトについては、以上になります。ざっくり書くと以下のようなことを書きました。

  • トレイトに特性(メソッド)を集める。
  • トレイトに特性を追加する(スーパートレイトから引き継ぎ)。
  • ジェネリクスに対して、トレイトを使って制限を設ける。
  • トレイトを実装した型を戻り値とする

今回の記事では、ブランケット実装について書ききれなかったので別の記事で書こうと思います。

コメント

タイトルとURLをコピーしました