C#(.NET)入門シリーズ第三弾です。
今回は普段のコーディングでよくお世話になっているジェネリックです。
ジェネリックとは?
Listをnewする際に< >
を使って型を指定することありますよね?
そう、あれがジェネリックです。
主にListなどのコレクションクラスに適用されていることが多いですが、ジェネリックを使った実装はあらゆるところで有効です。
公式ドキュメントに記載されている概要は以下。(一部文言編集済)
- コードの再利用、タイプ セーフ、およびパフォーマンスを最大化するために使用
- ジェネリックの最も一般的な用途は、コレクションクラスの作成
- .NET クラス ライブラリには、複数のコレクションクラスが System.Collections.Generic 名前空間に含まれる
- 独自のインターフェイス、クラス、メソッド、イベント、およびデリゲートを作成可能
- ジェネリック クラスは、特定のデータ型のメソッドへのアクセスを有効にするように制限
- ジェネリック データ型で使用される型に関する情報を実行時に取得するには、リフレクションを使用
実装例を交えてジェネリックを使うメリットを紐解いていきましょう👍
メリット1: 型安全性の確保
様々な型を扱う想定のプロパティを持つクラスの場合、ジェネリックを使わずともプロパティの型をobject
として宣言すれば問題なく処理を実行できないことはないです。
ただし、object
はコンパイラに型情報を伝えないため、仮に型不一致のエラーが起きていた場合、エラーになっていることに気づきにくいです。
コンパイルエラーではなく実行時のエラーとなるからですね。
例えば、以下のようなクラスがあったとしましょう。
public class ObjectItem
{
private object value;
public ObjectItem(object initialValue)
{
this.value = initialValue;
}
public object GetValue() => this.value;
public void SetValue(object newValue)
{
this.value = newValue;
}
}
以下のような操作をすると実行時エラーになります。
var item = new ObjectItem("String Value");
// コンパイルは通るが、実行時にInvalidCastException発生
int intItem = (int)item.GetValue();
object
型は様々な型を受け入れ可能ですが、コードを見ながらキャスト可能か否かを己の力で注意深く見守っておく必要があるわけです😵💫
実際のプロダクトコードの量は膨大かつ、人間はミスをするものなので、この実装だとバグを埋め込む可能性が高いのは明白ですよね。
そんなときに役立つのがジェネリックです!
ジェネリックを使うとコンパイル時に型が確定するため、キャストのミスはコンパイル時に発見できます。
先ほどのクラスをジェネリックを使って書き換えると以下の実装になります。
// クラス宣言時に<T>をつける
public class GenericItem<T>
{
private T value;
public GenericItem(T initialValue)
{
this.value = initialValue;
}
public T GetValue() => this.value;
public void SetValue(T newValue)
{
this.value = newValue;
}
}
var genericItem = new GenericItem<string>("string Value");
// コンパイル時にエラーを発見できる
int intItem = (int)genericItem.GetValue();
ちなみに、ジェネリックはコンパイル時に型が決まっているため、型が一致している代入においてキャストは必要ありません。
// stringに代入する場合はキャスト不要
string stringItem = genericItem.GetValue();
メリット2: パフォーマンス向上
上記でも触れましたが、ジェネリックを使用しない場合はobject
で代用しようとすることが多いと思います。
object
型のプロパティで値型(intやdouble)を場合、ボックス化が起きます。
ボックス化が起きるとアプリケーションのパフォーマンスに大きな影響を与えるため、なるべくならobject
は使いたくないところ。
ボックス化がアプリケーションに与える影響については下記記事で熱く語ってますので、ぜひご一読ください🙇♂️
【C#】ボックス化を避けるべき理由
メリット3: コードの共通化と簡易化
中身の処理が共通なのに扱う型が異なる場合、いちいち型ごとに同じ処理を実装するのは無駄ですよね?
ジェネリックを使えば型にとらわれず共通のロジックを共有することができます。
List
などのコレクションクラスがまさにそうですね。
複数の値(オブジェクト)を扱うという役割が果たせてれば良いので、コレクションの中身の型には関心がありません。
簡易的に実装してみました。
public class GenericList<T>
{
private T[] items;
public GenericList(T[] initialList)
{
this.items = initialList;
}
public int GetCount() => this.items.Count();
}
このクラスを利用する際に型を指定することで配列の中身の値を確定できます。
ただ、このクラスの役割からすると中身の型には関心がなく、配列の合計値を返すことができれば良いわけです。
仮にジェネリックを使用しないとするとobject
型を採用するか、使用される型が増えるごとに配列を管理するクラスを作成するしかないのです。
異なる型を扱う可能性がある & 複数回呼ばれる可能性があるクラスやメソッドにはジェネリックが有用になります。
注意点
-
過剰な汎用化を避ける
必要以上にジェネリクスを使用するとコードが複雑になるケースも。単純なケースでは通常の型や非ジェネリックな方法を検討すべきです。 -
パフォーマンスへの影響
ジェネリックコードは効率的ですが、大量の値型引数の場合には追加コピーが発生する可能性があります。 -
テストの重要性
ジェネリックコードは異なる型引数で動作するため、多様なシナリオで十分にテストする必要があります。
まとめ
特徴 | object型 | ジェネリック |
---|---|---|
型安全性 | 実行時まで保証されない | コンパイル時に保証される |
パフォーマンス | ボックス化・アンボックス化で低下 | 高速 |
可読性・メンテナンス性 | キャストや型チェック必要 | 簡潔で明確 |
参考