重温CLR(九) 接口
对于多继承(multiple inheritance)的概念,许多程序员并不陌生,他是指一个类从两个或多个基类派生的能力。例如,假定TransmitData类的作用是发送数据,ReceiveData类的作用是接收数据。现在要创建SocketPort类,作用是发送和接收数据。在这种情况下,你会希望SocketPort从TransmitData和ReceiveData这两个类继承。
有的编程语言允许多继承,所以能从transmitData和receiveData这两个基类派生出SocketPort。但clr不支持多继承(因此所有托管编程语言也支持不了)。clr只是通过接口提供了“缩水版”的多继承,就是接口。本章我们讨论如何定义和使用接口,还提供一些指导性原则,以便你判断何时应该使用接口而不是基类。
类和接口继承
在Microsoft.Net Framwork中,有一个名为System.Object的类,它定义了4个公共实例方法:ToString, Equals, GetHashCode和GetType。该类是其他所有类的根或者说最终基类。换言之,所有类都继承了Object的4个实例方法。这还意味着能操作Object类的实例的代码,就能操作任何类的实例。
由于Microsoft的开发团队已实现了object的方法,所以从object派生的任何类实际都继承了一下内容。
方法签名
使代码认为自己是在操作object类的实例,但实际操作的可能是其他类的实例。
方法实现
是开发人员定义object的派生类时不必手动实现object的方法
在CLR中,任何类都肯定从一个类也只能从一个类(而且只能从Objetc派生的类)派生的。这个类称为基类。基类提供了一组方法签名和这些方法的实现。你定义的新类可在将来由其它开发人员用作基类——所有方法签名和方法实现都会由新的派生类继承。
CLR允许开发人员定义接口,它实际只是对一组方法签名进行了统一的命名。这些方法没有提供任何实现。类通过指定接口与名称来实现这个接口,而且必须显式实现接口方法,否则CLR会认为此类型定义无效。当然,实现接口方法的过程可能比较繁琐,所以我才在前面说接口继承是实现多继承的一种缩水版机制。c#编译器和clr允许一个类继承多个接口。当然,继承的所有接口方法都必须实现。
类继承有一个重要特点,凡是能使用基类型实例的地方,都能使用派生类型的实例。类似的,凡是能使用具有接口类型实例的地方,都能使用实现了这个接口的一个类型的实例。
定义接口
接口对一组方法签名进行了统一命名。注意,接口还能定义事件、无参属性和有参属性(c#的索引器)。所有这些东西本质上都是方法,他们只是语法上的简化。不过,接口不能定义任何构造器方法,也不能定义任何实例字段。
虽然clr允许接口定义静态方法、静态字段、常量和静态构造器,但符合cls标准的接口决不允许,因为有的编程语言不能定义或访问它们。事实上,c#禁止接口定义任何一种这样的静态成员。
C#用interface关键字定义接口。要为接口指定名称和一组实例方法签名。下面是fcl中的几个接口定义:
public interface IDisposable {
void Dispose();
}
public interface IEnumerable {
IEnumerator GetEnumerator();
}
public interface IEnumerable<T> : IEnumerable {
new IEnumerator<T> GetEnumerator();
}
public interface ICollection<T>: IEnumerable<T>, IEnumerable
{
void Add(T item);
void Clear();
Boolean Contains(T item);
void CopyTo(T[] array,Int32 arrayIndex)
Boolean Remove(T item);
Int32 Count{get;}//只读属性
Boolean IsReadOnly{get;}//只读属性
}
在clr看来,接口定义就是类型定义。也就是说,clr会为接口类型对象定义内部数据结构,同时可通过反射机制来查询接口类型的功能。和类型一样,接口可在文件范围中定义,也可嵌套在另一个类型中。定义接口类型时,可指定你希望的任何可见性/可访问性(public,protected,internal等)
根据约定,接口类型名称以大写字母I开头,目的是方便在源代码中辨认接口类型。CLR支持泛型接口和接口中的泛型方法。
接口定义可从另一个或多个接口“继承”。但“继承”应打上引号,因为它并不是严格的继承。接口继承的工作方式并不全程和类继承一样,我个人倾向于将接口继承看成是将其他接口协定(contract)包括到新接口中。例如ICollection<T>:接口定义就包含了IEnumerable<T>和IEnumerable两个接口的协定。这有下面两层含义
1 继承ICollection<T>接口的任何类必须实现ICollection<T>,IEnumerable<T>和IEnumerable这三个接口所定义的方法
2 任何代码在引用对象时,如果期待该对象的类实现了ICollection<T>接口,可以认为该类型还实现了IEnumerable<T>和IEnumerable接口。
继承接口
本节介绍如何定义实现了接口的类型,然后介绍如何创建该类型的实例,并用这个对象调用接口方法。c#将这个过程变得很简单,但幕后发生的事情还是有点复杂。
下面是在MSCorLib.dll中定义的System.IComparable<T>接口:
public interface IComparable<in T> {
Int32 CompareTo(T other);
}
以下代码展示类如何定义一个实现类该接口的类型,还展示了对象两个Point对象进行比较的代码:
// Point 从 System.Object 派生并且实现了 IComparable<T>.
public sealed class Point : IComparable<Point>
{
private Int32 m_x, m_y;
public Point(Int32 x, Int32 y)
{
m_x = x;
m_y = y;
}
// 该方法实现了 IComparable<T>.CompareTo()
public Int32 CompareTo(Point other)
{
return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y)
- Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
}
public override String ToString()
{
return String.Format("({0}, {1})", m_x, m_y);
}
}
public static void Go()
{
Point[] points = new Point[] {
new Point(, ),
new Point(, ),
};
//调用了由Point实现的IComparable<T>.CompareTo()方法
if (points[].CompareTo(points[]) > )
{
Point tempPoint = points[];
points[] = points[];
points[] = tempPoint;
}
Console.WriteLine("Points from closest to (0, 0) to farthest:");
foreach (Point p in points)
Console.WriteLine(p);
}
C#编译器要求将用于实现一个接口的方法标记为public。CLR要求将接口方法标记为virtual。如果在源代码中没有显式地将方法标记为virtual,编译器会将它们标记为virtual和sealed;这会阻止派生类重写接口方法。如果显式地将方法标记为virtual,编译器就会将该方法标记为virtual(并保持它的非密封状态)。这样一来,派生类就能重写它了。
如果一个接口方法是sealed的,派生类就不能重写它。不过,派生类可以重写继承同一个接口,并可为该接口的方法提供它自己的实现。在一个对象上调用一个接口的方法时,将调用该方法在该对象的类型中的实现。
internal static class InterfaceReimplementation
{
public static void Go()
{
/************************* 第一个例子 *************************/
Base b = new Base();
// 结果显示: "Base's Dispose"
b.Dispose();
// 用b的对象的类型来调用Dispose,结果显示: "Base's Dispose"
((IDisposable)b).Dispose();
/************************* 第二个例子 ************************/
Derived d = new Derived();
// 结果显示: "Derived's Dispose"
d.Dispose();
// 用d的对象的类型来调用Dispose,结果显示: "Derived's Dispose"
((IDisposable)d).Dispose();
/************************* 第三个例子 *************************/
b = new Derived();
// 用b的类型来调用Dispose,结果显示: "Base's Dispose"
b.Dispose();
// 用b的对象的类型来调用Dispose,显示: "Derived's Dispose"
((IDisposable)b).Dispose();
}
} // 这个类型派生自 Object 并且实现了 IDisposable
internal class Base : IDisposable
{
// 这个方法是隐式密封的,不能被重写
public void Dispose()
{
Console.WriteLine("Base's Dispose");
}
} // 这个类继承了Base并且实现了IDisposable接口
internal class Derived : Base, IDisposable
{
// 这个方法不能重写 Base's Dispose.
// 'new' 关键字表明重新实现了IDisposable的Dispose
new public void Dispose()
{
Console.WriteLine("Derived's Dispose"); // 注意: 下一行展示了如何让调用基类的方法
// base.Dispose();
}
}
关于调用接口方法的更多讨论
FCL的System.String类型继承了System.Object的方法签名及其实现。此外,String类型还实现了几个接口:IComparable,ICloneable,IEnumerable,IComparable<String>,IEnumerable<Char>等。这意味着String类型不需要实现(或重写)其Object基类型提供的方法,但必须实现所有接口中声明的方法。
CLR允许定义接口类型的字段,参数或局部变量。使用接口类型的一个变量,可以调用由那个接口定义的方法。例如:
// s变量引用了String对象,使用s时,可以调用String, Object,IComparable等定义的方法
private String s = "abc";
// comparable变量引用指向同一个String对象,使用comparable
//只能调用IComparable接口中的定义任何方法(包括Objetc中的方法)
private IComparable comparable = s; // convertible变量引用指向同一个String对象,使用convertible
// 只能调用IConvertible接口中的定义任何方法(包括Objetc中的方法)
private IConvertible convertible = s; // enumerable 变量引用同一个String对象
// 在运行时,可将变量从一种接口类型转型到另一种类型,
// 只要该对象的类型实现了这两个个接口
// 使用enumerable,只能调用IEnumerable声明的任何方法(包括Objetc中的方法)
private IEnumerable enumerable = (IEnumerable) convertible;
在这段代码中,所有变量都引用同一个“jeffrey”string对象。该对象在托管堆中;所以,使用其中任何变量时,调用的任何方法都会影响“feffrey”string对象。不过,变量的类型规定了能对这个对象执行的操作。s变量是string类型,所以可以用s调用string类型定义的任何成员。还可用变量s调用从object继承的任何方法。
和引用类型相似,值类型也可以实现零个或多个接口。不过,将值类型的实例转型为接口类型时必须装箱。这是由于接口变量是一个引用,它必须指向堆上的一个对象,使CLR能检查对象的类型对象指针,从而判断对象的真实类型。然后,再调用已装箱值类型的一个接口方法时,CLR会跟随对象的类型对象指针,找到类型对象的方法表,从而调用正确的方法。
隐式和显式接口方法实现(幕后发生的事情)
类型加载到clr中时,会为该类型创建并初始化一个方法表。在这个方法表中,类型引入的每个新方法都有对应的记录项;另外,还为该类型继承的所有虚方法添加了记录项目。继承的虚方法既有继承层次结构中的各个基类型定义的,也有接口类型定义的。所以,对于下面这个简单的类型定义:
interter sealed class SimpleType : IDisposable {
public void Dispose() { console.WriteLine("Dispose"); }
}
类型的方法表将包含以下方法对应的记录项:
1)Object(隐式继承的基类)定义的所有虚实例方法。
2)IDisposable(实现的接口)定义的所有接口方法。本例只有一个方法,即Dispose,因为IDisposable只定义了该方法。
3)SimpleType 引入的新方法Dispose。
为简化编程,C#编译器假定SimpleType引入的Dispose方法是对IDisposable的Dispose方法的实现。C#编译器之所以做出这样的假定,是因为Dispose方法的可访问性是public,而且接口方法的签名和新引入的方法完全一致,也就是说,这两个方法具有相同的参数和返回类型。还有,如果新的Dispose方法被标记为virtual,C#编译器仍会认为该方法匹配于接口方法。
C#编译器将一个新方法和一个接口方法匹配起来之后,便会生成元数据,指明SimpleType类型的方法表中的两个记录项引用同一个实现。下面的代码演示了如果调用类的公共Dispose方法以及如何调用IDisposable的Dispose方法在类中的实现:
public static void Main()
{
SimpleType st = new SimpleType(); // 调用公共的 Dispose 方法实现
st.Dispose(); // 调用 IDisposable 的 Dispose 方法实现
IDisposable d = st;
d.Dispose();
}
执行后,两者是没有任何却别的。输出结果都是Dispose
在第一个dispose方法调用中,调用的是SimpleType定义的dispose方法。然后定义IDisposable接口类型的变量d,它引用SimpleType对象。调用SimpleType时,调用的是IDisposable接口的dispose方法。由于c#要求公共dispose方法同时是IDisposable的Dispose方法实现,所以会执行相同的代码。
现在我们更改一下SimpleType,以便看出区别:
public sealed class SimpleType : IDisposable
{
public void Dispose() { Console.WriteLine("public Dispose"); }
void IDisposable.Dispose() { Console.WriteLine("IDisposable Dispose"); }
}
现在再次程序运行,会得到如下结果;
public Dispose
IDisposable Dispose
在c#中,将定义方法的那个接口的名称作为方法名前缀(例如IDisposable.Dispose),就会创建显式接口方法实现(explicit interface Method Implementation,EIMI)。注意,c#中不允许在定义显式接口方法时指定可访问性(比如public或private)。但是,编译器生成方法的元数据时,可访问性会自动设为private,防止其他代码在使用类的实例时直接调用接口方法。只有通过接口类型的变量才能调用接口方法。
还要还要好主意,EIMI方法不能标记为virtual,所以不能被重写。这是由于eimi方法并非真的是类型的对象模型的一部分,它只是将接口和类型连接起来,同时避免公开行为/方法。
泛型接口
C#和CLR所支持的泛型接口为开发人员提供了许多非常出色的功能。本节要讨论泛型接口提供的一些一些好处。
首先,泛型接口提供了出色的编译时类型安全性。有的接口(比如非泛型IComparable接口)定义的方法使用了Object参数或Object返回类型。在代码中调用这些接口方法时,可传递对任何类型的实例的引用。但这通常不是我们期望的。下面的代码对此进行了演示:
private static void SomeMethod1()
{
Int32 x = , y = ;
IComparable c = x; // CompareTo 期望接口一个 Object 类型; 传递 y (一个 Int32 类型) 允许
c.CompareTo(y); // y在这里装箱 // CompareTo期望接口一个 Object 类型; 传递 "2" (一个 String 类型) 允许
// 但运行是抛出 ArgumentException 异常
c.CompareTo("");
}
接口方法理想情况下应该使用强类型。这正是FCL为什么包含泛型IComparable<in T>接口的原因。下面修改代码来使用泛型接口
private static void SomeMethod2()
{
Int32 x = , y = ;
IComparable<Int32> c = x; // CompareTo 期望接口一个 Int32 类型; 传递 y (一个 Int32 类型) 允许
c.CompareTo(y); // y在这里不装箱 // CompareTo 期望接口一个 Int32 类型; 传递 "2" (一个 String 类型) 编译不通过
// 指出 String 不能被隐式转型为 Int32
// c.CompareTo("2");
}
泛型接口的第二个好处在于,处理值类型时装箱次数会少很多。
注意:fcl定义了IComparable、ICollection等接口的泛型和非泛型版本。定义类型时要实现其中任何接口,一般应使用泛型版本。fcl保留非泛型版本是为了向后兼容。
泛型接口的第三个好处在于,类可以实现同一个接口若干次,只要每次使用不同的类型参数。
public static void Go()
{
Number n = new Number(); // n 与 一个 Int32类型 5 作比较
IComparable<Int32> cInt32 = n;
Int32 result = cInt32.CompareTo(); // n 与一个 String类型 "5" 作比较
IComparable<String> cString = n;
result = cString.CompareTo("");
} // 该类实现了 IComparable<T> 接口两次
public sealed class Number : IComparable<Int32>, IComparable<String>
{
private Int32 m_val = ; // 该方法实现了 IComparable<Int32>’s CompareTo
public Int32 CompareTo(Int32 n)
{
return m_val.CompareTo(n);
} // 该方法实现了 IComparable<String>’s CompareTo
public Int32 CompareTo(String s)
{
return m_val.CompareTo(Int32.Parse(s));
}
}
接口的泛型类型参数可标记为逆变和协变,为泛型接口的使用提供更大的灵活性。
泛型和接口约束
上一节讨论了泛型接口的好处。本节要讨论泛型类型参数约束为接口的好处。
第一个好处在于,可将泛型类型参数约束为多个接口。这样一来,传递的参数的类型必须实现全部接口约束。
public sealed class SomeType
{
private static void Test()
{
Int32 x = ;
Guid g = new Guid();
// 对M的调用能通过编译,因为Int32实现了IComparable 和 IConvertible
M(x);
// 对M的调用能不通过编译,因为Guid实现了IComparable,但没实现了 IConvertible
// M(g);
} // M类型参数T被约束为需要支持同时实现IComparable 和 IConvertible interfaces接口的类型
private static Int32 M<T>(T t) where T : IComparable, IConvertible
{
// ...
return ;
}
}
这真的很酷!定义方法参数时,参数的类型规定了传递的实参必须是该类型或者它的派生类型。如果参数的类型是接口,那么实参可以是任意类类型,只要该类实现了接口。使用多个接口约束,实际是标识向方法传递的实参必须实现多个接口。
事实上,如果将T约束为一个类和两个接口,就标识传递的实参类型必须是指定的基类(或者它的派生类),而且必须实现两个接口。这种灵活性使方法能细致地约束调用者能传递的内容。调用者不满足这些约束,就会产生编译错误。
接口约束的第二个好处是传递值类型的实例时减少装箱。上述代码向M方法传递了x(值类型int实例)。x传给M方法时不会发生装箱。
c#编译器为接口约束生成特殊IL指令,导致直接在值类型上调用接口方法而不装箱。不用接口约束边没有其他办法让c#编译器生成这些IL指令。一个例外是如果值类型实现了一个接口方法,在值类型的实例上调用这个方法不会造成值类型的实例装箱。
实现多个具有相同方法名和签名的接口
定义实现多个接口的一个类型时,这些接口可能定义了具有相同名称和签名的方法。例如
public interface IWindow
{
Object GetMenu();
} public interface IRestaurant
{
Object GetMenu();
}
要定义一个实现了这两个接口的类型,必须使用"显示接口方法实现"来实现这个类型的成员,如下:
// 这个类型派生自 System.Object and
// 并不实现 IWindow 和 IRestaurant 接口.
public class MarioPizzeria : IWindow, IRestaurant
{
// 这是IWindow 的 GetMenu 方法.
Object IWindow.GetMenu()
{
// ...
return null;
} // 这是 IRestaurant 的 GetMenu 方法.
Object IRestaurant.GetMenu()
{
// ...
return null;
} // 这个GetMenu方法是可选的,与接口无关
public Object GetMenu()
{
// ...
return null;
}
}
由于这个类型必须实现多个接口的GetMenu方法,所以要告诉c#编译器每个GetMenu方法对应的是哪个接口的实现。
代码在使用一个MarioPizzeria 对象时,必须把对象转型为具体的接口,以调用所需的方法。
public static void Go()
{
MarioPizzeria mp = new MarioPizzeria(); // 这行调用 MarioPizzeria 的公共 GetMenu 方法
mp.GetMenu(); // 这行调用 MarioPizzeria 的 IWindow.GetMenu 方法
IWindow window = mp;
window.GetMenu(); // 这行调用 MarioPizzeria 的 IRestaurant.GetMenu 方法
IRestaurant restaurant = mp;
restaurant.GetMenu();
}
用显式接口方法实现来增强编译时类型安全性
接口很好用,它们定义了在类型之间进行沟通的标准方式。前面曾讨论了泛型接口,讨论了它们如何增强编译时的类型安全性和减少装箱操作。遗憾的是,有些时候由于不存在泛型版本,仍然需要实现泛型接口。如果接口的任何一个方法接受System.Object类型的参数或返回一个System.Object类型的值,就会丧失编译时的类型安全性,装箱也会发生。所以,本节将介绍如何用"显示接口方法"(EIMI)在某种程度上改善这个局面。
下面定义了一个寻常的接口:
public interface IComparable {
Int32 CompareTo(Objetc other);
}
这个接口定义了一个方法,该方法接受一个System.Object类型的参数。可像下面一样实现该接口的一个类型:
internal struct SomeValueType : IComparable
{
private Int32 m_x;
public SomeValueType(Int32 x) { m_x = x; }
public Int32 CompareTo(Object other)
{
return (m_x - ((SomeValueType)other).m_x);
}
}
可以使用SomeVakueType写下面的代码:
public static void Go()
{
SomeValueType v = new SomeValueType();
Object o = new Object();
Int32 n = v.CompareTo(v); // 非预期装箱
n = v.CompareTo(o); // 抛出异常
}
上述代码有两个地方不理想。
1) 非预期装箱操作
v作为实参传给CompareTo方法时,因为CompareTo预期接受类型是Objec,它必须进行装箱。
2)缺乏类型安全性
代码能通过编译,但在CompareTo方法试图进行转换时可能会抛出异常。
使用EIMI解决这两个问题:
internal struct SomeValueType : IComparable
{
private Int32 m_x;
public SomeValueType(Int32 x) { m_x = x; }
public Int32 CompareTo(SomeValueType other)
{
return (m_x - other.m_x);
}
// 注意: 这个是显示实现接口 没有指定public或者private的可访问性
Int32 IComparable.CompareTo(Object other)
{
return CompareTo((SomeValueType)other);
}
}
注意新版本的几个改动。第一,他现在有两个CompareTo方法。第一个CompareTo方法不是获取一个Object作为参数,而是获取一个SomeValueType作为参数。这个参数改变后,就没必要再用代码将other类型转型为SomeValueType,所以用于转型的代码被去掉了。第二,修改第一个CompareTo方法,使它类型安全。这意味着SomeValueType现在没有实现ICompareTo接口,所以不满足IComparable接口的契约。所以,SomeValueType必须实现一个CompareTo方法来满足IComparable的契约。第二个IComparable.CompareTo方法正是出于这个目的而设计的,它是一个EIMI。
这两个改动意味着想在获得了编译时的类型安全性,而且不会发生装箱:
public static void Go()
{
SomeValueType v = new SomeValueType();
Object o = new Object();
Int32 n = v.CompareTo(v); // 不发生装箱
n = v.CompareTo(o); //编译时错误
}
不过,如果定义一个接口类型的变量,就会再次丧失编译时的类型安全性,而且再次发生非预期的装箱操作。
public static void Go()
{
SomeValueType v = new SomeValueType();
IComparable c = v; //装箱 Object o = new Object();
Int32 n = v.CompareTo(v); // 非预期装箱操作
n = c.CompareTo(o); //InvalidCastException异常
}
事实上,如本章前面所述,将一个值类型的实例转型为接口类型时,CLR必须对指向类型的实例进行转型。因此,前面的Go方法中会发生两次装箱。
实现IConvertible, ICollection,IList和IDictionary等接口时,可利用EIMI为这些接口的方法创建类型安全的版本,并减少值类型的装箱。
谨慎使用显式接口方法实现
使用EIMI也可能造成一些严重后果,所以应该尽量避免使用EIMI。幸好,泛型接口可帮助我们在大多数时候避免使用EIMI。但有时,比如实现具有相同名称和签名的两个接口方法时,仍需要它们。
EIMI最主要的问题如下:
1)没有文档解释一个类型具体如何实现一个EIMI方法,也没有vs的智能感知。
2)值类型的实例在转型为接口时装箱
3)EIMI不能由派生类调用
下面详细讨论下这些问题。
例如,int32类型的文档只是说它实现了IConvertible接口的所有方法。但你不能直接在一个int32上调用IConvertible方法。例如下面代码无法编译
public static void Main()
{
int x=;
Single s=x.ToSingle(null);
}
编译这个方法时,c#编译器会提示 error cs0117 int不包含ToSingle的定义。这个错误信息使开发人员感到困惑,因为它实际上是定义了。
要在一个int32上调用toSingle,首先必须将其转换为IConvertible接口,然后再进行调用,如下
public static void Main()
{
int x=;
Single s=((IConvertible) x).ToSingle(null);
}
对类型转换的要求不明确,让许多开发人员看不出自己的问题在哪,还有一个更让人烦恼的问题,int值类型转换为IConvertible会发生装箱,既浪费内存,又损害性能。
EIMI的第三个,也是最大的问题,他们不能被派生类调用。
设计 :基类还是接口
选择基类还是接口的指导性原则:
1) IS-A vs. CAN-DO关系
类型只能继承一个实现。如果派生类型和基类型不能建立起is –a关系,就不用基类而用接口。接口意味着Can do关系。如果多种对象类型都能做某事,就为它们创建接口。例如,一个类型能将自己的实例转换转换二维另一个类型(IConvertible),一个类型能序列化自己的实例(ISerializable)。注意,值类型必须从system.valueType派生,所以不能从一个人一的基类派生。这时必须使用can do关系并定义接口。
2) 易用性
对于开发人员,定义从基类派生的新类型通常比实现接口的所有方法容易得多。基类可提供大量功能,所以派生类型可能只需要稍微改动。而提供接口的话,新类型必须实现所有成员。
3) 一致性的实现
无论接口协定订立得有多好,都无法保证所有人百分之百正确实现它。事实上,com颇受该问题之累。而如果为基类型提供良好的默认实现,那么一开始得到的就是能正常工作并经过良好测试的类型。以后根据需要修改就可以了。
4) 版本控制
向基类型添加一个方法,派生类将继承新方法。一开始使用就是一个能正常工作的类型,用户的源代码甚至不需要编译。向接口添加一个新成员,会强迫接口的实现者更改其源代码。
最后要指出的是,这两件事情实际上是可以同时做:定义一个接口,同时提供一个实现了这个接口的基类。
FCL中涉及数据流处理的类采用的是实现继承方法。system.IO.Stream是抽象基类,提供了包括read和write在内的一组方法。其他类(filestream,memoryStream和NetWorkStream等)都从stream派生。在这三个类中,每一个和stream类都是is –a关系,这使得具体类的实现变得更容易。
相反,Microsoft采用基于接口的方式设计FCL中的集合。
重温CLR(九) 接口的更多相关文章
- 重温CLR(十六) CLR寄宿和AppDomain
寄宿(hosting)使任何应用程序都能利用clr的功能.特别要指出的是,它使现有应用程序至少能部分使用托管代码编写.另外,寄宿还为应用程序提供了通过编程来进行自定义和扩展的能力. 允许可扩展性意味着 ...
- 重温CLR(十五) 托管堆和垃圾回收
本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计 ...
- 重温CLR(十四) 可空类型
我们知道,一个值类型的变量永远不可能为null.它总是包含值类型本身.遗憾的是,这在某些情况下会成为问题.例如,设计一个数据库时,可将一个列定义成为一个32位的整数,并映射到FCL的Int32数据类型 ...
- 重温CLR(十三) 定制特性
利用定制特性,可宣告式为自己的代码构造添加注解来实现特殊功能.定制特性允许为几乎每一个元数据表记录项定义和应用信息.这种可扩展的元数据信息能在运行时查询,从而动态改变代码的执行方式.使用各种.NET技 ...
- 重温CLR(十一) 枚举类型、位标志和数组
枚举类型 枚举类型(enumerated types)定义了一组"符号名称/值"配对.例如,以下Color类型定义了一组符号,每个符号都标识一种颜色: internal enum ...
- 重温CLR(十) 字符、字符串和文本处理
本章将介绍.net中处理字符和字符串的机制 字符 在.NET Framewole中,字符总是表示成16位Unicode代码值,这简化了国际化应用程序的开发. 每个字符都表示成System.Char结构 ...
- 重温CLR(八 ) 泛型
熟悉面向对象编程的开发人员都深谙面向对象的好处,其中一个好处是代码重用,它极大提高了开发效率.也就是说,可以派生出一个类,让他继承基类的所有能力.派生类只需要重写虚方法,或添加一些新方法,就可定制派生 ...
- 重温CLR(七 ) 属性和事件
无参属性 许多类型都定义了能被获取或更高的状态信息.这种状态信息一般作为类型的字段成员实现.例如一下类型包含两个字段: public sealed class Employee{ public str ...
- 重温CLR(六)方法和参数
实例构造器和类(引用类型) 构造器是将类型的实例初始化为良好状态的特殊方法.构造器方法在“方法定义元数据表”中始终叫做.ctor(constructor的简称).创建引用类型的实例时,首先为实例的数据 ...
随机推荐
- Bigdecimal: Non-terminating decimal expansion; no exact representable decimal result.
做除法没有指定保留小数点后几位,就会抛出此异常. 因为会除不尽 Non-terminating decimal expansion; no exact representable decimal re ...
- centos cgroup配置
centOS 6:1. 启用cgroup 查看内核是否支持cgroup功能:cat /boot/config-`uname -r` | grep -i rt_group 查看支持的子系统: ...
- iQuery移动端手势事件插件-jGestures
jGestures下载 jGestures事件简介 orientationchange 代表设备顺时针或者逆时针旋转.此事件可以被设备触发,可能使用的是重力传感器. pinch 缩放手势(两个手指在屏 ...
- 数据库建表char(10)和VARCHAR(10)
1.CHAR的长度是固定的,而VARCHAR2的长度是可以变化的, 比如,存储字符串“abc",对于CHAR (10),表示你存储的字符将占10个字节(包括7个空字符),而同样的VARCHA ...
- 通用Mapper相关
1.通用Mapper中,用@Table来映数据表与实体,其中 name:指定表的名称,例如@Table(name="ls_post") catalog: 指定数据库名称,默认为当前 ...
- ContOS网络连接及简单的ssh Xshell连接!
这边简单的记录一下下ContOS网络连接及简单的ssh Xshell连接! 首先你得安装一个Contos Linux系统对吧! 1.找到设置--->网络-->有线连接-->IPv4 ...
- Metasploit没有db_autopwn命令的解决办法
将附件db_autopwn.rp 拷贝到/opt/framework3/msf3/plugins/PS:db_autopwn.rp 下载地址:http://dl.vmall.com/c04w8czlc ...
- Python中的map和reduce函数简介
①从参数方面来讲: map()函数: map()包含两个参数,第一个是参数是一个函数,第二个是序列(列表或元组).其中,函数(即map的第一个参数位置的函数)可以接收一个或多个参数. reduce() ...
- js从数组中随机获取n个不重复的数据
做云课堂的作业时遇到一要求,实现刷新页面时显示不同数据,(数组中20个据,页面加载10个).思路就是从0-19中随机生成10个不同的数,让数组取下标输出数据. 下面是在num的范围内生成n个不重复的数 ...
- String类运算符重载,自己实现
body, table{font-family: 微软雅黑; font-size: 10pt} table{border-collapse: collapse; border: solid gray; ...