更新:2007 年 11 月

在 C# 中添加泛型的一个主要好处是能够使用 System.Collections.Generic 命名空间中的类型轻松地创建强类型集合。例如,您可以创建一个类型为 List<int> 的变量,编译器将检查对该变量的所有访问,确保只将 List<int> 添加到该集合中。与 C# 1.0 版中的非类型化集合相比,这是可用性方面的一个很大改进。

遗憾的是强类型集合有自身的缺陷。例如,假设您有一个强类型 List<object>,您想将 List<int> 中的所有元素追加到 List<object> 中。您可能希望能够如下面的示例一样编写代码:

C# 复制代码
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); 

在这种情况下,您希望能够将 List<int>(它同时也是 IEnumerable<int>)作为 IEnumerable<object> 处理。这样做看起来似乎很合理,因为 int 可以转换为对象。这与能够将 string[] 当作 object[](现在您就可以这样做)非常相似。如果您正面临这种情况,那么您需要一种称为泛型变化的功能,它将泛型类型的一种实例化(在本例中为 IEnumerable<int>)当成该类型的另一种实例化(在本例中为 IEnumerable<object>)。

由于 C# 不支持泛型类型的变化,所以当遇到这种情况时,您需要尝试几种可能的方法来解决此问题。对于最简单的情况,例如上例中的单个方法 AddRange,您可以声明一个简单的帮助器方法来为您执行转换。例如,您可以编写如下方法:

C# 复制代码
// 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);
    }
}

它使您能够完成以下操作:

C# 复制代码
// does compile
VarianceWorkaround.Add<int, object>(ints, objects);

此示例演示了一种简单的变化解决方法的一些特征。帮助器方法带两个类型参数,分别对应于源和目标,源类型参数 S 有一个约束,即目标类型参数 D。这意味着读取的 List<> 所包含的元素必须可以转换为插入的 List<> 类型的元素。这使编译器可以强制 int 可转换为对象。将类型参数约束为从另一类型参数派生被称为裸类型参数约束。

定义一个方法来解决变化问题不算是一种过于拙劣的方法。遗憾的是变化问题很快就会变得非常复杂。下一级别的复杂性产生在当您想要将一个实例化的接口当作另一个实例化的接口时。例如,您有一个 IEnumerable<int>,您想将它传递给一个只以 IEnumerable<object> 为参数的方法。同样,这样做也是有一定意义的,因为您可以将 IEnumerable<object> 看作对象的序列,将 IEnumerable<int> 看作 ints 的序列。由于 ints 是对象,因此 ints 的序列应当可以被当作对象序列。例如:

C# 复制代码
static void PrintObjects(IEnumerable<object> objects)
{
    foreach (object o in objects)
    {
        Console.WriteLine(o);
    }
}

您可能希望能够如下面的示例一样调用:

C# 复制代码
// would like to do this, but cant ...
// ... ints is not an IEnumerable<object>
//PrintObjects(ints);

接口 case 的解决方法是:创建为接口的每个成员执行转换的包装对象。这可能类似于如下示例:

C# 复制代码
// 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
{

它使您能够完成以下操作:

C# 复制代码
PrintObjects(VarianceWorkaround.Convert<int, object>(ints));

同样,请注意包装类和帮助器方法的裸类型参数约束。此系统已经变得相当复杂,但是包装类中的代码非常简单;它只委托给所包装接口的成员,除了简单的类型转换外,不执行其他任何操作。为什么不让编译器允许从 IEnumerable<int> 直接转换为 IEnumerable<object> 呢?

尽管在查看集合的只读视图的情况下,变化是类型安全的,然而在同时涉及读写操作的情况下,变化不是类型安全的。例如,不能用此自动方法处理 IList<> 接口。您仍然可以编写一个帮助器,用类型安全的方式包装 IList<> 上的所有读操作,但是写操作的包装就不能如此轻松了。

下面是处理 IList<(Of <(T>)>) 接口的变化的包装的一部分,它显示在读和写两个方面的变化所引发的问题:

C# 复制代码
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 当作参数,但是它必须将 D 插入到 IList<S> 中。由于 D 是 S 的基类型,不是所有的 D 都是 S,因此 Insert 操作可能会失败。此示例与数组的变化有相似之处。当将对象插入 object[] 时,将执行动态类型检查,因为 object[] 在运行时可能实际为 string[]。例如:

C# 复制代码
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,而并不引发。这种类型的包装不能由自动生成的包装提供。

到目前为止,我们描述了泛型变化问题的两种最简单的解决方法。然而,变化问题可能变得要多复杂就有多复杂。例如,当您将 List<IEnumerable<int>>当作 List<IEnumerable<object>>,或将 List<IEnumerable<IEnumerable<int>>> 当作 List<IEnumerable<IEnumerable<object>>> 时。

当生成这些包装以解决代码中的变化问题时,可能给代码带来巨大的系统开销。同时,它还会带来引用标识问题,因为每个包装的标识都与原始集合的标识不一样,从而会导致不易察觉的 Bug。当使用泛型时,应选择类型实例化,以减少紧密关联的组件之间的不匹配问题。这可能要求在设计代码时做出一些妥协。与往常一样,设计程序时必须权衡相互冲突的要求,在设计过程中应当考虑语言中类型系统具有的约束。

有的类型系统将泛型变化作为语言的首要任务。Eiffel 是其中一个主要示例。然而,将泛型变化作为类型系统的首要任务会明显增加 C# 的类型系统的复杂性,即使在不涉及变化的相对简单方案中也是如此。因此,C# 的设计人员觉得不包括变化才是 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(); 
        }
    }
}

请参见