C#学习笔记(二)---在C#中创建类(下)

在C#中创建类(下)

接口

  • 接口和类相似,但接口成员只提供了定义而不提供实现。他和类的不同之处有:
    ①接口的成员都是抽象的。相反,类包含了抽象的成员和具体实现的成员。
    ②类只能继承一个类,但可以继承多个接口。而结构体不能继承(只能从System.ValueType)。

  • 接口只提供了成员的定义,也就是说成员都是隐含抽象的。接口的成员只能包括属性、方法、事件、索引器。而这些成员都是可以在类中被定义为抽象的。
    下面是System.Colleciton中定义的IEnumerator:

public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
  • 接口的成员总是被定义为public的,并且不能用访问修饰符进行改变。实现接口意味着为其所有成员提供public实现。
internal class Countdown : IEnumerator
{
int count = 11;
public bool MoveNext () { return count-- > 0 ; }
public object Current { get { return count; } }
public void Reset() { throw new NotSupportedException(); }
}

可以把对象转换为任意一个它实现了的接口类型:

IEnumerator e = new Countdown();
while (e.MoveNext())
Console.Write (e.Current); // 109876543210

提示:尽管Countdown是internal的,但是如果将它的对象转换为其实现的任意Interface,则它提供实现的Interface方法都会被作为public来调用。例如:如果同程序集中定义了一个方法:

public static class Util
{
public static object GetCountDown()
{
return new CountDown();
}
}

另一个程序集的调用者可以执行:

IEnumerator e = (IEnumerator) Util.GetCountDown();
e.MoveNext();

如果IEnumerator被定义为internal的,则不能进行这样的调用。

扩展接口

  • 接口可以从其他接口派生。一个接口(A)从另一个接口(B)派生,则A必须实现上B的所有成员。一个类要实现A,则它还得实现B的成员。

接口的显式实现

  • 当实现多个接口时,有时成员标识符会造成冲突,显式实现成员可以解决冲突。
interface I1 { void Foo(); }
interface I2 { int Foo(); }
public class Widget : I1, I2
{
public void Foo ()
{
Console.WriteLine ("Widget's implementation of I1.Foo");
}
int I2.Foo()
{
Console.WriteLine ("Widget's implementation of I2.Foo");
return 42;
}
}

在I1和I2中都有Foo标识符。Widget实现了这两个接口,在该类中同时存在两个Foo标识符,而且参数都一样,解决这个问题就是用接口的显式实现。调用显式实现的成员只能通过接口:

Widget w = new Widget();
w.Foo(); // Widget's implementation of I1.Foo
((I1)w).Foo(); // Widget's implementation of I1.Foo
((I2)w).Foo(); // Widget's implementation of I2.Foo

另一个使用显式实现接口成员的原因是隐藏那些可能造成和类的用法有很大差异或会造成严重干扰的成员。例如:实现ISerializible接口的成员,通常会需要强调避免它的ISerializible成员,除非用接口类型的引用来调用这个成员。

虚方法实现接口成员

  • 通常情况下,接口成员被默认声明为sealed,为了能重写(override),必须在基类中将实现的方法定义为virtual或者abstract。
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
public virtual void Undo()
{
Console.WriteLine ("TextBox.Undo");
}
}
public class RichTextBox : TextBox
{
public override void Undo()
{
Console.WriteLine ("RichTextBox.Undo");
}
}

不管是通过基类还是子类或者是接口调用,调用到的都是子类的实现(因为调用virtual方法会调用继承链上override的最远的方法。)。

RichTextBox r = new RichTextBox();
r.Undo(); // 子类的
((IUndoable)r).Undo(); // 调用的是子类的
((TextBox)r).Undo(); // 还是子类的

在子类中重新实现接口

  • 在子类中可以重新实现接口的成员,不管基类中实现的成员是不是virtual的。当通过接口引用调用时,重新实现都能够屏蔽virtual成员或非virtual成员的实现(而不是virtual方法调用的规则)。它也不管成员是隐式的还是显式的都有效。后者效果更好。注意上面的标黑的部分,是通过接口调用。
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
void IUndoable.Undo() { Console.WriteLine ("TextBox.Undo"); }
}
public class RichTextBox : TextBox, IUndoable
{
public new void Undo() { Console.WriteLine ("RichTextBox.Undo"); }
}

