LoginSignup
3
2

More than 5 years have passed since last update.

[RustDoc翻訳] 5.20. Traits

Last updated at Posted at 2015-08-23

構造体からRustの特徴であるトレイトベースのジェネリクスまでの流れを翻訳します。
前: 5.19. Generics 次: 5.36. Raw Pointers
英語できないので誤訳や改善案等、見つけた場合はご指摘の程宜しくお願いします。


トレイト

あなたはmethod syntaxで関数を呼び出すために用いたimplキーワードを覚えていますか?

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

トレイトはそれに似ていますが、定義するときにはシグネチャだけを記述し、その後構造体毎に実装します。

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

trait HasArea {
    fn area(&self) -> f64;
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

このように、traitsの記述はimplとかなり似ているように見えますが、関数の実装は記述せず、シグネチャだけを定義します。トレイトを実装するときは、ただimpl 実装の対象とするのではなく、impl トレイト for 実装の対象とします。

ジェネリクスに制約を持たせるためにトレイトを使用できます。以下の関数について考えてみましょう。

fn print_area<T>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

これはコンパイルできませんが、このようなエラーが発生するはずです。

error: type `T` does not implement any method in scope named `area`

Tはありとあらゆる型であるため、Rustは.area()メソッドがTについて実装されているか確信を持てません。しかしジェネリック型Tに対して'トレイトによる制約'を加えることで、.area()メソッドが実装されていることを保証できます。

trait HasArea {
    fn area(&self) -> f64;
}
fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

<T: HasArea>構文はHasAreaトレイトを実装するあらゆる型を意味します。トレイトは関数のシグネチャを定義しているため、HasAreaを実装するあらゆる型が.area()メソッドを持っていることを確認できます。
上記のコードを拡張した例がこちらです。

trait HasArea {
    fn area(&self) -> f64;
}

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct Square {
    x: f64,
    y: f64,
    side: f64,
}

impl HasArea for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

fn main() {
    let c = Circle {
        x: 0.0f64,
        y: 0.0f64,
        radius: 1.0f64,
    };

    let s = Square {
        x: 0.0f64,
        y: 0.0f64,
        side: 1.0f64,
    };

    print_area(c);
    print_area(s);
}

このプログラムの出力は、

This shape has an area of 3.141593
This shape has an area of 1

上記の通り、print_areaはジェネリックですが、正しい型が渡されることを保証します。もし不正な型が渡されるとすると、

print_area(5);

コンパイルエラーになります。

error: failed to find an implementation of trait main::HasArea for int

これまでは構造体にのみトレイトの実装を追加していましたが、あなたはあらゆる型に対してトレイトを実装することができます。技術的には、i32のためのHasAreaを実装することも可能です。

trait HasArea {
    fn area(&self) -> f64;
}

impl HasArea for i32 {
    fn area(&self) -> f64 {
        println!("this is silly");

        *self as f64
    }
}

5.area();

しかし例え可能であったとしても、プリミティブ型が持つメソッドを実装する方法としては望ましくないと考えられています。

ここまでくると何でもありな様に見えますが、手が負えなくなるのを防ぐためにトレイトの実装周りには2つの制限が設けられています。第1に、あなたのスコープ内で定義されていないトレイトは適用されません。例えば、標準ライブラリはFileにI/O機能を追加するためのwriteトレイトを提供します。デフォルトでは、Fileはそのメソッドを持っていません。

let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");
let result = f.write("whatever".as_bytes());
result.unwrap(); // ignore the error

エラーは以下のとおりです。

error: type `std::fs::File` does not implement any method in scope named `write`

let result = f.write(b"whatever");
               ^~~~~~~~~~~~~~~~~~

Writeトレイトのために、初めにuseが必要となります。

use std::io::Write;

let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");
let result = f.write("whatever".as_bytes());
result.unwrap(); // ignore the error

これはエラー無しでコンパイルされます。

これは、例え誰かがintへメソッドを追加するような望ましくない何かを行ったとしても、あなたがトレイトのuseを行わない限り、影響はないことを意味します。

トレイトの実装についての制限はもう1つあります。トレイト、またはあなたが書いているimplの対象となる型は、あなた自身によって実装されなければなりません。HasAreaは私たちが記述したコードであるため、i32型のためのHasAreaを実装することができます。しかし、i32のためにRustによって提供されているToStringトレイトを実装したいとしても、トレイトと型が共に私たちの記述したコードでないため、それはできません。

最後に1つ。トレイトによって束縛されたジェネリック関数はmonomorphization(mono:単一の、morph:様相)されるため、静的ディスパッチが行われます。一体どういう意味でしょうか?詳細については、トレイトオブジェクトの章をチェックしてください。

複数のトレイト束縛

ここまで、トレイトによってジェネリックな型パラメータが束縛できることを見てきました。

fn foo<T: Clone>(x: T) {
    x.clone();
}

もし2つ以上の束縛が必要なのであれば、+を使うことができます。

use std::fmt::Debug;

fn foo<T: Clone + Debug>(x: T) {
    x.clone();
    println!("{:?}", x);
}

Tは今CloneDebug、両方の実装が必要です。

Where 節

ジェネリック型とトレイト束縛が少ないうちは良いのですが、数が増えるといよいよこの構文では不便になってきます。

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

関数名は左端にあり、引数リストは右端にあります。トレイト束縛を記述する部分が邪魔になっているのです。

そこでRustは'where節'と呼ばれる解決策を用意しています。

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

fn bar<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

fn main() {
    foo("Hello", "world");
    bar("Hello", "workd");
}

foo()は先程見せたままの構文で、bar()where節を用いています。あなたは型パラメータの定義時ではなく、引数リストの後ろにwhereを追加することでトレイト束縛を記述できます。長いリストであれば、空白を加えることもできます。

use std::fmt::Debug;

fn bar<T, K>(x: T, y: K)
    where T: Clone,
          K: Clone + Debug {

    x.clone();
    y.clone();
    println!("{:?}", y);
}

この柔軟な構文により、複雑な状況であっても可読性を保つことができます。
また、whereは基本的な構文よりも強力です。例えば、
(訳注: std::convert::From)

trait ConvertTo<Output> {
    fn convert(&self) -> Output;
}

impl ConvertTo<i64> for i32 {
    fn convert(&self) -> i64 { *self as i64 }
}

// T == i32の時呼び出せる
fn normal<T: ConvertTo<i64>>(x: &T) -> i64 {
    x.convert()
}

// T == i64のとき呼び出せる
fn inverse<T>() -> T
        // これは"ConvertFrom<i32>"であるかのようにConvertToを用いている
        where i32: ConvertTo<T> {
    42.convert()
}

ここではwhere節の追加機能を披露しています。この節は型パラメータではない任意の型1に対してトレイト束縛が可能です。

デフォルトメソッド

私たちがカバーしておくべきトレイトの最後の機能がデフォルトメソッドです。これの例を示すのは最も簡単です。

trait Foo {
    fn bar(&self);

