• 沒有找到結果。

写类和写接口的对比

3.8 嵌套类型

3.9.11 类型参数的转换

static void Main() {

Console.WriteLine (++Bob<int>.Count); // 1 Console.WriteLine (++Bob<int>.Count); // 2 Console.WriteLine (++Bob<string>.Count); // 1 Console.WriteLine (++Bob<object>.Count); // 1 }

StringBuilder Foo<T> (T arg) {

if(arg is StringBuilder)

return (StringBuilder) arg; // 编译不通过 ...

}

因为不知道T的确切类型,编译器认为是自定义转换。最简单的解决方法就是改用a s运算符,因为它 不能进行自定义类型转换,所以不会产生二义性:

StringBuilder Foo<T> (T arg) {

StringBuilder sb = arg as StringBuilder;

if(sb!= null) return sb;

...

}

一般的做法是,先转换成object类型。这种方法能够实现,是因为转换自或转换到object类型,被 假定为不是自定义转换而是引用或装箱/拆箱转换。下例中,StringBuilder是引用类型,所以必须 是引用转换:

Return (StringBuilder)(object) arg;

拆箱转换也会产生二义性。下面可能是拆箱转换、数值转换或自定义转换:

int Foo<T> (T x) { return (int)x; } // 编译时错误

在C#中创建类 下面的方法先转换成object再转换成int(很明显这是一个拆箱转换):

int Foo<T> (T x) { return (int)(object)x; }

3.9.12 协变

假定S是B的子类,如果X<S>允许引用转换成X<B>,那么称X为协变类。

提示: 由于C#符号的共变性(和逆变性),所以“可改变”表示可以通过隐式引用转换进行改变——如A是B

的子类,或者A实现B。数字转换、装箱转换和自定义转换都不包含在内。

换句话说,如果下面的语句合法,那么类IFoo<T>是协变类:

IFoo<string> b = ... ; IFoo<object> s = b;

C#4.0中,泛化接口支持协变(泛化委托也支持,见第4章),但泛化类不支持。数组也支持协变(如 果S是B的子类,S[]可以转换成B[]),这里作一些讨论和比较。

提示: 协变性和逆变性(或简称变性)是高级概念。在C#中引入和强化协变的背后动机在于,允许泛化接口

和泛型(尤其是框架中定义的那些,例如IEnumerable<T>)像人们所期待的那样,做更多的工作。

编程人员可以利用协变性和逆变性这些概念的优点,而无需掌握它们背后的细节。

1. 类

为了保证静态类的安全性,泛化类不是协变的。看下面的例子:

class Animal { } class Bear : Animal { } class Camel: Animal { }

public class Stack<T> //简单的栈实现 {

int position;

T[] data = new T[100];

public void Push(T obj) { data[position++] = obj; } public T Pop() { return data[- - position]; } }

下面的语句编译通不过:

Stack<Bear> bears = new Stack<Bear>();

Stack<Animal> animals = bears; // 编译时错误

这个限制避免了下面代码发生运行时错误:

animal.Push(new Camel()); // 试图把Camel类加入Bear栈

但是,协变性的缺失可能妨碍复用性。假定下例中,我们想写一个Wash方法来操作整个Animal栈:

public class ZooCleaner

{

public static void Wash ( Stack<Animal> animals) {...}

}

如果调用Wash方法操作Bear栈,将会导致编译时错误。解决方法是,重新定义一个带有约束的Wash 方法:

class ZooCleaner {

public static void Wash<T> (Stack<T> animals) where T : Animal {...}

}

这样,我们就可以用如下方法调用Wash了:

Stack<Bear> bears = new Stack<Bear>();

ZooCleaner.Wash(bears);

另一种解决方法是让Stack<T>实现一个协变的泛化接口,后面会举出示例。

2. 数组

由于历史原因,数组array类型具有协变性。这表明如果B是A的子类(A和B都是引用类型),那么 B[]可以转换成A[]。例如:

Bear[] bears = new Bear[3];

Animal[] animals = bears; // 正确

这种可复用性的缺点在于,指定元素值可能导致运行时错误:

animal[0] = new Camel(); // 运行时错误

3. 接口

在C#4.0中,泛化接口对用out修饰符标注的类型参数支持协变。和数组不同,out修饰符保证了协变 性的接口是完全类型安全的。为了阐述这一点,假定Stack类实现了下面的接口:

public interface IPoppable<out T> { T Pop(); }

T前的out修饰符是C#4.0的新特性,表明T只用在输出的位置(例如:方法的返回值)。out修饰符将 接口标识为具有协变性的并允许以下操作:

var bears = new Stack<Bear>();

bears.Push (new Bear());

// Bears实现IPoppable<Bear>。我们可以把它转换成IPoppable<Animal>:

IPoppable<Animal> animals = bears; // 合法 Animal a = animal.Pop();

基于接口有协变性的优点,将bears转换成animals是编译器允许的。它是类型安全的,因为编译器 试图避免发生Camel类进栈,因为T仅能在输出的位置出现,所以不可能将Camel类输入接口。

提示: 接口中的协变和逆变的典型应用是使用接口:很少需要向协变性接口写入。确切地说,由于CLR的限

制,为了协变性将方法参数标注为out是不合法的。

在C#中创建类 如前所述,我们可以利用类型转换的协变性解决复用性问题:

public class ZooCleaner {

public static void Wash(IPoppable<Animal> animals) {...}

}

提示: 第7章所讲的IEnumerator<T>和IEnumerable<T>接口在Framwork4.0中都标识为协变的。这样,如果 需要,可以将IEnumerable<string>转换成IEnumerable<object>。

如果在输入的位置使用协变类型参数(例如:方法的参数或可写属性),编译器会产生错误。

提示: 不管泛型还是数组,协变(逆变)仅对引用转换的元素有效而对装箱转换无效。因此,如果写了一

个接收IPoppable<object>类为参数的方法,可以用IPoppable<string>作为参数调用,而不能用 IPoppable<int>。

3.9.13 逆变

前面我们已经知道,如果S是B的子类且X(S)允许引用类型转换到X(B),则类X是协变的。那么能反 方向转换的类是逆变的——从X(B)到X(S)。泛化接口支持逆变当泛型参数只出现在输入的位置,且 被指定了in修饰符时。扩展我们前面的示例,如果Stack<T>类实现了如下接口:

public interface IPushble<in T> { void Push(T obj); } 下面的语句是合法的:

IPushable<Animal> animals = new Stack<Animal>();

IPushable<Bear> bears = animals; // 合法 bears.Push (new Bear());

IPushable中没有成员输出类型为T,所以不会出现将animals转换成bears的问题(但是通过这个 接口不能实现Pop方法)。

提示: Stack<T>类既可以实现IPushable<T>接口也可以实现IPoppable<T>接口——尽管T在两个接口中有 不同的协变注解。它可以实现的原因是,只能从一个接口中激活协变,因此在协变转换之前,必须先 选定IPoppable或IPushable。选定的接口会限制在合适的协变规则下进行合法的操作。

这也说明了为什么将类(如Stack<T>)定义为协变是没有意义的。

再看一个例子,下面的接口是.NET框架中的定义:

Public interface IComparer<in T>

{

// 返回值为a和b的相对顺序 int Compare(T a, T b);

}

因为这个接口是逆变的,我们可以用IComparer<object>比较两个字符串:

var objectComparer = Comparer<object>.Default;

// objectComparer实现了IComparer<object>接口 IComparer<string> stringComparer = objectComparer;

int result = stringComparer.Compare("Brett", "Jemaine");

与之相对的协变,如果将逆变的参数用在输出的位置(例如,作为返回值或在可读属性中),编译器 将会报错。

相關文件