从接口调用成员的重新实现时,调用的是子类的实现:

RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo Case 1
((IUndoable)r).Undo(); // RichTextBox.Undo Case 2

假定RichTextBox的定义不变,如果TextBox隐式实现接口成员:

public class TextBox : IUndoable
{
public void Undo() { Console.WriteLine ("TextBox.Undo"); }
}

这样,为我们提供了另一种调用Undo()方法:

RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo Case 1
((IUndoable)r).Undo(); // RichTextBox.Undo Case 2
((TextBox)r).Undo(); // TextBox.Undo

最后一个代码段说明了子类重新实现的方法屏蔽功能仅当通过接口调用时是有效的。
关于Undo()方法(虚方法的调用规则)的调用,还有一个需要说明的是,区别于子类重新实现接口,如果这里没有接口的存在,则关于虚方法的调用还是遵循这样的规则:就是调用虚方法会调用继承链上虚的最远的。举例说明:

 class  Animal
        {
            public virtual void Voice()
            {
                Console.WriteLine("Animal's voice");
            }
        }

        class Bird:Animal
        {
            public override void Voice()
            {
               Console.WriteLine("Bird's voice");
            }
        }

        class Sparrow : Bird
        {
            public override void Voice()
            {
               Console.WriteLine("Sparrow's voice");
            }
        }
         Sparrow sparrow=new Sparrow();
            sparrow.Voice();//麻雀的叫声
            Animal animal = sparrow;
            animal.Voice();//麻雀的叫声
            Console.ReadKey();

这里说的继承链就是指Sparrow sparrow=new Sparrow();在这个语句中存在Animal、Bird、和Sparrow三个存在继承关系的类。不管是通过Animal还是通过其子类来调用,都是调用virtual的最远的(Sparrow上的voice)。如果做如下更改:

class  Animal
        {
            public virtual void Voice()
            {
                Console.WriteLine("Animal's voice");
            }
        }

        class Bird:Animal
        {
            public override void Voice()
            {
               Console.WriteLine("Bird's voice");
            }
        }

        class Sparrow : Bird
        {
            public new void Voice()
            {
               Console.WriteLine("Sparrow's voice");
            }
        }
         Sparrow sparrow=new Sparrow();
            sparrow.Voice();//麻雀的叫声
            Animal animal = sparrow;
            animal.Voice();//bird的叫声
            Console.ReadKey();

上面的代码把麻雀的叫声用new修饰了而不是override,这样,通过调用,发现animal.Voice()发出的是Bird的voice。还是一样,遵循了虚方法的调用规则。即调用继承链上调用的最远的。通过上面的分析,我们可以得出,在实际的编码过程中,当我们定义virtual方法时,要特别的小心谨慎,因为我们不知道在子类中重写virtual方法时,会发生什么,或者,为了安全,我们的关键代码不能放在virtual方法中去执行。

接口重新实现的替代方法

  • 即使在接口的显式重新实现中,还是会出现以下问题:
    ①子类无法调用基类的方法
    ②定义方法时不能预测方法是否会被重新实现。可能不允许这个潜在的功能。
    重新实现时未知子类时最不理想的方法,更好的选择设计一个永远不需要重新实现的基类。有两种方法可以做到:
    ①如果合适的话,在隐式实现一个接口成员时将其标记为virtual
    ②当显式的实现一个接口成员时,如果你(设计者)认为子类的设计者可能会重写成员的逻辑,用以下模式:
public class TextBox : IUndoable
{
void IUndoable.Undo() { Undo(); } // Calls method below
protected virtual void Undo() { Console.WriteLine ("TextBox.Undo"); }
}
public class RichTextBox : TextBox
{
protected override void Undo() { Console.WriteLine("RichTextBox.Undo"); }
}

当然,如果不需要添加子类,可将类直接设计成sealed。

