[C#]泛型中的强制类型转换

泛型类型中的变化(C# 编程指南)

更新:2007 年 11 月

C# 中添加泛型的一个主要好处是能够使用 System.Collections.Generic 命名空间中的类型轻松地创建强类型集合。例如,您可以创建一个类型为 List<int> 的变量,编译器将检查对该变量的所有访问,确保只将 List<int> 添加到该集合中。与 C# 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); 

在这种情况下,您希望能够将 List<int>(它同时也是 IEnumerable<int>)作为 IEnumerable<object> 处理。这样做看起来似乎很合理,因为 int 可以转换为对象。这与能够将 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);

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

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

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);

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

// 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<(Of <(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 当作参数,但是它必须将 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,而并不引发。这种类型的包装不能由自动生成的包装提供。

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

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

有的类型系统将泛型变化作为语言的首要任务。Eiffel 是其中一个主要示例。然而,将泛型变化作为类型系统的首要任务会明显增加 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();
}
}
}
赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