プロパティの型コンバータを用意する(層間型変換オブジェクト その4)

前回までは、二つの名前の同じプロパティを変換する際には、型コンバータが必要と書いた。今回は、型コンバータの中でも面倒だと思われる、コレクションを相互変換するコンバータの実装を考えてみる。

コレクションを相互に変換する際に、考えたパターンは以下の通り。(Tは型パラメタ)

・配列に変換するケース

  ICollection<T> -> T[]
  ICollection    -> T[]
  T[]            -> T[]

・コレクション(リスト)に変換するケース

  ICollection<T> -> IList
  ICollection    -> IList
  T[]            -> IList

ジェネリックコレクション(リスト)に変換するケース

  ICollection<T> -> IList<T>
  ICollection    -> IList<T>
  T[]            -> IList<T>

あとは、辞書をサポートする必要があるだろう。

・ディクショナリに変換するケース

  IDictionary -> IDictionary<K, V>
  IDictionary -> IDictionary

ジェネリックディクショナリに変換するケース

  IDictionary<K, V> -> IDictionary<K, V>
  IDictionary       -> IDictionary<K, V>

コレクションに変換するケースの変換先が、ICollection()ではなくIList(なのは、ICollectionがimmutableなためだ。
型コンバータはIPropertyConverterを実装するクラスとして起こすが、今回は、他のコレクションを配列に変換するArrayConverterクラスを、サンプルとして書いてみることにする。
ただ実装するだけではつまらないので、配列の要素の型を特定できるように、変換する配列の型パラメタを指定するジェネリックなクラスで書いてみた。

public class ArrayConverter<T> : IPropertyConverter
{
    public void Convert(object source, ref object dest, Type expectType)
    {
        if (dest == null)
        {
            dest = new T[0];
        }
        T[] array = dest as T[];
        Array.Clear(array, 0, array.Length);

        if (source is ICollection<T>)
        {
            if ((source as ICollection<T>).Count > array.Length)
            {
                array = new T[(source as ICollection<T>).Count];
                dest = array;
            }
            (source as ICollection<T>).CopyTo(array, 0);
        }
        else if (source is ICollection)
        {
            if ((source as ICollection).Count > array.Length)
            {
                array = new T[(source as ICollection).Count];
                dest = array;
            }
            if (source.GetType().IsArray)
            {
                if (source is T[])
                {
                    (source as T[]).CopyTo(array, 0);
                }
                else
                {
                    Type elementType = source.GetType().GetElementType();
                    if (typeof(T).IsAssignableFrom(elementType))
                    {
                        (source as Array).CopyTo(array, 0);
                    }
                }
            }
            else
            {
                int i = 0;
                foreach (object item in (source as ICollection))
                {
                    if (item.GetType().IsAssignableFrom(typeof(T)))
                    {
                        array.SetValue(item, i);
                        i++;
                    }
                }
            }
        }
    }
}

相変わらずベタな実装なので見苦しい点はご容赦願いたい。ジェネリックなクラスにしたことにより、第3パラメタのexpectTypeが不要になってしまうことが判る。

このように書いて未だに解せないのは、やはり ジェネリックなICollectionだろう。本当はトップレベルのifの条件はICollectionが真かを検査して、その後に詳細なコレクションの種類を判別したいのだが、ICollectionはICollectionから派生している訳でも、実装している訳でもないので、このように、わざわざ別に記述する必要があった。現実にはICollectionを実装しているクラスの殆どはICollectionも実装するので、意味が無いのかもしれない。

このジェネリックなコンバータのインスタンスを生成するには、

IPropertyConverter arrayConverter = new ArrayConverter<string>();

と、直接型パラメタを書いたクラスをnewするか、型パラメタを実行時に明示する必要がある。

Type converterType = typeof(ArrayConverter<>).MakeGenericType(new Type[] { typeof(string) });
IPropertyConverter arrayConverter = Activator.CreateInstance(converterType);

ジェネリックなクラスはこのような用途の場合、まさに「テンプレート」となる。
今回のDataExchangeクラスでは、不特定なプロパティの型を扱うので、当然後者を使うことになるだろう。