写类和写接口的对比

  • 当能自然的共享实现时,用类和子类
  • 当实现是独立的,用接口
    举个例子:当我们模拟生活中的事务时,比如爷爷、儿子、孙子这种很自然的人类关系时,用类和子类来模拟,因为类中可以添加表示事务或人类的数据(字段),而接口则不能,如果是具体到某一个事务的某一个功能,比如说说话(吼叫)等,可以用接口将这个功能”拿“出来,做成接口,接口相当于订立了一个契约,实现接口的类按照接口的契约去完成接口规定的功能。

枚举

  • 枚举类型是一种特殊的数值类型,可以在枚举类型中定义一组命名的数值常量。
public enum BorderSide { Left, Right, Top, Bottom }

默认情况下,每个枚举成员对应的数值为int型,从0开始索引(自动)。可以指定其他整数类型代替默认类型。也可以自定义的为每个枚举成员指定数值。

public enum BorderSide:byte { Left=1, Right=6, Top=8, Bottom=10 }
  • 枚举类型转换:枚举类型的实例可以和它对应的数值类型进行相互的转换(显式)。
int i = (int) BorderSide.Left;
BorderSide side = (BorderSide) i;
bool leftOrRight = (int) side <= 2;
  • 枚举类型之间可以进行转换
public enum HorizontalAlignment
{
Left = BorderSide.Left,
Right = BorderSide.Right,
Center
}
  • 在枚举类型的表达式中,编译器对0进行特殊处理,不用进行显式的转换。
BorderSide b = 0; // No cast required
if (b == 0) ...

编译器需要对0进行特别处理的原因:
①第一个枚举成员总是被用作默认值。
②在合并枚举类型中,0表示不标志类型。

  • 标志枚举类型:用FlagsAttribute特性可以将枚举类型设置为可进行位运算的(成员可以合并的)。如果没有指定FlagsAttribute特性,则虽然枚举成员仍然可以合并,但是调用ToString方法显示的是数值而非命名。要进行合并的枚举必须显示的为成员指定值,一般来说是0,1,2,4,8这种的(2的N次方)。
  • 枚举运算符:枚举类型可以使用的运算符有:
    = == != < > <= >= + - ^ & | ˜ += -= ++ – sizeof
    位运算符、算数运算符、和比较运算符都可以返回对应整型值的运算结果。枚举类型可以和整型值做加法运算,但枚举类型之间不可以做加法运算。
  • 类型安全问题
BorderSide b = (BorderSide) 12345;
Console.WriteLine (b);

上例可以反映的问题是,虽然枚举的真实值已经超出了枚举的数值范围,但是程序没有报错,并打印出了这个整型值。
位操作和算数运算也会产生非法值:

BorderSide b = BorderSide.Bottom;
b++; // 不会报错

不合法的枚举值会破坏程序:

void Draw (BorderSide side)
{
if (side == BorderSide.Left) {...}
else if (side == BorderSide.Right) {...}
else if (side == BorderSide.Top) {...}
else {...} // 这里的语句把BorderSide的值当成了BorderSide.Bottom
}

其中一个解决方案是在加上一个else语句:

...
else if (side == BorderSide.Bottom) ...
else throw new ArgumentException ("Invalid BorderSide: " + side, "side");

另一个解决方案是显示的检查一个值的合法性,Enum.IsDefined具有这个功能。

BorderSide side = (BorderSide) 12345;
Console.WriteLine (Enum.IsDefined (typeof (BorderSide), side)); // False

但是,Enum.IsDefined对标志枚举不起作用。然而下面的方法(使用ToString)可以在标志枚举类型合法时返回true。

static bool IsFlagDefined (Enum e)
{
decimal d;
return !decimal.TryParse(e.ToString(), out d);
}
[Flags]
public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }
static void Main()
{
for (int i = 0; i <= 16; i++)
{
BorderSides side = (BorderSides)i;
Console.WriteLine (IsFlagDefined (side) + " " + side);
}
}

嵌套类型

嵌套类型是一种在其他类型声明的类型:

