読者です 読者をやめる 読者になる 読者になる

謎言語使いの徒然

適当に気になった技術や言語を流すブログ。

.NET を理解する4

.NET C# Tips
ジェネリックを理解する

.NET Framework 2.0 から追加された機能です。
主にコレクションで良く見られますが、プログラムの設計においてもタイプセーフでかつ汎用性の高いオブジェクトを作成することができます。

論より Run

以下で簡単な Generic の実装サンプルを示します。
ホテルルームは基本的に何でも入ります。ここでは、人間がチェックイン可能なホテルルームを作ります。

class Program
{
    static void Main(string[] args)
    {
        HotelRoom<Human> room
            = new HotelRoom<Human>();

        room.CheckIn(new Human("azalea", true));
    }
}

public class Human
{
    private string name;
    private bool ismale;

    public Human(string name, bool isMale)
    {
        this.name = monsterName;
        this.ismale = isMale;
    }

    public bool IsMale
    {
        get
        {
            return ismale;
        }
    }

    public string Name
    {
        get
        {
           return this.name;
        }
    }
}

class HotelRoom<T>
{
    private T customer;

    public void CheckIn(T someone)
    {
        this.customer = someone;
    }

    public bool IsThere()
    {
        return customer != null;
    }
}

このとき、room は Human で型が決定されています。
すると、このような代入はできなくなります。

HotelRoom<Human> room
   = new HotelRoom<Human>();

room.CheckIn("liliy");


上記は、異なる型を挿入したことでコンパイルエラーになります。
これはソースを記述する際に取り込む型を決定せず、実行時に型を決定することができる仕組みです。
もちろんオブジェクト指向に則るコーディングも可能ではありますが、初期のクラス設計時点ではまだしも、後からクラスを追加する際に不便な場合もあります。そんなときに使える機能です。
しかし、このままでは実は危険ですね。

class Program
{
    static void Main(string[] args)
    {
        HotelRoom<Monster> room
            = new HotelRoom<Monster>();

        room.CheckIn(new Monster("Zombie"));
    }
}

public class Human
{
    private string name;
    private bool ismale;

    public Human(string name, bool isMale)
    {
        this.name = monsterName;
        this.ismale = isMale;
    }

    public bool IsMale
    {
        get
        {
            return ismale;
        }
    }

    public string Name
    {
        get
        {
           return this.name;
        }
    }
}

public class Monster
{
    private string name;

    public Monster(string monsterName)
    {
        this.name = monsterName;
    }

    public string Name
    {
        get
        {
           return this.name;
        }
    }
}

class HotelRoom<T>
{
    private T customer;

    public bool CheckIn(T someone)
    {
        this.customer = someone;
    }

    public bool IsThere()
    {
        return customer != null;
    }
}

なんということでしょう、ホテルにゾンビがチェックインしてきました!
これではこのホテルでバイオハザードが起きてしまいます(笑
そこで、Generic を指定する際に、制限を設けます。

class Program
{
    static void Main(string[] args)
    {
        HotelRoom<Programmer> room
            = new HotelRoom<Programmer>();

        room.CheckIn(new Programmer("azalea", true));
    }
}

public class Programmer : Human
{
    public Programmer(string name, bool isMale)
        : base(name, isMale)
    {
    }

    public void WriteCode()
    {
        // なんらかの処理
    }
}

public class Human
{
    private string name;
    private bool ismale;

    public Human(string name, bool isMale)
    {
        this.name = name;
        this.ismale = isMale;
    }

    public bool IsMale
    {
        get
        {
            return ismale;
        }
    }

    public string Name
    {
        get
        {
           return this.name;
        }
    }
}

class HotelRoom<T> where T : Human
{
    private T customer;

    public void CheckIn(T someone)
    {
        this.customer = someone;
    }

    public bool IsThere()
    {
        return customer != null;
    }
}

「where T : Human」の記述によって、このジェネリッククラスは Human かそれを継承するクラスしか指定できなくなりました。
これでモンスターは入ってこれません。
実はこの記述には、もうひとつの意味があります。制限を課したことで、「少なくとも T で入ってくるのは Human である」ということです。
これにより、以下のような記述を行うことができます。

class HotelRoom<T> where T : Human
{
    private T customer;

    public void CheckIn(T someone)
    {
        this.customer = someone;
    }

    public string RoomCustomer()
    {
        // customer は Human なので、Name がある
        if (this.customer != null)
            return this.customer.Name;
        return null;
    }

    public bool IsThere()
    {
        return customer != null;
    }
}

これで結構使い道があるかと思います。
他の制限としては

  • 「where T : struct」構造体である。
  • 「where T : class」参照型である。
  • 「where T : new()」引数なしのコンストラクタを持つ。
  • 「where T : [interface]」[interface]を継承する。

等が使えます。*1
Generic は基本的に型宣言で使うことが多いですが、メソッドにも指定することができます。

Dictionary<string, T> getNameMap<T>(T[] args)
{
 ...
}

*1:殆どの場合、クラス制限とインターフェース制限だと思います。