全3869文字
PR

 プログラミングが持つ大きな力が「抽象化」だ。抽象化によってコードの再利用性を高めたり、拡張を容易にしたりできる。Rustでは「トレイト」と「ジェネリクス」という2つの機能を利用することで、抽象化を高めてよりスマートなコードを書くことができる。

任意の型を受け付けられるジェネリクス

 ジェネリクスはJavaやC#、TypeScriptなど多くのプログラミング言語にもある機能だ。なじみがある読者もいるかもしれない。最近ではGoにジェネリクスが導入され、大きな話題になった。

 コードを書いていると、「適用される型が異なるだけで、処理の根幹は同じ」というシチュエーションによく遭遇する。構造体の中に持たせたデータ型の種類が増えるだけで「そこに値を持たせる」という目的は変わらない、あるいは関数の引数の型が異なるだけで処理内容が全く同じ、といったケースがある。

 ジェネリクスを利用すると、そうした「適用される型が異なるだけで、処理の根幹は同じ」というケースをまとめられる。共通処理をまとめて抽象化するのはプログラミングの基本的動作である。プログラムの再利用性が高まるといった実務上のメリットもある。

複数の処理を1つにまとめる

 ジェネリクスを使うと何がうれしいのだろうか。それを理解するために、「大元の処理は同じだが、受け取る引数の型が異なる関数」を取り上げよう。例として、i32型の値を受け取り、その内容を「content is {任意の値}」と標準出力する次のような関数を考えてみる。

fn print_content_i32(content: i32) {
    println!("content is {}", content);
}

fn main() {
    // 「content is 42」と出力される
    print_content_i32(42);
}

 こうした関数では「i32型を出力できるのだから、f32型も出力できるようにしたい」という場合がよくある。そのために「print_content_f32」という関数を新たに用意し、同様に標準出力を行う処理をそこに書くことになる。

fn print_content_i32(content: i32) {
    println!("content is {}", content);
}

fn print_content_f32(content: f32) {
    println!("content is {}", content);
}

fn main() {
    // 「content is 42」と出力される
    print_content_i32(42);
    // 「content is 42.1」と出力される
    print_content_f32(42.1);
}

 ここでさらに、u32型に対しても同様に標準出力を行いたいとする。すると、「print_content_u32」という関数を新たに用意し、同様に標準出力する処理を書くことになるだろう。

 数値型には、64ビットのi64型やu64型もある。様々な数値型に対応しようとすると、それぞれの型を扱う関数が増えていってしまう。ここまで共通処理が多いなら、関数を1つにまとめたいと思うのは自然なことだ。