public class TopLevel
{
public class Nested { } // Nested class
public enum Color { Red, Blue, Tan } // Nested enum
}
  • 一个嵌套类型具有以下功能:
    ①它能够访问嵌套它的类型的私有的部分和其他任何他能访问的部分。
    ②可以使用所有的访问修饰符进行修饰,而不仅限于public和internal
    ③嵌套类型的默认访问权限是private而不是internal
    ④从外层类以外的地方访问嵌套类,需要使用嵌套类型的限定名(TopLevel.Nested)。
  • 所有类型都可以被嵌套,但只有类和结构才可以嵌套其他类型。
    提示:如果使用嵌套类型的一个主要原因,是要避免一个命名空间中类型定义的杂乱无章,那更好的解决办法是使用嵌套的命名空间,所以,使用嵌套类型的一个重要的原因,是因为嵌套类型有较强的访问能力,以及要必须从内层空间访问外层空间的私有类型。

泛化

  • C#能书写跨类型复用的代码,有两个不同的支持机制:继承和泛化。继承的复用机制来自基类,而泛化的复用机制是通过“占位符”类的“模板”。与继承相比,泛化能够提高类型的安全性以及减少类型的转换和装箱。C#的泛化和C++的模板具有相似的概念,但实现的方法不同。
  • 泛型:泛型中声明类型参数(也叫泛型参数),也叫占位符类型。他由使用者填充,下面是一个泛型类Stack用于在栈中存放T类型的实例。Stack演示的是一种LIFO(last in first out).
public class Stack
{
int position;
T[] data = new T[100];
public void Push (T obj) { data[position++] = obj; }
public T Pop() { return data[--position]; }
}

我们像这样使用Stack:

Stack<int> stack = new Stack<int>();
stack.Push(5);
stack.Push(10);
int x = stack.Pop(); // x is 10
int y = stack.Pop(); // y is 5

Stack用类型参数int填充Stack并将一个开放的泛型变更位一个闭合的泛型,并在运行时自动创建一个类。也就是说,在运行时,所有的泛型类都是闭合的,例如:

var Stack<t>=new Stack<t>();//这种声明是错误的。
  • 为什么存在泛化:泛化的存在是为了解决代码的复用的,加入存在一个Stack,我们必须为用到的所有类型分别创建一个Stack,比如intStack,stringStack等等。另一个解决方法是使用object:
public class ObjectStack
{
int position;
object[] data = new object[10];
public void Push (object obj) { data[position++] = obj; }
public object Pop() { return data[--position]; }
}

但是objectStack类不会像硬编码的intStack那样只处理整型类型,另外,objectStack会用到装箱转换和向下类型转换,这些都不会在编译时进行检查

//假设我们只想存储整型值:
ObjectStack stack = new ObjectStack();
stack.Push ("s"); // 类型错误,但不会报错
int i = (int)stack.Pop(); // 向下转换- 运行时错误

我们需要的Stack是既能对各种不同元素类型进行支持,又能很容易的限定Stack的元素为特定类型,以提高类型的安全性和减少类型向下转换和装箱转换。泛化恰好通过参数化元素类型,提供了这些功能。Stack具有intStack和objectStack的所有优点,与objectStack类似,Stack只需要编写一次,就能在所有类型上工作,和intStack的相似处在于,Stack能够排除int类型之外的其他类型,提高了类型的安全性。

  • 泛化方法:泛化方法是指在方法的标识符内声明类型参数。使用泛化方法,许多数学函数都能用一个通用的方法来实现了,下面是交换两个任意类型的值的方法Swap:
static void Swap (ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}

该方法的使用过程如下:

int x = 5;
int y = 10;
Swap (ref x, ref y);

通常在调用泛化方法时,不需要提供参数的类型给泛化方法,编译器会推断出类型参数的类型。如果有歧义,可以通过以下调用:

Swap<int> (ref x, ref y);
  • 在泛型中,只有引入了类型参数的方法才被定义为泛型方法,Stack泛型类中的Pop方法,它只是用了类型参数T来当作返回类型,所以Pop方法不是泛型方法。
  • 只有方法和类可以引入新的类型参数,也就是说只有上述两个可以称为泛型的,其他比如属性、索引器、事件、字段、运算符等都不能被声明为泛型的,但是它们可以参与使用在泛型类或方法中的类型参数T。
