接口和类相似,但接口成员只提供了定义而不提供实现。他和类的不同之处有:
①接口的成员都是抽象的。相反,类包含了抽象的成员和具体实现的成员。
②类只能继承一个类,但可以继承多个接口。而结构体不能继承(只能从System.ValueType)。
接口只提供了成员的定义,也就是说成员都是隐含抽象的。接口的成员只能包括属性、方法、事件、索引器。而这些成员都是可以在类中被定义为抽象的。
下面是System.Colleciton中定义的IEnumerator:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
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的,则不能进行这样的调用。
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成员,除非用接口类型的引用来调用这个成员。
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(); // 还是子类的
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方法中去执行。
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
}
BorderSide b = 0; // No cast required
if (b == 0) ...
编译器需要对0进行特别处理的原因:
①第一个枚举成员总是被用作默认值。
②在合并枚举类型中,0表示不标志类型。
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 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>();//这种声明是错误的。
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类型之外的其他类型,提高了类型的安全性。
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方法不是泛型方法。public Stack() { } // 不合法
class A<T> {}
class A {}
...
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.
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
}
}
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; }
X=X
那么就可以说X是协变的。 IFoo s = ...;
IFoo
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);
Bear[] bears = new Bear[3];
Animal[] animals = bears; // 编译时是通过的。
这种可复用性可能导致运行时错误:
animals[0] = new Camel(); // Runtime error
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");
如果将逆变的类型参数放在输出位置,编译器会报错。
本章完