🦀

100日後にRustをちょっと知ってる人になる: [Day 33]ジェネリクス

2022/09/26に公開

Day 33 のテーマ

[Day 32](https://zenn.dev/shinyay/articles/hello-rust-day032] ではクロージャの使い方についていろいろと見てみました。
その中でクロージャを引数として受けとるような関数を考えた場合、次のようにします。

fn apply<F>(f: F) where F: Fn() {
    f();
}

つまり、クロージャが定義されたときに生成される無名の構造体 (Fn, FnMut, FnOnce のいずれかのトレイトを介して関数としての機能が実装されたもの) が型が未指定のため、関数を実行するために ジェネリクスが必要となります。

このように汎用的に型や関数を使えるようにするためにの機能がジェネリクスです。

ジェネリクス(総称型)

最近ではどのプログラム言語を使ってプログラムをしていても、ジェネリクス総称型、またジェネリックプログラミングといったキーワードを耳にすると思います。Wikipia ではどのように説明されているか見てみようと思います。

ジェネリック(総称あるいは汎用)プログラミングは、具体的なデータ型に直接依存しない、抽象的かつ汎用的なコード記述を可能にするコンピュータプログラミング手法である。

ジェネリックプログラミングの特徴は、型を抽象化してコードの再利用性を向上させつつ、静的型付け言語の持つ型安全性を維持できることである。

つまり、型を抽象化して再利用性を高める仕組みといえます。
Rust や Java のようにコンパイル時に型が決定する静的型付け言語では、関数やメソッドを定義する際に受け取る引数データ型を予め決めて記述しておく必要があります。
そこで、型を抽象化した任意の型を用いることにより、引数の型の情報自体を引数として渡すなどの方法により、特定のデータ型に予め決定せずに処理内容を記述することができるようになります。
この仕組みのことをジェネリクスといい、任意の型のことをジェネリック型といいます。

Rust でのジェネリクス

関数、構造体、列挙型でジェネリック型を使うには、それぞれの名前のうしろにダイアモンド演算子 <> を使いジェネリック型の名前を指定します。
慣例的には、名前に T を使うことが多いです。

  • 関数
fn foo<T>(a: T, b:T) -> T {
    a+b
}
  • 列挙型
enum Result<T,E> {
    Ok(T),
    Err(E),
}
  • 構造体
struct Point<T> {
    x: T,
    y: T
}
  • メソッド
struct Point<T> { x: T, y: T }

impl<T> Point<T> {
    fn do_something(self) -> (T, T) {
        (self.x, self.y)
    }
}

トレイト境界とジェネリクス

データ型を分類するための仕組みでトレイトというものがありました。トレイトを使うことで、共通の振る舞いを抽象化して定義することができました。このトレイトには、トレイト境界という考え方があります。
トレイト境界とは、ジェネリック型に対して 「このトレイトを実装していなければならない」 という制約を課すものです。

ジェネリック型にトレイト境界を指定することで,その型が特定のトレイトのインスタンスであることを制約します。
トレイト境界は、Type: Trait と記述します。

  • 型引数宣言部に記述
fn draw<T: Geometry>(geometry: &T) {
    ...
}
  • where節に記述
fn draw<T>(geometry: &T)
    where T: Display {
    ...
}

Day 33 のまとめ

今日はジェネリクスの使い方について見てみました。同一の処理を型ごとに複数定義するのではなく、型定義を抽象化して再利用性を高める記述であるジェネリクスはとても便利です。

以下は Rust 以外の言語でのジェネリクスについて紹介されいてるドキュメントです。考え方・使い方は当然ながらほぼ同じなので別の言語を使っていた人は比較してみると分かりやすいかもしれないですね。

GitHubで編集を提案

Discussion