public Stack() { } // 不合法
  • 声明类型参数:可以在声明结构、类、方法、接口、委托的时候引入类型参数,其他如属性不能引入类型参数,但可以使用类型参数。
  • 泛型名和泛化方法可以重载,只要类型参数的数量不同即可。下面两个类不会造成冲突:
class A<T> {}
class A {}
...
  • typeof和无绑定泛型:在运行时是不存在开放的泛型类型的,开放泛型类型被编译成程序的一部分而闭合。然而,运行时可能存在无绑定泛型,只用作类对象。C#中唯一指定无绑定泛型的方法是使用typeof关键字:
class A<T> {}
class A {}
...
Type a1 = typeof (A<>); // Unbound type (notice no type arguments).
Type a2 = typeof (A<,>); // Use commas to indicate multiple type args.
  • 泛化的默认值:可以用default关键字来获取类型参数的默认值,引用类型的默认值为null,值类型的默认值为bitwise-zeroing(sets all the bits to zero):
static void Zap (T[] array)
{
for (int i = 0; i < array.Length; i++)
array[i] = default(T);
}
  • 泛化的约束:默认情况下,类型参数可以指定任意类型,在参数类型上运用约束,可以将参数类型指定为某一符合约束的类型:
where T : base-class // 基类约束
where T : interface // 接口约束
where T : class // 引用类型约束
where T : struct // 值类型约束(排除可空类型)
where T : new() // 无参的构造函数约束
where U : T // 裸类型约束

下面的代码中,指定T必须是继承自SomeClass且实现接口Interface1,U必须包含一个无参构造函数

class SomeClass {}
interface Interface1 {}
class GenericClass<T,U> where T : SomeClass, Interface1
where U : new()
{...}

约束可以应用在类和方法的任何类型参数的定义中。

  • 泛型的子类:泛型类和非泛型类一样,都可以用做子类,泛型的子类可以让基类的类型参数保持开放:
class Stack<T> {...}
class SpecialStack<T> : Stack<T> {...}

或者子类也可以用一个实体类型来关闭类型参数:

class IntStack : Stack {...}

泛型子类也可以引入新的类型参数:

class List<T> {...}
class KeyedList<T,TKey> : List<T> {...}
  • 技术上,所有的子类类型参数都是新的。可以说子类关闭了类型参数后,又重新开放。这表明子类可以为重新开放的类型参数赋予更有意义的名称:
class List<T> {...}
class KeyedList : List {...}
  • 静态数据:对每个类来说,静态数据是全局唯一的:
class Bob<T> { public static int Count; }
class Test
{
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
}
}
  • 类型参数的转换:C#的类型转换运算符可以进行多种转换:
    ①值类型的转换
    ②引用类型的转换
    ③装箱/拆箱的转换
    ④自定义类型的转换(利用运算符重载)
    根据原有类型,在编译时决定进行何种类型的转换,对于泛型,因为编译器在编译时不知道类型参数的具体类型,在转换上具有更有趣的语义。如果导致歧义,将抛出异常或产生一个错误。
    最常见的是实现一个引用类型的转换:
StringBuilder Foo<T> (T arg)
{
if (arg is StringBuilder)
return (StringBuilder) arg; // Will not compile
...
}

因为不知道T的确切类型,编译器会认为这是一个自定义转换。最简单的方法就是通过as运算符,因为它不能进行自定义转换。所以不能产生歧义。

StringBuilder Foo<T> (T arg)
{
StringBuilder sb = arg as StringBuilder;
if (sb != null) return sb;
...
}

更一般的做法是,先将类型参数转换为object,这种方法能实现,是因为转换自或转换到object类型被认为是引用类型转换或装箱/拆箱转换。下面的例子中,从object转换到StringBuilder,肯定是引用转换了。

return (StringBuilder) (object) arg;

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

int Foo (T x) { return (int) x; } // 编译时将产生错误

下面的方法将先把x转换成object,然后在转换为int:很明显这是一个拆箱转换。

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

协变

  • 协变:先给出协变的定义:假定S是B的子类,如果X=X那么就可以说X是协变的。
    提示:C#中“可改变”表示可以通过隐式的引用转换进行的改变,数值、装拆箱、自定义转换都不包含在内。
    换句话说,如果下面的赋值成立,那么就说IFoo是协变的类:
