Rustにおけるジェネリクスの理解:完全ガイド
Wenhao Wang
Dev Intern · Leapcell

プログラミングにおける一般的な要件は、同じ関数を使用して異なる型のデータを処理することです。ジェネリクスをサポートしないプログラミング言語では、通常、データ型ごとに個別の関数を作成する必要があります。ジェネリクスの存在は、開発者に利便性を提供し、コードの冗長性を減らし、言語の表現力を大幅に豊かにします。これにより、同じタスクを実行するが異なるデータ型で動作する多くの関数を、1つの関数で置き換えることができます。
たとえば、ジェネリクスを使用しない場合、パラメータの型が u8
、i8
、u16
、i16
、u32
、i32
などである double
関数を定義すると、次のようになります。
fn double_u8(i: u8) -> u8 { i + i } fn double_i8(i: i8) -> i8 { i + i } fn double_u16(i: u16) -> u16 { i + i } fn double_i16(i: i16) -> i16 { i + i } fn double_u32(i: u32) -> u32 { i + i } fn double_i32(i: i32) -> i32 { i + i } fn double_u64(i: u64) -> u64 { i + i } fn double_i64(i: i64) -> i64 { i + i } fn main(){ println!("{}", double_u8(3_u8)); println!("{}", double_i16(3_i16)); }
上記の double
関数は、ロジックは同一ですが、データ型のみが異なります。
ジェネリクスを使用すると、型が異なることによるコードの重複という問題を解決できます。ジェネリクスを使用する場合:
use std::ops::Add; fn double<T>(i: T) -> T where T: Add<Output=T> + Clone + Copy { i + i } fn main(){ println!("{}", double(3_i16)); println!("{}", double(3_i32)); }
上記の文字 T
はジェネリックであり(x
のような変数の意味と同様)、さまざまな可能なデータ型を表すために使用されます。
関数定義でジェネリクスを使用する
ジェネリクスを使用して関数を定義する場合、関数シグネチャでパラメータと戻り値の型を明示的に指定する代わりに、ジェネリクスを使用してコードをより適応的にします。これにより、関数呼び出し側にとってより柔軟になり、コードの重複を回避できます。
Rustでは、ジェネリックパラメータ名は任意ですが、慣例により、T
(「型」の最初の文字)が推奨されます。
ジェネリックパラメータを使用する前に、宣言する必要があります。
fn largest<T>(list: &[T]) -> T {...}
ジェネリックバージョンの関数では、型パラメータ宣言 <T>
は関数名とパラメータリストの間に現れます(largest<T>
など)。これはジェネリックパラメータ T
を宣言し、これはパラメータ list: &[T]
と戻り値の型 T
で使用されます。
パラメータ部分では、list: &[T]
は、パラメータ list
が型 T
の要素のスライスであることを意味します。
戻り値の部分では、-> T
は、関数の戻り値も型 T
であることを示します。
したがって、この関数定義は、関数がジェネリックパラメータ T
を持ち、その引数が型 T
の要素のスライスであり、型 T
の値を返すものとして理解できます。
要約すると、ジェネリック関数の場合、関数名の後の <T>
は、関数のスコープでジェネリック T
が定義されていることを意味します。このジェネリックは、スコープ内で定義された変数がそのスコープ内でのみ使用できるのと同様に、関数のシグネチャと本文内でのみ使用できます。ジェネリックは、単にデータ型の変数を表します。
したがって、関数シグネチャのこの部分が表す意味は、何らかのデータ型のパラメータが渡され、同じ型の値が返されることです。そして、この型は何でもかまいません。
構造体でジェネリクスを使用する
構造体のフィールド型も、ジェネリクスを使用して定義できます。例:
struct Point<T> { x: T, y: T, }
注:構造体のフィールドで T
を型として使用する前に、最初に Point<T>
でジェネリックパラメータを宣言する必要があります。また、この場合、x
と y
は同じ型です。
x
と y
を異なる型にしたい場合は、異なるジェネリックパラメータを使用できます。
struct Point<T, U> { x: T, y: U, } fn main() { let p = Point { x: 1, y: 1.1 }; }
列挙型でジェネリクスを使用する
ジェネリクスは列挙型でも使用できます。Rustで最も一般的なジェネリック列挙型は Option<T>
と Result<T, E>
です。
enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), }
Option
と Result
は、関数の戻り値の型としてよく使用されます。Option
は値の有無を示すために使用され、Result
は値が有効かどうか、またはエラーが発生したかどうかを重視します。
関数が正常に実行されると、Result
は Ok(T)
を返します。ここで、T
は実際の戻り値の型です。関数が失敗すると、Err(E)
を返します。ここで、E
はエラー型です。
メソッドでジェネリクスを使用する
ジェネリクスはメソッドでも使用できます。メソッド定義でジェネリックパラメータを使用する前に、impl<T>
を使用して事前に宣言する必要があります。このような宣言の後でのみ、Point<T>
を内部で使用して、Rustが山括弧内の型が具体的な型ではなくジェネリックであることを認識できるようにすることができます。
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } }
注:メソッド宣言内の Point<T>
はジェネリック宣言ではありません。これは構造体の完全な型です。なぜなら、構造体は単なる Point
ではなく Point<T>
として定義されているからです。
構造体のジェネリックパラメータを使用するだけでなく、ジェネリック関数のように、メソッド自体内に追加のジェネリックパラメータを定義することもできます。
struct Point<T, U> { // 構造体のジェネリクス x: T, y: U, } impl<T, U> Point<T, U> { // 関数のジェネリクス fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } }
この例では、T
と U
が構造体 Point
で定義されたジェネリックパラメータであり、V
と W
がメソッドで定義されたジェネリックパラメータです。競合はありません。それらを構造体のジェネリクスと関数のジェネリクスとして考えてください。
ジェネリクスに制約を追加して、特定の型に対してのみメソッドを定義することもできます。たとえば、Point<T>
型の場合、T
だけでなく、特定の型に基づいてメソッドを定義できます。これは、メソッドがその特定の型に対して定義され、異なる T
型の Point<T>
の他のインスタンスはそのメソッドを持たないことを意味します。これにより、特定のジェネリック型に対して特殊化されたメソッドを定義し、他の型をそのメソッドなしで残すことができます。
ジェネリクスに制約を追加する
ジェネリクスの制約は、トレイト境界とも呼ばれます。これには主に2つの構文があります。
- ジェネリック型
T
を定義するときに、T: Trait_Name
のような構文を使用して制約を適用します。 - 戻り値の型の後、関数の本体の前に
where
キーワードを使用します(where T: Trait_Name
など)。
要するに、ジェネリックを制約する主な理由は2つあります。
- 関数の本体は、特定のトレイトによって提供される機能が必要です。
- ジェネリック
T
で表されるデータ型は、十分に正確である必要があります。(制約が適用されない場合、ジェネリックは任意のデータ型を表すことができます。)
Const Generics
これまでのところ、ジェネリクスは型に対して実装されたジェネリクスとして要約できます。すべてのジェネリクスは、異なる型を抽象化するために使用されてきました。
同じ型でも長さが異なる配列は、Rustでは異なる型と見なされます。たとえば、[i32; 2]
と [i32; 3]
は異なる型です。スライス(参照)とジェネリクスを使用して、任意の型の配列を処理できます。例:
fn display_array<T: std::fmt::Debug>(arr: &[T]) { println!("{:?}", arr); }
ただし、上記の方法は、参照が不適切なシナリオではうまく機能しません(またはまったく機能しません)。そのような場合、値に対する抽象化を可能にする const generics を使用して、配列の長さの違いを処理できます。
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) { println!("{:?}", arr); }
このコードは、[T; N]
型の配列を定義します。ここで、T
は型ベースのジェネリックパラメータであり、N
は値ベースのジェネリックパラメータです。この場合、配列の長さを表します。
N
は const generic です。これは const N: usize
を使用して宣言されます。これは N
が usize
値に基づく const ジェネリックパラメータであることを意味します。const genericsの前に、Rustは複雑な行列計算にはあまり適していませんでした。const genericsを使用すると、それは変わりつつあります。
注: メモリ制約のあるプラットフォームでコードを実行する必要があり、関数のパラメータが消費するメモリ量を制限したい場合、そのような場合には、const genericsを使用してこれらの制約を表現できます。
ジェネリクスのパフォーマンス
Rustのジェネリクスは ゼロコスト抽象化 であり、使用時にパフォーマンスのオーバーヘッドを心配する必要はありません。一方、トレードオフは、コンパイル時間が長くなり、最終的なバイナリサイズが大きくなる可能性があることです。これは、Rustがコンパイル中にジェネリックで使用される特定の型ごとに個別のコードを生成するためです。
Rustは、コンパイル時にジェネリックコードを 単相化 することによって効率を保証します。単相化は、プログラムで使用される具体的な型を埋め込むことによって、ジェネリックコードを特定のコードに変換するプロセスです。コンパイラが行うことは、ジェネリック関数を記述するときに行うことの逆です。コンパイラは、ジェネリックコードが使用されているすべての場所を見て、それらの特定の型に対する具体的な実装を生成します。これが、Rustでジェネリクスを使用することにランタイムコストがない理由です。単相化は、Rustのジェネリクスがランタイムで非常に効率的な理由です。
rustc
がコードをコンパイルするとき、コンパイル時に変数名がメモリアドレスに置き換えられるのと同じように、すべてのジェネリクスをそれらが表す実際の具体的なデータ型に置き換えます。コンパイラはジェネリック型を具体的な型に置き換えるため、これにより コードの肥大化 が発生する可能性があります。この場合、1つの関数が異なるデータ型に対して複数の特殊化されたバージョンに拡張されます。場合によっては、この肥大化がコンパイルされたバイナリのサイズを大幅に増加させる可能性があります。ただし、ほとんどの場合、これは大きな問題ではありません。
良い面としては、ジェネリクスはコンパイル時に具体的な型に既に解決されているため、ジェネリックであった関数を呼び出すには、型を決定するための追加のランタイム計算は必要ありません。したがって、Rustのジェネリクスにはランタイムオーバーヘッドはゼロです。
まとめ
Rustでは、関数シグネチャや構造体などの項目の定義を作成するためにジェネリクスを使用できます。これにより、複数の具体的なデータ型で使用できます。ジェネリクスは、関数、構造体、列挙型、およびメソッドで使用して、コードをより柔軟にし、繰り返しの数を減らすことができます。ジェネリック型パラメータは、<A, B, ...>
のように、キャメルケースで大文字を使用して山括弧で指定されます。Rustは、コンパイル中の 単相化 を通じてジェネリクスの効率を保証します。これにより、ジェネリックコードは実際の型を埋め込むことによって特殊化された具体的なコードに変換されます。これにより、コードの肥大化が発生しますが、高いランタイムパフォーマンスが保証されます。
Leapcellは、Rustプロジェクトをホストするための最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、およびRedisのための次世代のサーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、または Rust で開発。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い - リクエストも料金もかかりません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60ミリ秒で694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムのメトリクスとロギング。
簡単なスケーラビリティと高性能
- 高い同時実行性を簡単に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ - 構築に集中するだけです。
ドキュメント で詳細をご覧ください。
X でフォローしてください: @LeapcellHQ