    fn baz(&self) { println!("We called baz."); }
}

Fooトレイトの実装者はbar()を実装する必要がありますが、baz()を実装する必要はありません。baz()にはデフォルトの動作が与えられているからです。実装者の選択次第ではこのデフォルトの動作をオーバーライドすることも可能です。

trait Foo {
    fn bar(&self);
    fn baz(&self) { println!("We called baz."); }
}
struct UseDefault;

impl Foo for UseDefault {
    fn bar(&self) { println!("We called bar."); }
}

struct OverrideDefault;

impl Foo for OverrideDefault {
    fn bar(&self) { println!("We called bar."); }

    fn baz(&self) { println!("Override baz!"); }
}

let default = UseDefault;
default.baz(); // prints "We called baz."

let over = OverrideDefault;
over.baz(); // prints "Override baz!"

継承

時々、トレイトを実装するのに他のトレイトの実装が必要なこともあります。

trait Foo {
    fn foo(&self);
}

trait FooBar : Foo {
    fn foobar(&self);
}

FooBarの実装者はFooもまた実装しなければなりません。以下のようになります。

trait Foo {
    fn foo(&self);
}
trait FooBar : Foo {
    fn foobar(&self);
}
struct Baz;

impl Foo for Baz {
    fn foo(&self) { println!("foo"); }
}

impl FooBar for Baz {
    fn foobar(&self) { println!("foobar"); }
}

もしFooの実装を忘れると、Rustは以下のように知らせます。

error: the trait `main::Foo` is not implemented for the type `main::Baz` [E0277]


  1. 訳注: Tではなく、今回はi32のような型 

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2