IFoo s = ...;
IFoo b = s; 
  
  • C#的泛型接口和泛型委托支持协变(和逆变)。但泛型类不支持。数组也支持(由于历史原因,这是一个垃圾的存在,稍后会讨论)
  • 为了保证静态类的安全性,泛型类不是协变的,看下面的例子:
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
public class Stack // A simple Stack implementation
{
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; // Compile-time error

原因是这样做避免了下面的错误:

animals.Push (new Camel()); // 试图将camel放到bear的Stack中

Push方法这样定义:

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

data[]数组在Stack定义时已经确定这是一个bear的数组,如果将camel放进去,将导致错误。
- 但是,协变性的缺失可能妨碍复用性。假定下例中,我们想写一个Wash方法来操作整个Animal的Stack:

public class ZooCleaner
{
public static void Wash (Stack animals) {...}
}

如果调用Wash方法Stack,会导致编译时错误。解决办法时,重新定义一个带有约束的Wash方法:

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

这样,我们也可以用这个方法来操作Stack了。

Stack<Bear> bears = new Stack<Bear>();
ZooCleaner.Wash (bears);
  • 数组:C#1.0的推出是为了能够创建一种与当时java功能类似的编程语言,所以,当时的C#将java的很多东西都照搬了过来。尽管现在的C#不论在更新的速度上还是其整体的功能上都与java不可同日而语,但是初期的C#沿袭了java的很多糟粕,比如数组的协变:
Bear[] bears = new Bear[3];
Animal[] animals = bears; // 编译时是通过的。

这种可复用性可能导致运行时错误:

animals[0] = new Camel(); // Runtime error
  • 接口:在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. 可以将其安全的转换为 IPoppable:
IPoppable<Animal> animals = bears; // 合法的
Animal a = animals.Pop();

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

  • 如前所述,我们可以利用接口的协变来解决代码的复用问题:
public class ZooCleaner
{
public static void Wash (IPoppable animals) { ... }
}

定义了如上方法,我们既可以传入接口,也可以传入任意一个实现了接口的类,还可以传入支持协变的Stack等。同时还能够保证是类型安全的—如果在输入的位置输入标记为out的类型参数,则会产生编译错误。
错误

  • 不管是接口还是数组,协变只针对引用的转换有效,而对装箱的转换无效,例如,
    IPopable可以转换为IPopable,但是不能转换为IPopable

    逆变

    • 逆变:先给出逆变的定义:相对于协变,能反方向转换的类是逆变的——如果X=X(假定S是B的子类)。将类型参数修饰为in,泛型接口将支持逆变,in修饰符保证类型参数只能出现在输入的位置。扩展前面的实例,假定Stack类实现了如下接口:
    public interface IPushable<in T> { void Push (T obj); }

    下面的语句是合法的:

    IPushable<Animal> animals = new Stack<Animal>();
    IPushable<Bear> bears = animals; // Legal
    bears.Push (new Bear());

    in修饰符确保类型参数不能出现在输出位置,IPusable中没有成员的输出类型是T,所以不会出现由animal转换为bear的结果。(但是通过这个接口不能实现Pop方法)
    提示Stack类即可以实现Pushable 也可以实现 IPoppable,尽管T在这两个接口中具有不同的语义。他可以实现的原因是,你只能通过一个接口实现一个转换。这也说明了为什么将类定义为可变的(指协变逆变)是没有意义的:实体类需要数据在两个不同的方向(输入和输出)进行传递。
    再来看一个例子,下面的接口是.NET框架中的定义:

    public interface IComparer<in T>
    {
    // Returns a value indicating the relative ordering of a and b
    int Compare (T a, T b);
    }

    因为这个接口是逆变的,我们可以使用IComparer来比较两个string:

    var objectComparer = Comparer<object>.Default;
    // objectComparer implements IComparer
    IComparer<string> stringComparer = objectComparer;
    int result = stringComparer.Compare ("Brett", "Jemaine"); 
      

    如果将逆变的类型参数放在输出位置,编译器会报错。
    本章完

    你可能感兴趣的:(c#语言基础)