Rustは、ジェネリクスとトレイトを活用することで、型に依存しない柔軟で再利用可能なコードを作成できます。
これにより、異なる型のデータに対して同じロジックを適用したり、型に応じた処理を分岐させることが可能です。
この記事では、Rustのジェネリクスとトレイトの基本を学び、これらを用いて柔軟なコード設計を行う方法を解説します。
ジェネリクスとは?
ジェネリクスは、特定の型に依存せずに処理を記述できる機能です。
関数や構造体、列挙型でジェネリクスを使用することで、さまざまな型に対応した柔軟な設計が可能になります。
ジェネリクスの基本
ジェネリクスを使って、複数の型に対応する関数を作成する例を見てみましょう。
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("一番大きい数は: {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("一番大きい文字は: {}", result);
}
ポイント:
largest
関数は、ジェネリクスT
を使用して、整数や文字といった異なる型に対応しています。T: PartialOrd + Copy
で、ジェネリクスが使える型の条件を指定しています。PartialOrd
は順序比較可能であることを示し、Copy
は値をコピーできることを示します。
ジェネリクスを使った構造体
構造体でもジェネリクスを使って、柔軟なデータ型を扱うことができます。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
println!("整数のPoint: ({}, {})", integer.x, integer.y);
println!("浮動小数点のPoint: ({}, {})", float.x, float.y);
}
ポイント:
Point
構造体は、ジェネリクスT
を使って定義され、異なる型(整数や浮動小数点数)を扱うことができます。
トレイトとは?
トレイトは、型が実装すべき共通のメソッドのセットを定義するものです。
トレイトを使用すると、異なる型に対して共通の処理を適用でき、動的な型に対しても安全に操作が可能になります。
トレイトの定義と実装
次に、トレイトを定義し、型に対して実装する例を見てみましょう。
// トレイトの定義
trait Summary {
fn summarize(&self) -> String;
}
// トレイトを型に実装
struct NewsArticle {
headline: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("記事のタイトル: {}", self.headline)
}
}
fn main() {
let article = NewsArticle {
headline: String::from("Rustの新機能"),
content: String::from("Rust 1.55がリリースされました。"),
};
println!("{}", article.summarize());
}
ポイント:
Summary
というトレイトを定義し、NewsArticle
型に対してSummary
トレイトを実装しています。- トレイトに定義されたメソッド
summarize
をNewsArticle
内で具体的に実装しています。
トレイト境界とジェネリクスの組み合わせ
ジェネリクスとトレイトを組み合わせることで、さらに柔軟なコードを記述できます。
トレイト境界を使用すると、ジェネリクスの型が特定のトレイトを実装しているかどうかを保証できます。
fn notify<T: Summary>(item: &T) {
println!("新しい通知: {}", item.summarize());
}
fn main() {
let article = NewsArticle {
headline: String::from("Rust 1.55がリリースされました"),
content: String::from("最新のRust機能をチェックしましょう。"),
};
notify(&article);
}
ポイント:
notify
関数は、Summary
トレイトを実装している型に対して動作します。これにより、特定のメソッドが実装されている型に対してのみ、関数が適用されることが保証されます。
トレイトオブジェクト
Rustでは、トレイトを使って動的ディスパッチ(実行時にメソッドを決定すること)が可能です。
これは、トレイトオブジェクトを使うことで実現できます。
トレイトオブジェクトの例
trait Draw {
fn draw(&self);
}
struct Button {
width: u32,
height: u32,
label: String,
}
impl Draw for Button {
fn draw(&self) {
println!("ボタン: {} ({}x{})", self.label, self.width, self.height);
}
}
fn draw_widget(widget: &dyn Draw) {
widget.draw();
}
fn main() {
let button = Button {
width: 50,
height: 20,
label: String::from("送信"),
};
draw_widget(&button);
}
ポイント:
- トレイトオブジェクト(
&dyn Draw
)を使って、動的にオブジェクトのメソッドを呼び出しています。 - トレイトオブジェクトを使用することで、異なる型のオブジェクトでも共通のメソッドを呼び出すことができます。
ジェネリクスとトレイトを組み合わせた実践例
ジェネリクスとトレイトを組み合わせて、汎用的なデータ構造を作成しましょう。
trait Area {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
struct Circle {
radius: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Area for Circle {
fn area(&self) -> f64 {
3.14159 * self.radius * self.radius
}
}
fn print_area<T: Area>(shape: &T) {
println!("図形の面積は: {}", shape.area());
}
fn main() {
let rectangle = Rectangle {
width: 30.0,
height: 50.0,
};
let circle = Circle { radius: 10.0 };
print_area(&rectangle);
print_area(&circle);
}
ポイント:
Rectangle
とCircle
という2つの図形に対して、Area
トレイトを実装しています。print_area
関数は、Area
トレイトを実装している型に対してのみ動作し、図形の面積を計算して表示します。
練習問題
練習問題1: ジェネリクスとトレイトを使った柔軟な関数の作成
次のコードを参考に、PartialOrd
とCopy
トレイトを組み合わせた関数smallest
を実装してください。関数は、与えられたリストから最小の要素を返します。
fn smallest<T: PartialOrd + Copy>(list: &[T]) -> T {
// ここでコードを記述
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = smallest(&number_list);
println!("一番小さい数は: {}", result);
}
まとめ
今回は、Rustのジェネリクスとトレイトについて学びました。ジェネリクスを使うことで、異なる型に対応する柔軟なコードを記述でき、トレイトを使うことで共通のメソッドを実装した複数の型に対して一貫性のある処理を行うことが可能です。
さらに、トレイトオブジェクトを使用することで、動的にメソッドを呼び出す柔軟性も得られます。