🦀

100日後にRustをちょっと知ってる人になる: [Day 85]書籍: Rust プログラミング完全ガイド その9

2022/12/12に公開

Day 85 のテーマ

Day 84 までに Rust の書籍の Rustプログラミング完全ガイド の 1 章から 18 章までを読み終わりました。

第19章 トレイトを使う

この章での内容:

  • トレイトを使えば、ジェネリック関数の呼び出しに関するコンパイラのエラーメッセージが読みやすくなる>こと
  • ジェネリックパラメータの制約が 1 つにまとまったり、複数のトレイトに分かれたりする理由
  • ジェネリックパラメータに制約がないと、できることがどれほど少ないか
  • トレイトに入れた関数のスコープ
  • 複数のメソッドを含むトレイトを作る方法
  • 複数のトレイトに渡すジェネリックパラメータを制約する方法
  • トレイト継承を使う方法
  • トレイトを使って外部型にメソッドを追加する方法
  • 標準トレイトの DisplayDebug を実装する方法
  • 便利なジェネリックトレイトを宣言する方法
  • 型指定を持たないトレイトをシンプルに使える関連型
  • イテレータの実装

トレイトの概要についてメモ

Day 17 などの振り返りになりますが、トレイトとはどういうものなのか、大事なことなので改めて自分のことばで表してみます。

他の言語仕様との比較をしながら、いろいろと振り返りを行っているのでトレイトについても Java ではどういうことかを考えてみます。
まず、トレイトの定義ですが、様々な型に対して抽象化した任意の振る舞い (関数) を持たせることができる言語仕様でした。つまり、トレイトとして意味のあるまとまりとして振る舞い (関数) のシグネチャを定義します。この時点では振る舞いの実体はなくてよくて、抽象化したシグネチャとして定義できていればよいです。この抽象化した振る舞いが定義されたトレイトを、何らかの型 (構造体など) に対して関連付け (実装) を行って使用します。

つまり、トレイトとは Java で言うところの、インターフェース抽象クラスのような役割をするものになります。

本に書かれている例として、平行根をもとめるメソッドを浮動小数点型 f32f64 に持たせることを行っていました。まずトレイトとしては次のように定義をします。

trait HasSqareRoot {
    fn sqrt(self) -> Self;
}

その実装としてそれぞれ次のようにしています。

impl HasSquareRoot for f32 {
    fn sq_root(self) -> Self { self.sqrt() }
}

impl HasSquareRoot for f64 {
    fn sq_root(self) -> Self { self.sqrt() }
}

他の例でも考えてみます。例えば、よくある演算として面積を求めるものを考えてみます。三角形と四角形、そして台形では面積の求め方がことなります。しかし、"面積を求める"という振る舞い自体は共通です。振る舞いが同じで挙動が異なるということになります。そこで、まず面積を求めるという振る舞いをするメソッドをトレイトに定義します。

trait Area {
    fn calc(&self) -> f64;
}

そして面積を求める対象の形に関する構造体を定義します。

struct Triangle {
    base: f64,
    height: f64,
}

struct Rectangle {
    base: f64,
    height: f64,
}

struct Trapezoid {
    top_base: f64,
    bottom_base: f64,
    height: f64,
}

この形に対して面積を求めるトレイトを実装します。

impl Area for Triangle {
    fn calc(&self) -> f64 {
        (self.base * self.height) / 2.0
    }
}

impl Area for Rectangle {
    fn calc(&self) -> f64 {
        self.base * self.height
    }
}

impl Area for Trapezoid {
    fn calc(&self) -> f64 {
        (self.top_base + self.bottom_base) * self.height / 2.0
    }
}
let triangle = Triangle {base: 10.0, height: 20.0};
let rectangle = Rectangle {base: 10.0, height: 20.0};
let trapezoid = Trapezoid {top_base: 10.0, bottom_base: 20.0, height: 10.0};

println!("三角形: {}", triangle.calc);
println!("四角形: {}", rectangle.calc);
println!("台形: {}", trapezoid.calc);

Day 85 のまとめ

この書籍の中でトレイトがこの後半になって出てくるのが少し意外でした。トレイトをつかって様々な振る舞いを複数の型に対して定義をしていくことが可能になってくるというものなので、もう少し前半に出ていても良かったのかなと思いました。ですが、次の章でオブジェクト指向について考えることになるので、オブジェクト指向の前にトレイトを見て、オブジェクト指向言語ではない Rust に存在しているオブジェクト指向のような捉え方ということの認識が深まればよいのかな、と改めて思いました。そんな考えも踏まえて、改めて公式ドキュメントのトレイトに関して共有する振る舞いということについて考えてみると、いろいろと理解が深まりました。

以下の内容が今日復習したことです。

  • 第19章 トレイトを使う
    • トレイトを使えば、ジェネリック関数の呼び出しに関するコンパイラのエラーメッセージが読みやすくなる>こと
    • ジェネリックパラメータの制約が 1 つにまとまったり、複数のトレイトに分かれたりする理由
    • ジェネリックパラメータに制約がないと、できることがどれほど少ないか
    • トレイトに入れた関数のスコープ
    • 複数のメソッドを含むトレイトを作る方法
    • 複数のトレイトに渡すジェネリックパラメータを制約する方法
    • トレイト継承を使う方法
    • トレイトを使って外部型にメソッドを追加する方法
    • 標準トレイトの DisplayDebug を実装する方法
    • 便利なジェネリックトレイトを宣言する方法
    • 型指定を持たないトレイトをシンプルに使える関連型
    • イテレータの実装
GitHubで編集を提案

Discussion