16
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rustの「ジェネリックなデータ型」を再履修(ブログ邦訳)

Last updated at Posted at 2022-09-03

概要

以下のdev.to記事の日本語訳です。
https://dev.to/talzvon/rust-generic-types-in-method-definitions-4iah
内容自体はRust公式チュートリアル10.1章に包含されますが、入門者が読み過ごしやすいポイントをより丁寧に説明しています。訳者による大幅な抜粋・追記・変更あり。

対象

導入

もしあなたがRustの初心者で、Rustの公式チュートリアルを読んでいるのなら(もちろん読むべきです)、私のように10.1章: ジェネリックなデータ型の章で混乱した経験があるかもしれません (例: impl<T> Struct<T> {}ってなんで<T>を2回書くんだっけ?)。その混乱を解消するために、この記事を書きました。すでに公式チュートリアルをその部分まで読んで、ジェネリックなデータ型についておぼろげには理解をしていることが前提になります。この記事では型定義とメソッド定義におけるジェネリック型の使い方について、初心者が犯しがちな間違いに注意しながら、順番にみていくことにしましょう。

最もシンプルな例

まずは一番簡単な例から。チュートリアルで紹介されているとおり、structキーワードを使って、ユーザーが自前の型(Type)を定義することができます。それにメソッドを追加したい場合、次のようにしますよね。

// 型を宣言する
struct Container {
    field: i32,
}

// 型にメソッドを実装する
impl Container {
    fn foo(&self) {
        // 何らかの操作
    }
}

implキーワードを使って、型にメソッドを追加しています。

Rustにおいて、structキーワードを使って定義される「構造体(struct)」は「型(type)」の1種です(「構造体であれば型である」は正しく、「型であれば構造体である」は誤り)。たとえばスカラー型(i32, bool, etc...)や複合型(タプル・配列)・列挙型などは構造体ではない型の代表例です。すべての型について、implを用いたメソッドの実装が可能です。よって本稿では構造体を例示しつつ、「型」という表現を主に使って説明を進めます

シンプルなジェネリック型の使用例

一方、構造体にジェネリックな型が含まれている場合、implを行う際にも型を指定しなければなりません。下の例で、[TYPE]とは、implブロックによってメソッドを実装される(具体的な)型を指しています。

struct Container<T> {
    field: T,
}

impl Container<[TYPE]> {

}

例えば、次のような場合ですね。

struct Container<T> {
    field: T,
}

impl Container<i32> {
    fn foo(&self) {
        println!("これはContainer::<i32>::foo()です!");
    }
}

これの意味するところは、「Tがどんな型であったとしてもContainerのインスタンスを作ることができるが、Tとしてi32型を使ったインスタンスだけがfooメソッドを持つことができる」ということになります。実際にインスタンスを作成してみると、このことが確認できます。

let inst1 = Container { field: 'c' };
let inst2 = Container { field: 10 };
let inst3 = Container { field: 12.2 };

inst1.foo(); // コンパイルエラー:  - method doesn't exist
inst2.foo(); // 動作OK
inst3.foo(); // コンパイルエラー:  - method doesn't exist

これら3つのインスタンスはすべてContainer<T>型ですが、インスタンス生成時にジェネリック型としてi32を渡したinst2のみがfooメソッドを持ちます。

これはある種のパターンマッチと言っても良いでしょう。インスタンス生成時に渡される具体的な型(matcher)と、impl内で定義されたメソッドの型情報(pattern)とを照らし合わせて、matcherとpatternが適合した時のみインスタンスにメソッドが付与される訳です。

独自型を使ったシンプルな例

先ほどはプリミティブ型であるi32を使いましたが、structenum等によって自分で定義した型を使っても同じことです。以下のコードはi32を独自型Colorに変更した以外は、先ほどのコードと全く同じものです。

struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

struct Container<T> {
    filed: T,
}

impl Container<Color> {
    fn foo(&self) {
        println!("これはContainer::<Color>::foo()です!");
    }
}

fn main() {
    let inst1 = Container {
        filed: Color { red: 10, green: 20, blue: 30 },
    };
    let inst2 = Container { field: 10 };
    let inst3 = Container { field: 12.2 };

    inst1.foo(); // 動作OK
    inst2.foo(); // コンパイルエラー:  - method doesn't exist
    inst3.foo(); // コンパイルエラー:  - method doesn't exist
}

「インスタンス生成時にジェネリック型としてColorを渡したインスタンスのみがfooメソッドを持つ」。さっきと一緒ですね。

よくある入門者のミス

さて、ここまで特定の型にマッチした時のみメソッドを実装したい場合を見てきました。では、インスタンス生成時にTとしてどのような型を与えた場合でもメソッドを付与させたい時はどうすればよいのでしょうか。最初に思いつくのは、次のようなコードかもしれません:

struct Container<T> {
    field: T,
}

impl Container<T> {
    fn foo(&self) {
        println!("これはContainer::<ANYTYPE>::foo()です??");
    }
}

残念ながら、これは期待どおりには動きません。何がおかしいのでしょうか? Color型を使った例と比較して、コンパイラがどう解釈するのかを忠実に再現してみてください: 「インスタンス生成時にジェネリック型としてTを渡したインスタンスのみがfooメソッドを持つ」

