2005年C#の旅その4

C#2.0の目玉の一つはなんと言ってもGenericsです。色々なところで話題になったので今更取り上げるネタもあまり無いのですが、その中でも影の薄そうなところをピックアップしてみます。(^^;

まずは、簡単なGenericsの例です。

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ConsoleApplication1
{
    // T型の固定長配列
    class FixedArray<T>
    {
        private T data;

        // コンストラクタで配列のサイズを指定する
        public FixedArray(int size)
        {
            data = new T[size];
        }

        public T this[int index]
        {
            get { return data[index]; }
            set { data[index] = value; }
        }

        public int Length
        {
            get { return data.Length; }
        }
    }

    class Program
    {
        static void Main(string args)
        {
            FixedArray<int> fai = new FixedArray<int>(3);

            for (int i = 0; i < fai.Length; ++i)
                Console.WriteLine(fai[i].GetType());

            Console.ReadLine();
        }
    }
}

/* 結果
System.Int32
System.Int32
System.Int32*/

FixedArrayの型がパラメータ化されているので、intやlong、doubleなどでそれぞれクラスを作る必要が無くなります。ところで次のような場合は、どうなりますでしょうか?

class Program
{
    static void Main(string[] args)
    {
        // 参照型であるStringBuilderを指定してみる
        FixedArray<StringBuilder> faa = new FixedArray<StringBuilder>(3);

        for (int i = 0; i < faa.Length; ++i)
            Console.WriteLine(faa[i].GetType());

        Console.ReadLine();
    }
}

FixedArrayの型をStringBuilderにして実行すると例外が発生してしまいます。これは、StringBuilderが参照型なので配列要素の初期値がnullとなるのでGetType()の呼び出しが問題になるわけです。つまり、Genericsを使う場合には値型/参照型を意識する必要があります。これに対するアプローチを2つほどあげてみます。一つは値型/参照型両方で動くようにすることです。

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ConsoleApplication1
{
    // T型の固定長配列
    class FixedArray<T> where T : new()
    {
        private T data;

        // コンストラクタで配列のサイズを指定する
        public FixedArray(int size)
        {
            data = new T[size];
            // 参照型の場合default(T)はnullを返す
            if (default(T) == null)
                for (int i = 0; i < data.Length; ++i)
                    data[i] = new T();
        }

        public T this[int index]
        {
            get { return data[index]; }
            set { data[index] = value; }
        }

        public int Length
        {
            get { return data.Length; }
        }
    }

    class Program
    {
        static void Main(string args)
        {
            // 値型
            FixedArray<int> fai = new FixedArray<int>(3);
            for (int i = 0; i < fai.Length; ++i)
                Console.WriteLine(fai[i].GetType());

            // 参照型
            FixedArray<StringBuilder> faa = new FixedArray<StringBuilder>(3);
            for (int i = 0; i < faa.Length; ++i)
                Console.WriteLine(faa[i].GetType());

            Console.ReadLine();
        }
    }
}

/* 結果
System.Int32
System.Int32
System.Int32
System.Text.StringBuilder
System.Text.StringBuilder
System.Text.StringBuilder*/

ポイントは2つで、1つは型パラメータTが値型なのか参照型なのかを判断することです。これは、defaultキーワードを使って次のように出来ます。

if (default(T) == null)

また、参照型の場合オブジェクトを生成する必要がありますが、new T()とするためには、Tがデフォルトコンストラクタを持っていることを保証しなければなりません。

class FixedArray<T> where T : new()

このように型にwhereで制約を掛けるときにnew()とするとデフォルトコンストラクタを持つ型以外を型パラメータとして指定できなくなります。


もう一つのアプローチは、型パラメータTに値型以外指定できないようにすることです。

class FixedArray<T> where T : struct

と、すれば参照型について悩む必要は無くなります。ちなみに参照型だけ指定可能にするには

class FixedArray<T> where T : class

と、します。