うに.log

作業した内容のまとめとか読んだ本のまとめとか。間違っていることがあったらツッコミとかがもらえるといいなぁという願望のもと、とりあえずやったこと、調べたこと、理解していることしていないことをだらだら書いていく。勉強用のブログ。

C# 2.0 - ジェネリックについて

C# の古い仕様から順に雑にまとめていく予定。

ジェネリック Generics

ジェネリックとは

ジェネリックとは、クラスや構造体、関数に対して型のパラメータを持たせることができるようになる機能のことです。

実際には <T><T, U> <TKey, TValue>などの形式で見かけます。((T は Type つまり型の省略形で非常によく使用されます))*1

C# のコードでよく見るものとしては Dictionary<TKey, TValue>, List<T>, Task<T>, Enumerable.Select<TSource, TResult> など。

T などの事を型引数と言います。

何ができるのか

具体例

具体的に置き換わる例を載せた方が早いと思われるのでまずは例から。*2

ここでは単純に値を保持するだけのクラスを作ることを考えてみます。

ジェネリックを使用しない場合

public class Int32Holder
{
    public Int32 Item;
}
public class Int64Holder
{
    public Int64 Item;
}
public class StringHolder
{
    public string Item;
}

ジェネリックを使用した場合

public class Holder<T>
{
    public T Item;
}

非常にシンプルになりました。

利用するときは以下ように利用します。

Holder<string> stringHolder = new Holder<string>();
Holder<Int32> Int32Holder = new Holder<Int32>();
・
・
・

この例としている Holder の場合、型に依存する処理が一切存在しないため、完全に置き換え可能でした。

このような状態のことを直行性) が高いと表現します。

このように、直行性の高い処理系をまとめて実装することができるのがジェネリックの強みであり真骨頂であると言えます。

脱線:object について

ジェネリックを使わずに object でやってもいいよね?って言う人がいたりするのですが、それは誤りで、パフォーマンス的にもコード的にも直行性の高い処理をまとめるために object を使うのは無しです。

Holder の例ですとこういった実装になると思います。

public class ObjectHolder
{
    public object Item;
}

しかし、この実装だと利用側で逐一キャストしなければならないため、非常に扱いづらいものとなります。

string item = (string)holder.Item;

また、中に何が入っているかを把握していないといけないため、キャストが失敗する可能性も考慮に入れないといけません。

ObjectHolder holder = new ObjectHolder();
holder.Item = 1; // 何でも入る
string item:
if (holder.item is string) // string 以外だとキャスト時にエラーが起きてしまうので避ける
{
    item = (string)holder.Item;
}

一方、ジェネリックを使った場合はを使用することで型安全に、柔軟に多様な型を利用することができます。

Holder<string> holder = new Holder<string>();
// holder.Item = 1; // コンパイルできない
holder.Item = *Item";
string item = holder.Item;

また、object に構造体(struct)を入れるとボックス化*3が発生するため、パフォーマンス面でもデメリットが大きいので、ジェネリックが存在する現在は滅多に使用することはありません。*4

型引数の制約

ここまでの説明ではジェネリックを使えるのは完全に置き換え可能な直行性の非常に高いものだけしか扱え無いことになります。

そこで利用されるのが制約です。

実際に使っている例を見てみましょう。

public class Creator<T>
    where T : new()
{
    public T Create()
    {
        return new T();
    }
}

ここで新しいキーワードとして where が出てきました。

where を宣言することで T がどういったものなのかを定義することができるようになります。

今回の例では特殊なキーワード new() によって、引数なしのコンストラクタを持つ、という制約を与えたため、 Create 関数にて引数なしコントラクタを呼び出してインスタンスを生成して返却することができるようになっています。

where では以下の制約を与えることができます。

キーワード 制約内容
new() 引数なしコンストラクタを持っていること
struct 構造体であること
class クラスであること
[Base Name] [Base Name]を継承していること
[Interface Name] [Interface Name] を実装していること

複数の型引数に制約を与えたり、1つの型引数に複数の制約を与えることもできます。その場合は以下のように宣言します。

public class Sample<T, U>
    where T : class, new()
    where U : struct, IDisposable

これらを活用することでより柔軟に、型安全に様々な処理をすることができるようになります。使える場面があれば使ってみてください。

実際には使ってみたけど実は必要なかった、っていう場面には結構出くわすと思いますが、この辺りを判断するのは難しいため、使って自爆していくしか無いんじゃないかなと思います。

*1:U は単にアルファベット順で T の次という理由でこれもよく使用されます

*2:プロパティは別記事で書くためここではただの public なフィールドとして定義

*3:別の記事で細かいことは解説します

*4:稀に意図して利用することはあります