.net中的接口
.net的接口和java不同,不可以有field,
1.它的成员必须是方法、属性、事件或索引器。接口不能包含常数、字段、运算符、实例构造函数、析构函数或类型,也不能包含任何种类的静态成员。
2.接口成员默认访问方式是public。接口成员定义不能包含任何修饰符,比如成员定义前不能加abstract,public,protected,internal,private,virtual,override 或static 修饰符。
3. 接口的成员之间不能相互同名。继承而来的成员不用再定义,但接口可以定义与继承而来的成员同名的成员,这时我们说接口成员覆盖了继承而来的成员,这不会导致错误,但编译器会给出一个警告。关闭警告提示的方式是在成员定义前加上一个new关键字。但如果没有覆盖父接口中的成员,使用new 关键字会导致编译器发出警告。
方法的名称必须与同一接口中定义的所有属性和事件的名称不同。此外,方法的签名必须与同一接口中定义的所有其他方法的签名不同。
7、属性或事件的名称必须与同一接口中定义的所有其他成员的名称不同。
8、一个索引器的签名必须区别于在同一接口中定义的其他所有索引器的签名。
9、接口方法声明中的属性(attributes), 返回类型(return-type), 标识符(identifier), 和形式参数列表(formal-parameter-lis)与一个类的方法声明中的那些有相同的意义。一个接口方法声明不允许指定一个方法主体,而声明通常用一个分号结束。
10、接口属性声明的访问符与类属性声明的访问符相对应,除了访问符主体通常必须用分号。因此,无论属性是读写、只读或只写,访问符都完全确定。
11、接口索引声明中的属性(attributes), 类型(type), 和形式参数列表 (formal-parameter-list)与类的索引声明的那些有相同的意义。
下面例子中接口IMyTest包含了索引指示器、事件E、 方法F、 属性P 这些成员:
interface IMyTest{
string this[int index] { get; set; }
event EventHandler E ;
void F(int value) ;
string P { get; set; }
}
public delegate void EventHandler(object sender, EventArgs e) ;
下面例子中接口IStringList包含每个可能类型成员的接口:一个方法,一个属性,一个事件和一个索引。
public delegate void StringListEvent(IStringList sender);
public interface IStringList
{
void Add(string s);
int Count { get; }
event StringListEvent Changed;
string this[int index] { get; set; }
}
接口成员的全权名
使用接口成员也可采用全权名(fully qualified name)。接口的全权名称是这样构成的。接口名加小圆点"." 再跟成员名比如对于下面两个接口:
interface IControl {
void Paint( ) ;
}
interface ITextBox: IControl {
void GetText(string text) ;
}
其中Paint 的全权名是IControl.Paint,GetText的全权名是ITextBox. GetText。当然,全权名中的成员名称必须是在接口中已经定义过的,比如使用ITextBox.Paint.就是不合理的。
如果接口是名字空间的成员,全权名还必须包含名字空间的名称。
namespace System
{
public interface IDataTable {
object Clone( ) ;
}
}
那么Clone方法的全权名是System. IDataTable.Clone
实现—访问
1、显式实现接口成员
为了实现接口,类可以定义显式接口成员执行体(Explicit interface member implementations)。显式接口成员执行体可以是一个方法、一个属性、一个事件或者是一个索引指示器的定义,定义与该成员对应的全权名应保持一致。
using System ;
interface ICloneable {
object Clone( ) ;
}
interface IComparable {
int CompareTo(object other) ;
}
class ListEntry: ICloneable, IComparable {
object ICloneable.Clone( ) {…}
int IComparable.CompareTo(object other) {…}
}
上面的代码中ICloneable.Clone 和IComparable.CompareTo 就是显式接口成员执行体。
说明:
1、不能在方法调用、属性访问以及索引指示器访问中通过全权名访问显式接口成员执行体。事实上,显式接口成员执行体只能通过接口的实例,仅仅引用接口的成员名称来访问。
2、显式接口成员执行体不能使用任何访问限制符,也不能加上abstract, virtual, override或static 修饰符。
3、显式接口成员执行体和其他成员有着不同的访问方式。因为不能在方法调用、属性访问以及索引指示器访问中通过全权名访问,显式接口成员执行体在某种意义上是私有的。但它们又可以通过接口的实例访问,也具有一定的公有性质。
4、只有类在定义时,把接口名写在了基类列表中,而且类中定义的全权名、类型和返回类型都与显式接口成员执行体完全一致时,显式接口成员执行体才是有效的,例如:
class Shape: ICloneable {
object ICloneable.Clone( ) {…}
int IComparable.CompareTo(object other) {…}
}
使用显式接口成员执行体通常有两个目的:
1、因为显式接口成员执行体不能通过类的实例进行访问,这就可以从公有接口中把接口的实现部分单独分离开。如果一个类只在内部使用该接口,而类的使用者不会直接使用到该接口,这种显式接口成员执行体就可以起到作用。
2、显式接口成员执行体避免了接口成员之间因为同名而发生混淆。如果一个类希望对名称和返回类型相同的接口成员采用不同的实现方式,这就必须要使用到显式接口成员执行体。如果没有显式接口成员执行体,那么对于名称和返回类型不同的接口成员,类也无法进行实现。
下面的定义是无效的,因为Shape 定义时基类列表中没有出现接口IComparable。
class Shape: ICloneable
{
object ICloneable.Clone( ) {…}
}
class Ellipse: Shape
{
object ICloneable.Clone( ) {…}
}
在Ellipse 中定义ICloneable.Clone是错误的,因为Ellipse即使隐式地实现了接口ICloneable,ICloneable仍然没有显式地出现在Ellipse定义的基类列表中。
接口成员的全权名必须对应在接口中定义的成员。如下面的例子中,Paint的显式接口成员执行体必须写成IControl.Paint。
using System ;
interface IControl
{
void Paint( ) ;
}
interface ITextBox: IControl
{
void SetText(string text) ;
}
class TextBox: ITextBox
{
void IControl.Paint( ) {…}
void ITextBox.SetText(string text) {…}
}
实现接口的类可以显式实现该接口的成员。当显式实现某成员时,不能通过类实例访问该成员,而只能通过该接口的实例访问该成员。显式接口实现还允许程序员继承共享相同成员名的两个接口,并为每个接口成员提供一个单独的实现。
下面例子中同时以公制单位和英制单位显示框的尺寸。Box类继承 IEnglishDimensions和 IMetricDimensions两个接口,它们表示不同的度量衡系统。两个接口有相同的成员名 Length 和 Width。
程序清单1 DemonInterface.cs
interface IEnglishDimensions {
float Length ( ) ;
float Width ( ) ;
}
interface IMetricDimensions {
float Length ( ) ;
float Width ( ) ;
}
class Box : IEnglishDimensions, IMetricDimensions {
float lengthInches ;
float widthInches ;
public Box(float length, float width) {
lengthInches = length ;
widthInches = width ;
}
float IEnglishDimensions.Length( ) {
return lengthInches ;
}
float IEnglishDimensions.Width( ) {
return widthInches ;
}
float IMetricDimensions.Length( ) {
return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
return widthInches * 2.54f ;
}
public static void Main( ) {
//定义一个实类对象 "myBox"::
Box myBox = new Box(30.0f, 20.0f);
// 定义一个接口" eDimensions"::
IEnglishDimensions eDimensions = (IEnglishDimensions) myBox;
IMetricDimensions mDimensions = (IMetricDimensions) myBox;
// 输出:
System.Console.WriteLine(" Length(in): {0}", eDimensions.Length( ));
System.Console.WriteLine(" Width (in): {0}", eDimensions.Width( ));
System.Console.WriteLine(" Length(cm): {0}", mDimensions.Length( ));
System.Console.WriteLine(" Width (cm): {0}", mDimensions.Width( ));
}
}
输出:Length(in): 30,Width (in): 20,Length(cm): 76.2,Width (cm): 50.8
代码讨论:如果希望默认度量采用英制单位,请正常实现 Length 和 Width 这两个方法,并从 IMetricDimensions 接口显式实现 Length 和 Width 方法:
public float Length( ) {
return lengthInches ;
}
public float Width( ){
return widthInches;
}
float IMetricDimensions.Length( ) {
return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
return widthInches * 2.54f ;
}
这种情况下,可以从类实例访问英制单位,而从接口实例访问公制单位:
System.Console.WriteLine("Length(in): {0}", myBox.Length( )) ;
System.Console.WriteLine("Width (in): {0}", myBox.Width( )) ;
System.Console.WriteLine("Length(cm): {0}", mDimensions.Length( )) ;
System.Console.WriteLine("Width (cm): {0}", mDimensions.Width( )) ;
2、继承接口实现
接口具有不变性,但这并不意味着接口不再发展。类似于类的继承性,接口也可以继承和发展。
注意:接口继承和类继承不同,首先,类继承不仅是说明继承,而且也是实现继承;而接口继承只是说明继承。也就是说,派生类可以继承基类的方法实现,而派生的接口只继承了父接口的成员方法说明,而没有继承父接口的实现,其次,C#中类继承只允许单继承,但是接口继承允许多继承,一个子接口可以有多个父接口。
接口可以从零或多个接口中继承。从多个接口中继承时,用":"后跟被继承的接口名字,多个接口名之间用","分割。被继承的接口应该是可以访问得到的,比如从private 类型或internal 类型的接口中继承就是不允许的。接口不允许直接或间接地从自身继承。和类的继承相似,接口的继承也形成接口之间的层次结构。
请看下面的例子:
using System ;
interface IControl {
void Paint( ) ;
}
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
interface IComboBox: ITextBox, IListBox { }
对一个接口的继承也就继承了接口的所有成员,上面的例子中接口ITextBox和IListBox都从接口IControl中继承,也就继承了接口IControl的Paint方法。接口IComboBox从接口ITextBox和IListBox中继承,因此它应该继承了接口ITextBox的SetText方法和IListBox的SetItems方法,还有IControl的Paint方法。
一个类继承了所有被它的基本类提供的接口实现程序。
不通过显式的实现一个接口,一个派生类不能用任何方法改变它从它的基本类继承的接口映射。例如,在声明中
interface IControl {
void Paint( );
}
class Control: IControl {
public void Paint( ) {...}
}
class TextBox: Control {
new public void Paint( ) {...}
}
TextBox 中的方法Paint 隐藏了Control中的方法Paint ,但是没有改变从Control.Paint 到IControl.Paint 的映射,而通过类实例和接口实例调用Paint将会有下面的影响
Control c = new Control( ) ;
TextBox t = new TextBox( ) ;
IControl ic = c ;
IControl it = t ;
c.Paint( ) ; // 影响Control.Paint( ) ;
t.Paint( ) ; // 影响TextBox.Paint( ) ;
ic.Paint( ) ; // 影响Control.Paint( ) ;
it.Paint( ) ; // 影响Control.Paint( ) ;
但是,当一个接口方法被映射到一个类中的虚拟方法,派生类就不可能覆盖这个虚拟方法并且改变接口的实现函数。例如,把上面的声明重新写为
interface IControl {
void Paint( ) ;
}
class Control: IControl {
public virtual void Paint( ) {...}
}
class TextBox: Control {
public override void Paint( ) {...}
}
就会看到下面的结果:
Control c = new Control( ) ;
TextBox t = new TextBox( ) ;
IControl ic = c ;
IControl it = t ;
c.Paint( ) ; // 影响Control.Paint( );
t.Paint( ) ; // 影响TextBox.Paint( );
ic.Paint( ) ; // 影响Control.Paint( );
it.Paint( ) ; // 影响TextBox.Paint( );
由于显式接口成员实现程序不能被声明为虚拟的,就不可能覆盖一个显式接口成员实现程序。一个显式接口成员实现程序调用另外一个方法是有效的,而另外的那个方法可以被声明为虚拟的以便让派生类可以覆盖它。例如:
interface IControl {
void Paint( ) ;
}
class Control: IControl {
void IControl.Paint( ) { PaintControl( ); }
protected virtual void PaintControl( ) {...}
}
class TextBox: Control {
protected override void PaintControl( ) {...}
}
这里,从Control 继承的类可以通过覆盖方法PaintControl 来对IControl.Paint 的实现程序进行特殊化。
3、重新实现接口
我们已经介绍过,派生类可以对基类中已经定义的成员方法进行重载。类似的概念引入到类对接口的实现中来,叫做接口的重实现(re-implementation)。继承了接口实现的类可以对接口进行重实现。这个接口要求是在类定义的基类列表中出现过的。对接口的重实现也必须严格地遵守首次实现接口的规则,派生的接口映射不会对为接口的重实现所建立的接口映射产生任何影响。
下面的代码给出了接口重实现的例子:
interface IControl {
void Paint( ) ;
class Control: IControl
void IControl.Paint( ) {…}
class MyControl: Control, IControl
public void Paint( ) {}
}
实际上就是:Control把IControl.Paint映射到了Control.IControl.Paint上,但这并不影响在MyControl中的重实现。在MyControl中的重实现中,IControl.Paint被映射到MyControl.Paint 之上。
在接口的重实现时,继承而来的公有成员定义和继承而来的显式接口成员的定义参与到接口映射的过程。
using System ;
interface IMethods {
void F( ) ;
void G( ) ;
void H( ) ;
void I( ) ;
}
class Base: IMethods {
void IMethods.F( ) { }
void IMethods.G( ) { }
public void H( ) { }
public void I( ) { }
}
class Derived: Base, IMethods {
public void F( ) { }
void IMethods.H( ) { }
}
这里,接口IMethods在Derived中的实现把接口方法映射到了Derived.F,Base.IMethods.G, Derived.IMethods.H, 还有Base.I。前面我们说过,类在实现一个接口时,同时隐式地实现了该接口的所有父接口。同样,类在重实现一个接口时同时,隐式地重实现了该接口的所有父接口。
using System ;
interface IBase {
void F( ) ;
}
interface IDerived: IBase {
void G( ) ;
}
class C: IDerived {
void IBase.F( ) {
//对F 进行实现的代码…
}
void IDerived.G( ) {
//对G 进行实现的代码…
}
}
class D: C, IDerived {
public void F( ) {
//对F 进行实现的代码…
}
public void G( ) {
//对G 进行实现的代码…
}
}
这里,对IDerived的重实现也同样实现了对IBase的重实现,把IBase.F 映射到了D.F。
4、映射接口
类必须为在基类表中列出的所有接口的成员提供具体的实现。在类中定位接口成员的实现称之为接口映射(interface mapping )。
映射,数学上表示一一对应的函数关系。接口映射的含义也是一样,接口通过类来实现,那么对于在接口中定义的每一个成员,都应该对应着类的一个成员来为它提供具体的实现。
类的成员及其所映射的接口成员之间必须满足下列条件:
1、如果A和B都是成员方法,那么A和B的名称、类型、形参表(包括参数个数和每一个参数的类型)都应该是一致的。
2、如果A和B都是属性,那么A和B的名称、类型应当一致,而且A和B的访问器也是类似的。但如果A不是显式接口成员执行体,A允许增加自己的访问器。
3、如果A和B都是时间那么A和B的名称、类型应当一致。
4、如果A和B都是索引指示器,那么A和B的类型、形参表(包括参数个数和每一个参数的类型)应当一致。而且A和B的访问器也是类似的。但如果A不是显式接口成员执行体,A允许增加自己的访问器。
那么,对于一个接口成员,怎样确定由哪一个类的成员来实现呢?即一个接口成员映射的是哪一个类的成员?在这里,我们叙述一下接口映射的过程。假设类C实现了一个接口IInterface,Member是接口IInterface中的一个成员,在定位由谁来实现接口成员Member,即Member的映射过程是这样的:
1、如果C中存在着一个显式接口成员执行体,该执行体与接口IInterface 及其成员Member相对应,则由它来实现Member 成员。
2、如果条件(1)不满足,且C中存在着一个非静态的公有成员,该成员与接口成员Member相对应,则由它来实现Member 成员。
3、如果上述条件仍不满足,则在类C定义的基类列表中寻找一个C 的基类D,用D来代替C。
4、重复步骤1-- 3 ,遍历C的所有直接基类和非直接基类,直到找到一个满足条件的类的成员。
5、如果仍然没有找到,则报告错误。
下面是一个调用基类方法来实现接口成员的例子。类Class2 实现了接口Interface1,类Class2 的基类Class1 的成员也参与了接口的映射,也就是说类Class2 在对接口Interface1进行实现时,使用了类Class1提供的成员方法F来实现接口Interface1的成员方法F:
interface Interface1 {
void F( ) ;
}
class Class1 {
public void F( ) { }
public void G( ) { }
}
class Class2: Class1, Interface1 {
new public void G( ) {}
}
注意:接口的成员包括它自己定义的成员,而且包括该接口所有父接口定义的成员。在接口映射时,不仅要对接口定义体中显式定义的所有成员进行映射,而且要对隐式地从父接口那里继承来的所有接口成员进行映射。
在进行接口映射时,还要注意下面两点:
1、在决定由类中的哪个成员来实现接口成员时,类中显式说明的接口成员比其它成员优先实现。
2、使用Private、protected和static修饰符的成员不能参与实现接口映射。例如:
interface ICloneable {
object Clone( ) ;
}
class C: ICloneable {
object ICloneable.Clone( ) {…}
public object Clone( ) {…}
}
例子中成员ICloneable.Clone 称为接口ICloneable 的成员Clone 的实现者,因为它是显式说明的接口成员,比其它成员有着更高的优先权。
如果一个类实现了两个或两个以上名字、类型和参数类型都相同的接口,那么类中的一个成员就可能实现所有这些接口成员:
interface IControl {
void Paint( ) ;
}
interface IForm {
void Paint( ) ;
}
class Page: IControl, IForm {
public void Paint( ) {…}
}
这里,接口IControl和IForm的方法Paint都映射到了类Page中的Paint方法。当然也可以分别用显式的接口成员分别实现这两个方法:
interface IControl {
void Paint( ) ;
}
interface IForm {
void Paint( ) ;
}
class Page: IControl, IForm {
public void IControl.Paint( ) {
//具体的接口实现代码
}
public void IForm.Paint( ) {
//具体的接口实现代码
}
}
上面的两种写法都是正确的。但是如果接口成员在继承中覆盖了父接口的成员,那么对该接口成员的实现就可能必须映射到显式接口成员执行体。看下面的例子:
interface IBase {
int P { get; }
}
interface IDerived: IBase {
new int P( ) ;
}
接口IDerived从接口IBase中继承,这时接口IDerived 的成员方法覆盖了父接口的成员方法。因为这时存在着同名的两个接口成员,那么对这两个接口成员的实现如果不采用显式接口成员执行体,编译器将无法分辨接口映射。所以,如果某个类要实现接口IDerived,在类中必须至少定义一个显式接口成员执行体。采用下面这些写法都是合理的:
//一:对两个接口成员都采用显式接口成员执行体来实现
lass C: IDerived {
int IBase.P
get
{ //具体的接口实现代码 }
int IDerived.P( ){
//具体的接口实现代码 }
}
//二:对Ibase 的接口成员采用显式接口成员执行体来实现
class C: IDerived {
int IBase.P
get {//具体的接口实现代码}
public int P( ){
//具体的接口实现代码 }
}
//三:对IDerived 的接口成员采用显式接口成员执行体来实现
class C: IDerived{
public int P
get {//具体的接口实现代码}
int IDerived.P( ){
//具体的接口实现代码}
}
另一种情况是,如果一个类实现了多个接口,这些接口又拥有同一个父接口,这个父接口只允许被实现一次。
using System ;
interface IControl {
void Paint( ) ;
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
class ComboBox: IControl, ITextBox, IListBox {
void IControl.Paint( ) {…}
void ITextBox.SetText(string text) {…}
void IListBox.SetItems(string[] items) {…}
}
上面的例子中,类ComboBox实现了三个接口:IControl,ITextBox和IListBox。如果认为ComboBox不仅实现了IControl接口,而且在实现ITextBox和IListBox的同时,又分别实现了它们的父接口IControl。实际上,对接口ITextBox 和IListBox 的实现,分享了对接口IControl 的实现。
我们对C#的接口有了较全面的认识,基本掌握了怎样应用C#的接口编程,但事实上,C#的不仅仅应用于.NET平台,它同样支持以前的COM,可以实现COM类到.NET类的转换,如C#调用API。
第六节、接口转换
C#中不仅支持.Net 平台,而且支持COM平台。为了支持 COM和.Net,C# 包含一种称为属性的独特语言特性。一个属性实际上就是一个 C# 类,它通过修饰源代码来提供元信息。属性使 C# 能够支持特定的技术,如 COM 和 .Net,而不会干扰语言规范本身。C# 提供将COM接口转换为 C#接口的属性类。另一些属性类将 COM类转换为C# 类。执行这些转换不需要任何 IDL 或类工厂。
现在部署的任何COM 组件都可以在接口转换中使用。通常情况下,所需的调整是完全自动进行的。
特别是,可以使用运行时可调用包装 (RCW) 从 .NET 框架访问 COM 组件。此包装将 COM 组件提供的 COM 接口转换为与 .NET 框架兼容的接口。对于 OLE 自动化接口,RCW 可以从类型库中自动生成;对于非 OLE 自动化接口,开发人员可以编写自定义 RCW,手动将 COM 接口提供的类型映射为与 .NET 框架兼容的类型。
使用ComImport引用COM组件
COM Interop 提供对现有 COM 组件的访问,而不需要修改原始组件。使用ComImport引用COM组件常包括下面 几个方面的问题:
1、创建 COM 对象。
2、确定 COM 接口是否由对象实现。
3、调用 COM 接口上的方法。
4、实现可由 COM 客户端调用的对象和接口。
创建 COM 类包装
要使 C# 代码引用COM 对象和接口,需要在 C# 中包含 COM 接口的定义。完成此操作的最简单方法是使用 TlbImp.exe(类型库导入程序),它是一个包括在 .NET 框架 SDK 中的命令行工具。TlbImp 将 COM 类型库转换为 .NET 框架元数据,从而有效地创建一个可以从任何托管语言调用的托管包装。用 TlbImp 创建的 .NET 框架元数据可以通过 /R 编译器选项包括在 C# 内部版本中。如果使用 Visual Studio 开发环境,则只需添加对 COM 类型库的引用,将为您自动完成此转换。
TlbImp 执行下列转换:
1、COM coclass 转换为具有无参数构造函数的 C# 类。
2、COM 结构转换为具有公共字段的 C# 结构。
检查 TlbImp 输出的一种很好的方法是运行 .NET 框架 SDK 命令行工具 Ildasm.exe(Microsoft 中间语言反汇编程序)来查看转换结果。
虽然 TlbImp 是将 COM 定义转换为 C# 的首选方法,但也不是任何时候都可以使用它(例如,在没有 COM 定义的类型库时或者 TlbImp 无法处理类型库中的定义时,就不能使用该方法)。在这些情况下,另一种方法是使用 C# 属性在 C# 源代码中手动定义 COM 定义。创建 C# 源映射后,只需编译 C# 源代码就可产生托管包装。
执行 COM 映射需要理解的主要属性包括:
1、ComImport:它将类标记为在外部实现的 COM 类。
2、Guid:它用于为类或接口指定通用唯一标识符 (UUID)。
3、InterfaceType,它指定接口是从 IUnknown 还是从 IDispatch 派生。
4、PreserveSig,它指定是否应将本机返回值从 HRESULT 转换为 .NET 框架异常。
声明 COM coclass
COM coclass 在 C# 中表示为类。这些类必须具有与其关联的 ComImport 属性。下列限制适用于这些类:
1、类不能从任何其他类继承。
2、类不能实现任何接口。
4、类还必须具有为其设置全局唯一标识符 (GUID) 的 Guid 属性。
以下示例在 C# 中声明一个 coclass:
// 声明一个COM类 FilgraphManager
[ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")]
class FilgraphManager
{ }
C# 编译器将添加一个无参数构造函数,可以调用此构造函数来创建 COM coclass 的实例。
创建 COM 对象
COM coclass 在 C# 中表示为具有无参数构造函数的类。使用 new 运算符创建该类的实例等效于在 C# 中调用 CoCreateInstance。使用以上定义的类,就可以很容易地实例化此类:
class MainClass
{
public static void Main()
{
FilgraphManager filg = new FilgraphManager();
}
}
声明 COM 接口
COM 接口在 C# 中表示为具有 ComImport 和 Guid 属性的接口。它不能在其基接口列表中包含任何接口,而且必须按照方法在 COM 接口中出现的顺序声明接口成员函数。
在 C# 中声明的 COM 接口必须包含其基接口的所有成员的声明,IUnknown 和 IDispatch 的成员除外(.NET 框架将自动添加这些成员)。从 IDispatch 派生的 COM 接口必须用 InterfaceType 属性予以标记。
从 C# 代码调用 COM 接口方法时,公共语言运行库必须封送与 COM 对象之间传递的参数和返回值。对于每个 .NET 框架类型均有一个默认类型,公共语言运行库将使用此默认类型在 COM 调用间进行封送处理时封送。例如,C# 字符串值的默认封送处理是封送到本机类型 LPTSTR(指向 TCHAR 字符缓冲区的指针)。可以在 COM 接口的 C# 声明中使用 MarshalAs 属性重写默认封送处理。
在 COM 中,返回成功或失败的常用方法是返回一个 HRESULT,并在 MIDL 中有一个标记为"retval"、用于方法的实际返回值的 out 参数。在 C#(和 .NET 框架)中,指示已经发生错误的标准方法是引发异常。
默认情况下,.NET 框架为由其调用的 COM 接口方法在两种异常处理类型之间提供自动映射。
返回值更改为标记为 retval 的参数的签名(如果方法没有标记为 retval 的参数,则为 void)。
标记为 retval 的参数从方法的参数列表中剥离。
任何非成功返回值都将导致引发 System.COMException 异常。
此示例显示用 MIDL 声明的 COM 接口以及用 C# 声明的同一接口(注意这些方法使用 COM 错误处理方法)。
下面是接口转换的C#程序:
using System.Runtime.InteropServices;
// 声明一个COM接口 IMediaControl
[Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"),
InterfaceType(ComInterfaceType.InterfaceIsDual)]
interface IMediaControl // 这里不能列出任何基接口
{
void Run();
void Pause();
void Stop();
void GetState( [In] int msTimeout, [Out] out int pfs);
void RenderFile(
[In, MarshalAs(UnmanagedType.BStr)] string strFilename);
void AddSourceFilter(
[In, MarshalAs(UnmanagedType.BStr)] string strFilename,
[Out, MarshalAs(UnmanagedType.Interface)] out object ppUnk);
[return : MarshalAs(UnmanagedType.Interface)]
object FilterCollection();
[return : MarshalAs(UnmanagedType.Interface)]
object RegFilterCollection();
void StopWhenReady();
}
若要防止 HRESULT 翻译为 COMException,请在 C# 声明中将 PreserveSig(true) 属性附加到方法。
下面是一个使用C# 映射媒体播放机COM 对象的程序。
程序清单2 DemonCOM.cs
using System;
using System.Runtime.InteropServices;
namespace QuartzTypeLib
{
//声明一个COM接口 IMediaControl,此接口来源于媒体播放机COM类
[Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"),
InterfaceType(ComInterfaceType.InterfaceIsDual)]
interface IMediaControl
{ //列出接口成员
void Run();
void Pause();
void Stop();
void GetState( [In] int msTimeout, [Out] out int pfs);
void RenderFile(
[In, MarshalAs(UnmanagedType.BStr)] string strFilename);
void AddSourceFilter(
[In, MarshalAs(UnmanagedType.BStr)] string strFilename,
[Out, MarshalAs(UnmanagedType.Interface)]
out object ppUnk);
[return: MarshalAs(UnmanagedType.Interface)]
object FilterCollection();
[return: MarshalAs(UnmanagedType.Interface)]
object RegFilterCollection();
void StopWhenReady();
}
//声明一个COM类:
[ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")]
class FilgraphManager //此类不能再继承其它基类或接口
{
//这里不能有任何代码 ,系统自动增加一个缺省的构造函数
}
}
class MainClass
{
public static void Main(string[] args)
{
//命令行参数:
if (args.Length != 1)
{
DisplayUsage();
return;
}
String filename = args[0];
if (filename.Equals("/?"))
{
DisplayUsage();
return;
}
// 声明FilgraphManager的实类对象:
QuartzTypeLib.FilgraphManager graphManager =new QuartzTypeLib.FilgraphManager();
//声明IMediaControl的实类对象::
QuartzTypeLib.IMediaControl mc =(QuartzTypeLib.IMediaControl)graphManager;
// 调用COM的方法:
mc.RenderFile(filename);
//运行文件.
mc.Run();
//暂借停.
Console.WriteLine("Press Enter to continue.");
Console.ReadLine();
}
private static void DisplayUsage()
{ // 显示
Console.WriteLine("媒体播放机: 播放 AVI 文件.");
Console.WriteLine("使用方法: VIDEOPLAYER.EXE 文件名");
}
}
运行示例:
若要显示影片示例 Clock.avi,请使用以下命令:
interop2 %windir%\clock.avi
这将在屏幕上显示影片,直到按 ENTER 键停止。
在 .NET 框架程序中通过DllImport使用 Win32 API
.NET 框架程序可以通过静态 DLL 入口点的方式来访问本机代码库。DllImport 属性用于指定包含外部方法的实现的dll 位置。
DllImport 属性定义如下:
namespace System.Runtime.InteropServices
{
[AttributeUsage(AttributeTargets.Method)]
public class DllImportAttribute: System.Attribute
{
public DllImportAttribute(string dllName) {...}
public CallingConvention CallingConvention;
public CharSet CharSet;
public string EntryPoint;
public bool ExactSpelling;
public bool PreserveSig;
public bool SetLastError;
public string Value { get {...} }
}
}
说明:
1、DllImport只能放置在方法声明上。
2、DllImport具有单个定位参数:指定包含被导入方法的 dll 名称的 dllName 参数。
3、DllImport具有五个命名参数:
a、CallingConvention 参数指示入口点的调用约定。如果未指定 CallingConvention,则使用默认值 CallingConvention.Winapi。
b、CharSet 参数指示用在入口点中的字符集。如果未指定 CharSet,则使用默认值 CharSet.Auto。
c、EntryPoint 参数给出 dll 中入口点的名称。如果未指定 EntryPoint,则使用方法本身的名称。
d、ExactSpelling 参数指示 EntryPoint 是否必须与指示的入口点的拼写完全匹配。如果未指定 ExactSpelling,则使用默认值 false。
e、PreserveSig 参数指示方法的签名应当被保留还是被转换。当签名被转换时,它被转换为一个具有 HRESULT 返回值和该返回值的一个名为 retval 的附加输出参数的签名。如果未指定 PreserveSig,则使用默认值 true。
f、SetLastError 参数指示方法是否保留 Win32"上一错误"。如果未指定 SetLastError,则使用默认值 false。
4、它是一次性属性类。
5、此外,用 DllImport 属性修饰的方法必须具有 extern 修饰符。
下面是 C# 调用 Win32 MessageBox 函数的示例:
using System;
using System.Runtime.InteropServices;
class MainApp
{ //通过DllImport引用user32.dll类。MessageBox来自于user32.dll类
[DllImport("user32.dll", EntryPoint="MessageBox")]
public static extern int MessageBox(int hWnd, String strMessage, String strCaption, uint uiType);
public static void Main()
{
MessageBox( 0, "您好,这是 PInvoke!", ".NET", 0 );
}
}
面向对象的编程语言几乎都用到了抽象类这一概念,抽象类为实现抽象事物提供了更大的灵活性。C#也不例外, C#通过覆盖虚接口的技术深化了抽象类的应用。欲了解这方面的知识,请看下一节-覆盖虚接口
第七节、覆盖虚接口
有时候我们需要表达一种抽象的东西,它是一些东西的概括,但我们又不能真正的看到它成为一个实体在我们眼前出现,为此面向对象的编程语言便有了抽象类的概念。C#作为一个面向对象的语言,必然也会引入抽象类这一概念。接口和抽象类使您可以创建组件交互的定义。通过接口,可以指定组件必须实现的方法,但不实际指定如何实现方法。抽象类使您可以创建行为的定义,同时提供用于继承类的一些公共实现。对于在组件中实现多态行为,接口和抽象类都是很有用的工具。
一个抽象类必须为类的基本类列表中列出的接口的所有成员提供实现程序。但是,一个抽象类被允许把接口方法映射到抽象方法中。例如
interface IMethods {
void F();
void G();
}
abstract class C: IMethods
{
public abstract void F();
public abstract void G();
}
这里, IMethods 的实现函数把F和G映射到抽象方法中,它们必须在从C派生的非抽象类中被覆盖。
注意显式接口成员实现函数不能是抽象的,但是显式接口成员实现函数当然可以调用抽象方法。例如
interface IMethods
{
void F();
void G();
}
abstract class C: IMethods
{
void IMethods.F() { FF(); }
void IMethods.G() { GG(); }
protected abstract void FF();
protected abstract void GG();
}
这里,从C派生的非抽象类要覆盖FF 和 GG, 因此提供了IMethods的实际实现程序。