ジェネリック型の分散 (C# プログラミング ガイド)

更新 : 2007 年 11 月

C# にジェネリックが追加されたことによる主な利点の 1 つは、System.Collections.Generic 名前空間の型を使用して、厳密に型指定されたコレクションを容易に作成できることです。たとえば、 List<int> 型の変数を作成した場合、コンパイラでこの変数へのすべてのアクセスをチェックできるため、コレクションには int だけを追加できます。この点は、型指定のないコレクションを使用する C# Version 1.0 と比べ、使いやすさが大幅に向上しています。

残念ながら、厳密に型指定されたコレクションにも欠点があります。たとえば、厳密に型指定された List<object> を使用している場合に、List<int> のすべての要素を List<object> に追加する必要が生じたとします。次の例のようにコードを作成することもできます。

List<int> ints = new List<int>();
ints.Add(1);
ints.Add(10);
ints.Add(42);
List<object> objects = new List<object>();

// doesnt compile ints is not a IEnumerable<object>
//objects.AddRange(ints); 

この場合、IEnumerable<int> でもある List<int> を IEnumerable<object> として扱う必要があります。int は object に変換できるため、この点は特に問題がないように思えます。これは string[] を object[] として扱うことができるのと非常に似ており、この処理は現在は可能です。このような場合に必要となる機能をジェネリックの分散といいます。ジェネリックの分散では、ジェネリック型のインスタンス化 (このケースでは IEnumerable<int>) を同じ型の別のインスタンス化 (このケースでは IEnumerable<object>) として扱います。

C# はジェネリック型の分散をサポートしていないため、このような処理が必要になった場合、いくつかの技法のいずれかを使用してこの問題を回避する必要があります。最も単純なケース、たとえば上記の例の AddRange のように単一のメソッドを使用する場合は、単純なヘルパー メソッドを宣言して自動的に変換を行うことができます。たとえば、次のようなメソッドを記述できます。

// Simple workaround for single method
// Variance in one direction only
public static void Add<S, D>(List<S> source, List<D> destination)
    where S : D
{
    foreach (S sourceElement in source)
    {
        destination.Add(sourceElement);
    }
}

このメソッドを使用すると、次の処理が可能になります。

// does compile
VarianceWorkaround.Add<int, object>(ints, objects);

この例には、単純な分散の回避策の特性がいくつか示されています。このヘルパー メソッドは、変換元と変換先の 2 つの型パラメータを受け取ります。変換元の型パラメータ S は、変換先の型パラメータが D に制約されています。つまり、読み込み元の List<> には、挿入先の List<> の要素型に変換可能な要素が含まれている必要があります。これによりコンパイラは確実に int を object に変換できるようになります。型パラメータを別の型パラメータから強制的に派生させることを、生の型パラメータ制約といいます。

単一のメソッドを定義することによって分散の問題を回避する方法は、ある程度まで有効ですが、残念なことに、分散の問題は短時間のうちに複雑化する場合があります。あるインスタンス化のインターフェイスを別のインスタンス化のインターフェイスとして扱おうとした場合に、さらに複雑になります。たとえば、IEnumerable<int> を IEnumerable<object> だけを受け取るメソッドに渡す必要があるような場合です。この場合も、IEnumerable<object> を一連の object、IEnumerable<int> を一連の ints であると見なすことができるため、問題はないように思えます。ints は object であるため、一連の ints は一連の object として扱うことができると推測されます。次に例を示します。

static void PrintObjects(IEnumerable<object> objects)
{
    foreach (object o in objects)
    {
        Console.WriteLine(o);
    }
}

これは、次の例のように呼び出す必要があります。

// would like to do this, but cant ...
// ... ints is not an IEnumerable<object>
//PrintObjects(ints);

インターフェイスの代替手段として、このインターフェイスの各メンバについて変換を実行するラッパー オブジェクトを作成します。これは次の例のようになります。

// Workaround for interface
// Variance in one direction only so type expressinos are natural
public static IEnumerable<D> Convert<S, D>(IEnumerable<S> source)
    where S : D
{
    return new EnumerableWrapper<S, D>(source);
}

private class EnumerableWrapper<S, D> : IEnumerable<D>
    where S : D
{

このメソッドを使用すると、次の処理が可能になります。

PrintObjects(VarianceWorkaround.Convert<int, object>(ints));

この場合も、ラッパー クラスとヘルパー メソッドにおける生の型パラメータ制約に注目してください。この処理はかなり複雑なものになりますが、ラッパー クラスのコードは非常に単純です。つまり、ラップされたインターフェイスのメンバに処理を渡すだけで、単純な型変換以外には何も行いません。コンパイラで直接 IEnumerable<int> から IEnumerable<object> への変換を行わない理由はどこにあるのでしょうか。

分散は、コレクションの読み取り操作だけを行う場合はタイプ セーフですが、読み取り操作と書き込み操作の両方を伴う場合はタイプ セーフではなくなります。たとえば、IList<> インターフェイスは自動的に処理できない場合があります。タイプ セーフになるように、IList<> のすべての読み取り操作をラップするヘルパーを記述することは可能ですが、読み取り操作はそれほど簡単にはラップできません。

IList<T> インターフェイスの分散を処理するためのラッパーの一部を次に示します。読み取りと書き込みの双方向において分散に関係する問題が生じることがわかります。

private class ListWrapper<S, D> : CollectionWrapper<S, D>, IList<D>
    where S : D
{
    public ListWrapper(IList<S> source) : base(source)
    {
        this.source = source;
    }

    public int IndexOf(D item)
    {
        if (item is S)
        {
            return this.source.IndexOf((S) item);
        }
        else
        {
            return -1;
        }
    }

    // variance the wrong way ...
    // ... can throw exceptions at runtime
    public void Insert(int index, D item)
    {
        if (item is S)
        {
            this.source.Insert(index, (S)item);
        }
        else
        {
            throw new Exception("Invalid type exception");
        }
    }

ラッパーの Insert メソッドに問題があります。このメソッドは引数として D を受け取りますが、これを IList<S> に挿入する必要があります。D は S の基本型であるため、すべての D が S であるとは限りません。したがって、Insert 操作は失敗します。この例は配列の分散と似ています。オブジェクトを object[] に挿入する場合、object[] が実際には string[] である可能性が実行時にはあるため、動的な型チェックが実行されます。次に例を示します。

object[] objects = new string[10];

// no problem, adding a string to a string[]
objects[0] = "hello"; 

// runtime exception, adding an object to a string[]
objects[1] = new object(); 

IList<> の例では、実行時に実際の型が目的の型と一致しない場合は単に Insert メソッドのラッパーをスローできます。ここでも、プログラマに代わってコンパイラが自動的にラッパーを生成すると考えることはできます。しかし、このようなポリシーではうまくいかないケースが存在します。IndexOf メソッドは指定した項目をコレクションで検索し、項目が見つかった場合はコレクションのインデックスを返します。ただし、項目が見つからなかった場合、IndexOf メソッドは -1 を返すだけで、ラッパーはスローされません。この種のラップは、自動的に生成されるラッパーでは提供されません。

ここまでは、ジェネリックの分散に関する問題の回避策として 2 つの単純な方法を説明しました。しかし、分散の問題はいっそう複雑化する可能性があります。たとえば、List<IEnumerable<int>> を List<IEnumerable<object>> として扱ったり、List<IEnumerable<IEnumerable<int>>> を List<IEnumerable<IEnumerable<object>>> として扱ったりする場合などです。

分散の問題を回避するためにコードでこのようなラッパーを生成すると、コードに著しいオーバーヘッドが生じることがあります。また、参照 ID に関連した問題が生じることもあります。これは、各ラッパーが元のコレクションと同じ ID を持たないためで、軽度のバグが発生する可能性があります。ジェネリックを使用する場合は、密に結合されたコンポーネント間での不一致が少なくなるように、型のインスタンス化を選択する必要があります。そのためには、コードのデザインにおいてある程度の妥協が必要になることもあります。当然のことながら、デザインには競合する要件の間でのトレードオフが付き物であり、デザイン プロセスでは当該言語における型システムの制約を考慮に入れる必要があります。

その言語の中核の部分としてジェネリックの分散が含まれるような型システムも存在します。Eiffel などはその主な例です。ただし、型システムの中核の部分としてジェネリックの分散が含まれた場合、C# の型システムは、分散を伴わない比較的単純なシナリオにおいてさえ著しく複雑になってしまいます。その結果、C# の設計では、分散を含めないことが妥当な選択であると考えられました。

上記の例の完全なソース コードを次に示します。

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

static class VarianceWorkaround
{
    // Simple workaround for single method
    // Variance in one direction only
    public static void Add<S, D>(List<S> source, List<D> destination)
        where S : D
    {
        foreach (S sourceElement in source)
        {
            destination.Add(sourceElement);
        }
    }

    // Workaround for interface
    // Variance in one direction only so type expressinos are natural
    public static IEnumerable<D> Convert<S, D>(IEnumerable<S> source)
        where S : D
    {
        return new EnumerableWrapper<S, D>(source);
    }

    private class EnumerableWrapper<S, D> : IEnumerable<D>
        where S : D
    {
        public EnumerableWrapper(IEnumerable<S> source)
        {
            this.source = source;
        }

        public IEnumerator<D> GetEnumerator()
        {
            return new EnumeratorWrapper(this.source.GetEnumerator());
        }

        IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private class EnumeratorWrapper : IEnumerator<D>
        {
            public EnumeratorWrapper(IEnumerator<S> source)
            {
                this.source = source;
            }

            private IEnumerator<S> source;

            public D Current
            {
                get { return this.source.Current; }
            }

            public void Dispose()
            {
                this.source.Dispose();
            }

            object IEnumerator.Current
            {
                get { return this.source.Current; }
            }

            public bool MoveNext()
            {
                return this.source.MoveNext();
            }

            public void Reset()
            {
                this.source.Reset();
            }
        }

        private IEnumerable<S> source;
    }

    // Workaround for interface
    // Variance in both directions, causes issues
    // similar to existing array variance
    public static ICollection<D> Convert<S, D>(ICollection<S> source)
        where S : D
    {
        return new CollectionWrapper<S, D>(source);
    }


    private class CollectionWrapper<S, D> 
        : EnumerableWrapper<S, D>, ICollection<D>
        where S : D
    {
        public CollectionWrapper(ICollection<S> source)
            : base(source)
        {
        }

        // variance going the wrong way ... 
        // ... can yield exceptions at runtime
        public void Add(D item)
        {
            if (item is S)
            {
                this.source.Add((S)item);
            }
            else
            {
                throw new Exception(@"Type mismatch exception, due to type hole introduced by variance.");
            }
        }

        public void Clear()
        {
            this.source.Clear();
        }

        // variance going the wrong way ... 
        // ... but the semantics of the method yields reasonable semantics
        public bool Contains(D item)
        {
            if (item is S)
            {
                return this.source.Contains((S)item);
            }
            else
            {
                return false;
            }
        }

        // variance going the right way ... 
        public void CopyTo(D[] array, int arrayIndex)
        {
            foreach (S src in this.source)
            {
                array[arrayIndex++] = src;
            }
        }

        public int Count
        {
            get { return this.source.Count; }
        }

        public bool IsReadOnly
        {
            get { return this.source.IsReadOnly; }
        }

        // variance going the wrong way ... 
        // ... but the semantics of the method yields reasonable  semantics
        public bool Remove(D item)
        {
            if (item is S)
            {
                return this.source.Remove((S)item);
            }
            else
            {
                return false;
            }
        }

        private ICollection<S> source;
    }

    // Workaround for interface
    // Variance in both directions, causes issues similar to existing array variance
    public static IList<D> Convert<S, D>(IList<S> source)
        where S : D
    {
        return new ListWrapper<S, D>(source);
    }

    private class ListWrapper<S, D> : CollectionWrapper<S, D>, IList<D>
        where S : D
    {
        public ListWrapper(IList<S> source) : base(source)
        {
            this.source = source;
        }

        public int IndexOf(D item)
        {
            if (item is S)
            {
                return this.source.IndexOf((S) item);
            }
            else
            {
                return -1;
            }
        }

        // variance the wrong way ...
        // ... can throw exceptions at runtime
        public void Insert(int index, D item)
        {
            if (item is S)
            {
                this.source.Insert(index, (S)item);
            }
            else
            {
                throw new Exception("Invalid type exception");
            }
        }

        public void RemoveAt(int index)
        {
            this.source.RemoveAt(index);
        }

        public D this[int index]
        {
            get
            {
                return this.source[index];
            }
            set
            {
                if (value is S)
                    this.source[index] = (S)value;
                else
                    throw new Exception("Invalid type exception.");
            }
        }

        private IList<S> source;
    }
}

namespace GenericVariance
{
    class Program
    {
        static void PrintObjects(IEnumerable<object> objects)
        {
            foreach (object o in objects)
            {
                Console.WriteLine(o);
            }
        }

        static void AddToObjects(IList<object> objects)
        {
            // this will fail if the collection provided is a wrapped collection 
            objects.Add(new object());
        }
        static void Main(string[] args)
        {
            List<int> ints = new List<int>();
            ints.Add(1);
            ints.Add(10);
            ints.Add(42);
            List<object> objects = new List<object>();

            // doesnt compile ints is not a IEnumerable<object>
            //objects.AddRange(ints); 

            // does compile
            VarianceWorkaround.Add<int, object>(ints, objects);

            // would like to do this, but cant ...
            // ... ints is not an IEnumerable<object>
            //PrintObjects(ints);

            PrintObjects(VarianceWorkaround.Convert<int, object>(ints));

            AddToObjects(objects); // this works fine
            AddToObjects(VarianceWorkaround.Convert<int, object>(ints));
        }
        static void ArrayExample()
        {
            object[] objects = new string[10];

            // no problem, adding a string to a string[]
            objects[0] = "hello"; 

            // runtime exception, adding an object to a string[]
            objects[1] = new object(); 
        }
    }
}

参照

概念

C# プログラミング ガイド

参照

ジェネリック (C# プログラミング ガイド)