今回は、ポリモーフィズム編では分量の問題で取り扱えなかったジェネリックについて解説していきたいと思います。
オブジェクト指向入門 一覧
- その1 概要編
- その2 カプセル化編
- その3 ポリモーフィズム編
- その4 ジェネリック編 ←ここ
ジェネリックとは
まず初めに、プログラミングにおけるジェネリックはジェネリック医薬品とは全く関係ありません。ジェネリックという単語自体が幅広い意味を持っていて、前者は「総称的」、後者は「一般的」という風に訳せます。これ以上深掘りすると筆者が混乱するのでここで留めます。
ジェネリックプログラミングという概念自体の説明というのは難しいので、C#に絞って具体的に見ていきましょう(他のOO言語にも転用できるはずです)。C#におけるジェネリックは2つの用途があります。
- 型に依存しない汎用的なクラスを作る。
- 特定の型を継承、ないしインターフェイスを実装したクラスを扱うクラスを作る。
矛盾した用途に答えられる万能の解決策がジェネリックです。これにより、ポリモーフィズムは完成を見たと筆者は考えています。
用途1:汎用的コレクション
C#(をはじめとしたプログラミング言語)はリスト構造、すなわち自由に拡張・挿入・削除ができる配列的構造をサポートしています。しかしその原始はかなり粗雑なものでした。今まで扱うのを避けていましたが、C#の型・クラスの共通祖先としてObject
型があります。進次郎構文的に言えば、すべての型はObject
に変換できるのです。そのため、当初のリストはすべての要素をObject
に変換して保持していました。ArrayList
型です。
これが問題なのは、読者にもお分かりだと思います。一番最初に思い浮かぶのは「取り出すときに元の型に戻さねばならない」ことです。これは気分が下がりますよね。さらに致命的なのはどんなものでもArrayListに入れられるという問題です。つまり、本来入れられたくないデータでも入るため、不正な型のデータが入る前に例外を出す必要が出てきて、使い勝手がなおさら良ろしくありません。
using System.Collections;
public class Program
{
public static void Main()
{
ArrayList hopefullyIntList = new ArrayList();
hopefullyIntList.Add(1);
hopefullyIntList.Add("two"); // 危険!
hopefullyIntList.Add(3);
int sum = 0;
foreach (object hopefullyInt in hopefullyIntList)
{
if (hopefullyInt is int integer) // hopefullyIntがintに変換できる場合、変換した値をintegerという変数で使用する
{
sum += integer;
}
}
Console.WriteLine($"リストの総和は{sum}です。"); // 4
}
}
そこで編み出されたのがジェネリック構文です。List
型を宣言する際に同時に許容する型の要素を入れられるようにしたのです。これにより、リスト構造の安全性が飛躍的に上昇しました。そして取り出す際の変換も不要です。
using System.Collections.Generic;
public class Program
{
public static void Main()
{
List<int> intList = new List<int>();
intList.Add(1);
// intList.Add("two"); という入力はコンパイラがエラーを出す。
intList.Add(3);
int sum = 0;
foreach (int item in intList)
{
sum += item;
}
Console.WriteLine($"リストの総和は{sum}です。"); // 4
}
}
ジェネリック型の後に山かっこでそのジェネリック型が何を許容するかを示します。もはやこれがあれば、ArrayList
を使うことはありません。
非ジェネリックコレクションの原罪
しかし、だからと言ってArrayList
などの非ジェネリックコレクションを廃止にはできません。それは歴史的経緯というものです。まだまだ世界には初期のC#で書かれたコードがたくさんあるようです。それらを切り捨てるのは生産性を逆に下げるというものです。覆水盆に返らず。過去の過ちは消えることは無いのです。個人的には.Net環境のオープンソース化に合わせて廃止すべきだったのでは、とも思いますが…
でも、ジェネリックコレクションを書いたら非ジェネリック版を自動生成できそうな気もします。
IComparable<T>
などの比較用インターフェイスを実装した場合、Visual Studioは自動的に非ジェネリック版の実装を促してくれるので助かります。
LINQ:コレクションの中身を抜き出す
また、一つ一つ読み出すことが可能なジェネリックコレクションIEnumerable<T>
を利用した機能として、LINQ(Language-INtegrated Query, 日本語に訳すと統合言語クエリ)があります。これはコレクションの中身を取り出して、いろいろと汎用的な処理ができるメソッド集というべきものです。専門用語では「リスト内包表記」(List comprehension)と言います。JavaScriptのArray.prototype.filter
関数などがこれに当たります。Javaも後発でStream APIとして類似機能をサポートするようになりました。例えば先ほどの例のsum
計算はLINQを使えばスマートに書くことができます。
int sum = intList.Sum();
集計作業をたった一行で書けるようになり、また何をするかも自明になりました。処理を加える場合も簡単です。
int sum1 = 0;
foreach (int item in intList)
{
sum1 += item > 0 ? item : 0;
}
int sum2 = intList.Sum(item => item > 0 ? item : 0);
最初の書き方ではforeach
ループ中にノイズが入る可能性がありますが、LINQならその心配はありません。
用途2:特化メソッド
逆に、ジェネリック型の制限を使うことで、メソッドの引数の型を制限できます。前回紹介したHarpGuitar
クラスを使ってみましょう。
public interface IHarp //ハープが持つ機能を抽出
{
int NumberOfStrings { get; }
void Strum(int[] stringsToPlay);
void PlayNote(int stringToPlay);
}
public interface IGuitar //ギターが持つ機能を抽出
{
int NumberOfStrings { get; }
void Strum(List<Fretting> fingering); // コード弾き。<>のジェネリック宣言は後述
void PlayNote(Fretting fret); // 単音弾き
}
public class Fretting // 運指
{
public int FretNumber { get; }
public int StringNumber { get; }
public Fretting(int stringNumber, int fretNumber)
{
StringNumber = stringNumber;
FretNumber = fretNumber;
}
}
public class HarpGuitar : IHarp, IGuitar // ハープギター。ギターにさらに開放弦をたくさん張ったギター
{
public int IHarp.NumberOfStrings { get; } // このようにメソッドシグネチャが完全に被るメンバーは
public int IGuitar.NumberOfStrings { get; } // インターフェイス毎に定義しないといけない
public HarpGuitar(int harpStrings, int guitarStrings = 6)
{
IHarp.NumberOfStrings = harpStrings;
IGuitar.NumberOfStrings = guitarStrings;
}
public void Strum (int[] stringsToPlay) { /* ハープ部分を弾く処理 */ } // IHarpインターフェイスの実装
public void Strum(List<Fretting> fingering) { /* ギター部分を弾く処理 */ }
// IGuitarインターフェイスの実装。メソッドシグネチャが違うのでインタフェイス名を付けなくてもよい
public void PlayNote(int stringtoPlay) { /*ハープの単音引き*/ }
public void PlayNote(Fretting fret) { /* ギターの単音弾き */ }
}
インタフェイス経由ではHarpGuitar
クラスのハープの部分とギターの部分を別々にしか使えません。通常はIHarp
とIGuitar
を合成したインタフェイスを作ります。しかし、ジェネリック制約をうまく使えば、両方扱えるメソッドを作れます。
public static void PlayHarpGuitar<THarpGuitar>(THarpGuitar harpGuitar) where THarpGuitar : IHarp, IGuitar
{
harpGuitar.Strum([1, 3, 5, 8]); // ドミソド
harpGuitar.Strum(new List<Fingering> { new Fingering(1, 1), new Fingering(2, 1), new Fingering(3, 2), new Fingering(4, 3), new Fingering(5, 3), new Fingering(6, 1) }); // ドファラド
harpGuitar.Strum(new List<Fingering> { new Fingering(1, 0), new Fingering(2, 1), new Fingering(3, 0), new Fingering(4, 2), new Fingering(5, 3) }); // ドミソド
harpGuitar.Strum([2, 4, 5, 7]); // レファソシ
harpGuitar.Strum([1, 3, 5, 8]); // ドミソド
}
このように両方読み出せるメソッドを作ることができました。このようにジェネリックに複数制約を自由に付けられるのはJavaには無い利点です1。例えばデータコンテナクラスから情報を分離する際に使えると思います。また、ジェネリック制約はクラスに掛けることもできますが、クラス宣言時に具象クラスが必要になるので自由度は下がります。
終わりに
以上4回に分けてお送りしたオブジェクト指向入門。少々駆け足ですし、説明が至らない部分も多々あったと思いますが、オブジェクト指向で大事な部分をまとめられたかなと自負します。オブジェクト指向に触れる皆さんはオブジェクト指向で物事を考えられるプログラマーになりましょう!
-
Javaは実行時に型情報を消し込んでいるためです。つまりコードのビルド時に論理的におかしくないか見ているわけです。これには内部で特殊な型を作らなくていいことと、今までの非ジェネリックコレクションと内部処理を共通化できるという利点があります。要するに過去の資産を活用する方向に進んだわけですね。逆にC#は中間言語のMSILが型情報を持っているので、型に沿った処理ができるという利点があります。 ↩