つまり、このコードは 「Tという名前の(具体的な)型」 に対してfooメソッドを付与しようとするのです。そんな名前の型はこのコードのどこにも定義されていないので、「Tなんて名前の型は見当たらないよ」というメッセージとともにコンパイルエラーという事になります。「私はContainer<T>の部分でTをジェネリック型引数として定義したよ!」と言うかもしれませんが、その定義のスコープはstructブロックの中だけです。implブロックからすれば知る由もありません。

要はここまでの情報では片手落ちだったのです。まだ型定義の際(structブロック)のジェネリクスの使い方を学んだだけでした。我々は新たにメソッド定義の際(implブロック)のジェネリクスの使い方を学ぶ必要があります。

全ての型にマッチするimplブロック

じゃあどうすればあなたはimplブロックがすべての型に対してマッチすることをコンパイラに伝えられるでしょうか?
答え: implブロックの中でもジェネリック型を定義してやれば良いのです。

struct Container<T> {
    field: T,
}

impl<T> Container<T> {
    fn foo(&self) {
        println!("これはContainer::<ANYTYPE>::foo()です!");
    }
}

ここでは、impl<T>でまずジェネリック型Tを定義して、それからContainer<T>とすることでインスタンスがどんな型をTとして持った場合でもマッチするように指定しました。トレイト境界(公式本10.2章で学びます)による制約なしで、あらゆる型です。結果として、インスタンス生成時にTとしてどのような型を与えた場合でもメソッドfooが付与されることになります。

これで、なぜimpl行の中で<T>を2回書く必要があるのか、お分かり頂けたかと思います。1回はジェネリック型引数を定義するためもう1回はそれを使ってpatternを構築するためだったという訳です。

結局、何が混乱の元だったのでしょうか?それはおそらく、implではジェネリック型引数の宣言impl<T>)とジェネリック型を用いたpatternの構築Cotainer<T>)が分離していたのに対し、structでは型引数宣言pattern構築を同時に行っている(struct Container<T>)という違いからでしょう。

ではなぜstruct<T> Container<T>のような書き方はしないのか?答えは「意味がないから」になるでしょう。struct Container<T><T>の部分にi32のような具象型が来る可能性はありません。よって、この部分はその型で使用するジェネリック型引数を、使用する数だけ並べる以外にやりようがなく、すなわちそれはジェネリック型引数を宣言することと同義だからです。ですから、struct Container<T>と書かれた時点で<T>がジェネリック型であることは自明であり、わざわざstruct<T> Container<T>のように別途Tがジェネリック型引数であることを宣言する必要は無い、冗長である、であれば省略してしまえ、という訳です。

画像1.png

(おまけ)ジェネリクスを書く時のルール

implの振る舞いに慣れるために、ジェネリック型を扱う時に覚えておくて良いいくつかのルールを紹介しましょう(訳注: 正確に言えばルールではなく、ルールから導かれるいくつかの重要な特徴)。

ルール1:

  • impl定義内のmatcherの数は、型定義内のジェネリック型の引数の数と一致しなければならない

例えば下の例はもちろん上手く行きます。

struct Container<T, V> { }
impl Container<i32, i32> { } 

structの定義には2つのジェネリック型引数T, Vがあるので、implブロックのmatcherの数は絶対に「2」でなければなりません。つまり下の例はダメです。

struct Container<T, V> { }
impl Container<i32> { }

次の例もダメです。

struct Container<T, V> { }
impl Container { }

ルール2

  • impl定義内のジェネリック型引数の数は、型定義内のジェネリック型の引数の数と一致しなくても良い

これはうまくいきます(型定義では2つのジェネリック型引数、impl定義では0)。

struct Container<T, V> { }
impl Container<i32, i32> { }

下の例も同じくOKです(型定義では2つのジェネリック型引数、impl定義では1)。

struct Container<T, V> { } 
impl<T> Container<T, i32> { }

下の例も当然OKです(型定義では2つのジェネリック型引数、impl定義では2)。

struct Container<T, V> { }
impl<T, V> Container<T, V> { } 

ルール3

  • impl定義内のジェネリック型引数の名前は、型定義内のジェネリック型の名前と一致しなくても良い

下の例は問題なく動作し、これまでの例と全く同じ振る舞いをします。

struct Container<T, V> { }
impl<A, B> Container<A, B> { }

型同士の関係性に対するマッチング

型定義内に複数のジェネリック型がある場合、それらに対して「具体的な型」をマッチングさせるだけでなく、それらジェネリック型の「関係性」に対してマッチングを行うことも可能です。

struct Container<T, V> { }
impl<X> Container<X, X> { }

上の例では、Xという一つのジェネリック型をimpl<X>で定義し、そしてContainer<X, X>とすることによって、ジェネリック型が両方とも同じ型であることを要求しています。どんな型でも良いから、とにかくTVが同じ型である場合のみ、このimplブロックが有効化される、ということです。くどいようですが、これはTVが異なる型である場合にインスタンスを「作れない」という意味ではありません。インスタンスは作れます。ただ、そのインスタンスに対してこのimplブロックは有効ではない、というだけです。

参考サイト

16
7
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
16
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?