C#接口编程接口

C#接口编程接口(一)
www.xyhhxx.com发布者:seo时间:2005-08-11

作者:黎宇

接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。有了这个协定,就可以抛开编程语言的限制(理论上)。接口可以从多个基接口继承,而类或结构可以实现多个接口。接口可以包含方法、属性、事件和索引器。接口本身不提供它所定义的成员的实现。接口只指定实现该接口的类或接口必须提供的成员。

接口好比一种模版,这种模版定义了对象必须实现的方法,其目的就是让这些方法可以作为接口实例被引用。接口不能被实例化。类可以实现多个接口并且通过这些实现的接口被索引。接口变量只能索引实现该接口的类的实例。例子:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IMyExample { 
    string this[int index] { get ; set ; } 
    event EventHandler Even ; 
    void Find(int value) ; 
    string Point { get ; set ; } 
} 
public delegate void EventHandler(object sender, Event e) ;


上面例子中的接口包含一个索引this、一个事件Even、一个方法Find和一个属性Point。

接口可以支持多重继承。就像在下例中,接口"IComboBox"同时从"ITextBox"和"IListBox"继承。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IControl { 
    void Paint( ) ; 
} 
interface ITextBox: IControl { 
    void SetText(string text) ; 
} 
interface IListBox: IControl { 
    void SetItems(string[] items) ; 
} 
interface IComboBox: ITextBox, IListBox { }


类和结构可以多重实例化接口。就像在下例中,类"EditBox"继承了类"Control",同时从"IDataBound"和"IControl"继承。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IDataBound { 
    void Bind(Binder b) ; 
} 
public class EditBox: Control, IControl, IDataBound { 
    public void Paint( ) ; 
    public void Bind(Binder b) {...} 
}


在上面的代码中,"Paint"方法从"IControl"接口而来;"Bind"方法从"IDataBound"接口而来,都以"public"的身份在"EditBox"类中实现。

说明:

· C#中的接口是独立于类来定义的。这与 C++模型是对立的,在 C++中接口实际上就是抽象基类。

· 接口和类都可以继承多个接口。

· 而类可以继承一个基类,接口根本不能继承类。这种模型避免了 C++的多继承问题,C++中不同基类中的实现可能出现冲突。因此也不再需要诸如虚拟继承和显式作用域这类复杂机制。C#的简化接口模型有助于加快应用程序的开发。

· 一个接口定义一个只有抽象成员的引用类型。C#中一个接口实际所做的,仅仅只存在着方法标志,但根本就没有执行代码。这就暗示了不能实例化一个接口,只能实例化一个派生自该接口的对象。

· 接口可以定义方法、属性和索引。所以,对比一个类,接口的特殊性是:当定义一个类时,可以派生自多重接口,而你只能可以从仅有的一个类派生。

接口与组件


接口描述了组件对外提供的服务。在组件和组件之间、组件和客户之间都通过接口进行交互。因此组件一旦发布,它只能通过预先定义的接口来提供合理的、一致的服务。这种接口定义之间的稳定性使客户应用开发者能够构造出坚固的应用。一个组件可以实现多个组件接口,而一个特定的组件接口也可以被多个组件来实现。

组件接口必须是能够自我描述的。这意味着组件接口应该不依赖于具体的实现,将实现和接口分离彻底消除了接口的使用者和接口的实现者之间的耦合关系,增强了信息的封装程度。同时这也要求组件接口必须使用一种与组件实现无关的语言。目前组件接口的描述标准是IDL语言。

由于接口是组件之间的协议,因此组件的接口一旦被发布,组件生产者就应该尽可能地保持接口不变,任何对接口语法或语义上的改变,都有可能造成现有组件与客户之间的联系遭到破坏。

每个组件都是自主的,有其独特的功能,只能通过接口与外界通信。当一个组件需要提供新的服务时,可以通过增加新的接口来实现。不会影响原接口已存在的客户。而新的客户可以重新选择新的接口来获得服务。

组件化程序设计


组件化程序设计方法继承并发展了面向对象的程序设计方法。它把对象技术应用于系统设计,对面向对象的程序设计的实现过程作了进一步的抽象。我们可以把组件化程序设计方法用作构造系统的体系结构层次的方法,并且可以使用面向对象的方法很方便地实现组件。

组件化程序设计强调真正的软件可重用性和高度的互操作性。它侧重于组件的产生和装配,这两方面一起构成了组件化程序设计的核心。组件的产生过程不仅仅是应用系统的需求,组件市场本身也推动了组件的发展,促进了软件厂商的交流与合作。组件的装配使得软件产品可以采用类似于搭积木的方法快速地建立起来,不仅可以缩短软件产品的开发周期,同时也提高了系统的稳定性和可靠性。

组件程序设计的方法有以下几个方面的特点:

· 编程语言和开发环境的独立性.

· 组件位置的透明性.

· 组件的进程透明性.

· 可扩充性.

· 可重用性.

· 具有强有力的基础设施.

· 系统一级的公共服务.

C#语言由于其许多优点,十分适用于组件编程。但这并不是说C#是一门组件编程语言,也不是说C#提供了组件编程的工具。我们已经多次指出,组件应该具有与编程语言无关的特性。请读者记住这一点:组件模型是一种规范,不管采用何种程序语言设计组件,都必须遵守这一规范。比如组装计算机的例子,只要各个厂商为我们提供的配件规格、接口符合统一的标准,这些配件组合起来就能协同工作,组件编程也是一样。我们只是说,利用C#语言进行组件编程将会给我们带来更大的方便。

知道了什么是接口,接下来就是怎样定义接口,这部分内容我们将在下一节——定义接口中详细讲述。
从技术上讲,接口是一组包含了函数型方法的数据结构。通过这组数据结构,客户代码可以调用组件对象的功能。

定义接口的一般形式为:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>[attributes] [modifiers] interface identifier [:base-list] {interface-body}[;]


说明:

· attributes(可选):附加的定义性信息。

· modifiers(可选):允许使用的修饰符有new和四个访问修饰符。分别是:new、public、protected、internal、private。在一个接口定义中同一修饰符不允许出现多次,new修饰符只能出现在嵌套接口中,表示覆盖了继承而来的同名成员。The public, protected, internal, and private修饰符定义了对接口的访问权限。

· 指示器和事件。

· identifier:接口名称。

· base-list(可选):包含一个或多个显式基接口的列表,接口间由逗号分隔。

· interface-body:对接口成员的定义。

· 接口可以是命名空间或类的成员,并且可以包含下列成员的签名: 方法、属性、索引器 。

· 一个接口可从一个或多个基接口继承。

接口这个概念在C#和Java中非常相似。接口的关键词是interface,一个接口可以扩展一个或者多个其他接口。按照惯例,接口的名字以大写字母"I"开头。下面的代码是C#接口的一个例子,它与Java中的接口完全一样:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IShape  { 
    void Draw ( ) ;
}


如果你从两个或者两个以上的接口派生,父接口的名字列表用逗号分隔,如下面的代码所示:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface INewInterface: IParent1, IParent2 { }


然而,与Java不同,C#中的接口不能包含域(Field)。另外还要注意,在C#中,接口内的所有方法默认都是公用方法。在Java中,方法定义可以带有public修饰符(即使这并非必要),但在C#中,显式为接口的方法指定public修饰符是非法的。例如,下面的C#接口将产生一个编译错误。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IShape { public void Draw( ) ; }


下面的例子定义了一个名为IControl 的接口,接口中包含一个成员方法Paint:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IControl {
void Paint( ) ;
}


在下例中,接口 IInterface从两个基接口 IBase1 和 IBase2 继承:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IInterface: IBase1, IBase2 {
   void Method1( ) ;
   void Method2( ) ;
}


接口可由类实现。实现的接口的标识符出现在类的基列表中。例如:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>class Class1: Iface1, Iface2 {
   // class 成员。
}


类的基列表同时包含基类和接口时,列表中首先出现的是基类。例如:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>class ClassA: BaseClass, Iface1, Iface2 {
   // class成员。
}


以下的代码段定义接口IFace,它只有一个方法:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IFace {
  void ShowMyFace( ) ;
}


不能从这个定义实例化一个对象,但可以从它派生一个类。因此,该类必须实现ShowMyFace抽象方法:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>class CFace:IFace
{
  public void ShowMyFace( )   {
    Console.WriteLine(" implementation " ) ;
   } 
}


基接口


一个接口可以从零或多个接口继承,那些被称为这个接口的显式基接口。当一个接口有比零多的显式基接口时,那么在接口的定义中的形式为,接口标识符后面跟着由一个冒号":"和一个用逗号","分开的基接口标识符列表。

接口基:

接口类型列表说明:

· 一个接口的显式基接口必须至少同接口本身一样可访问。例如,在一个公共接口的基接口中指定一个私有或内部的接口是错误的。

· 一个接口直接或间接地从它自己继承是错误的。

· 接口的基接口都是显式基接口,并且是它们的基接口。换句话说,基接口的集合完全由显式基接口和它们的显式基接口等等组成。在下面的例子中

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IControl {
	void Paint( ) ;
}
interface ITextBox: IControl {
	void SetText(string text) ;
}
interface IListBox: IControl {
	void SetItems(string[] items) ;
}
interface IComboBox: ITextBox, IListBox { }


IComboBox 的基接口是IControl, ITextBox, 和 IlistBox。

· 一个接口继承它的基接口的所有成员。换句话说,上面的接口IComboBox就像Paint一样继承成员SetText 和 SetItems。

· 一个实现了接口的类或结构也隐含地实现了所有接口的基接口。

接口主体

一个接口的接口主体定义接口的成员。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface-body:
{   interface-member-declarationsopt   }
接口可以包含一个和多个成员,这些成员可以是方法、属性、索引指示器和事件,但不能是常量、域、操作符、构造函数或析构函数,而且不能包含任何静态成员。接口定义创建新的定义空间,并且接口定义直接包含的接口成员定义将新成员引入该定义空间。

说明:

· 接口的成员是从基接口继承的成员和由接口本身定义的成员。

· 接口定义可以定义零个或多个成员。接口的成员必须是方法、属性、事件或索引器。接口不能包含常数、字段、运算符、实例构造函数、析构函数或类型,也不能包含任何种类的静态成员。

· 定义一个接口,该接口对于每种可能种类的成员都包含一个:方法、属性、事件和索引器。

· 接口成员默认访问方式是public。接口成员定义不能包含任何修饰符,比如成员定义前不能加abstract,public,protected,internal,private,virtual,override 或static 修饰符。

· 接口的成员之间不能相互同名。继承而来的成员不用再定义,但接口可以定义与继承而来的成员同名的成员,这时我们说接口成员覆盖了继承而来的成员,这不会导致错误,但编译器会给出一个警告。关闭警告提示的方式是在成员定义前加上一个new关键字。但如果没有覆盖父接口中的成员,使用new关键字会导致编译器发出警告。

· 方法的名称必须与同一接口中定义的所有属性和事件的名称不同。此外,方法的签名必须与同一接口中定义的所有其他方法的签名不同。

· 属性或事件的名称必须与同一接口中定义的所有其他成员的名称不同。

· 一个索引器的签名必须区别于在同一接口中定义的其他所有索引器的签名。

· 接口方法声明中的属性(attributes), 返回类型(return-type), 标识符(identifier)和形式参数列表(formal-parameter-lis)与一个类的方法声明中的那些有相同的意义。一个接口方法声明不允许指定一个方法主体,而声明通常用一个分号结束。

· 接口属性声明的访问符与类属性声明的访问符相对应,除了访问符主体通常必须用分号。因此,无论属性是读写、只读或只写,访问符都完全确定。

· 接口索引声明中的属性(attributes),类型(type)和形式参数列表(formal-parameter-list)与类的索引声明的那些有相同的意义。

下面例子中接口IMyTest包含了索引指示器、事件E、方法F、属性P这些成员:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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包含每个可能类型成员的接口:一个方法,一个属性,一个事件和一个索引。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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)。接口的全权名称是这样构成的。接口名加小圆点"." 再跟成员名比如对于下面两个接口:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IControl {
    void Paint( ) ;
}
interface ITextBox: IControl {
    void GetText(string text) ;
}


其中Paint 的全权名是IControl.Paint,GetText的全权名是ITextBox. GetText。当然,全权名中的成员名称必须是在接口中已经定义过的,比如使用ITextBox.Paint.就是不合理的。

如果接口是名字空间的成员,全权名还必须包含名字空间的名称。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>namespace System
{
    public interface IDataTable {
        object Clone( ) ;
    }
}


那么Clone方法的全权名是System. IDataTable.Clone。

定义好了接口,接下来我们关心的就是怎样实现对接口的访问。这部分内容,我将在下一篇文章中和您进一步探讨。

二.
对接口成员的访问


对接口方法的调用和采用索引指示器访问的规则与类中的情况也是相同的。如果底层成员的命名与继承而来的高层成员一致,那么底层成员将覆盖同名的高层成员。但由于接口支持多继承,在多继承中,如果两个父接口含有同名的成员,这就产生了二义性(这也正是C#中取消了类的多继承机制的原因之一),这时需要进行显式的定义:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>using System ;
interface ISequence {
    int Count { get; set; }
}
interface IRing {
    void Count(int i) ;
}
interface IRingSequence: ISequence, IRing { }
class CTest {
    void Test(IRingSequence rs) {
        //rs.Count(1) ;  错误, Count 有二义性
        //rs.Count = 1;   错误, Count 有二义性
        ((ISequence)rs).Count = 1;  // 正确
((IRing)rs).Count(1) ; // 正确调用IRing.Count
    }
}


上面的例子中,前两条语句rs.Count(1)和rs.Count=1会产生二义性,从而导致编译时错误,因此必须显式地给rs 指派父接口类型,这种指派在运行时不会带来额外的开销。

再看下面的例子:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>using System ;
interface IInteger {
    void Add(int i) ;
}
interface IDouble {
    void Add(double d) ;
}
interface INumber: IInteger, IDouble {}
class CMyTest {
    void Test(INumber Num) {
        // Num.Add(1) ; 错误
        Num.Add(1.0) ; // 正确
        ((IInteger)n).Add(1) ; // 正确
        ((IDouble)n).Add(1) ; // 正确
    }
}


调用Num.Add(1) 会导致二义性,因为候选的重载方法的参数类型均适用。但是,调用Num.Add(1.0) 是允许的,因为1.0是浮点数参数类型与方法IInteger.Add()的参数类型不一致,这时只有IDouble.Add 才是适用的。不过只要加入了显式的指派,就决不会产生二义性。

接口的多重继承的问题也会带来成员访问上的问题。例如:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IBase {
    void FWay(int i) ;
}
interface ILeft: IBase {
    new void FWay (int i) ;
}
interface IRight: IBase
{  void G( ) ; }
interface IDerived: ILeft, IRight { }
class CTest {
    void Test(IDerived d) {
        d. FWay (1) ; // 调用ILeft. FWay
        ((IBase)d). FWay (1) ; // 调用IBase. FWay
        ((ILeft)d). FWay (1) ; // 调用ILeft. FWay
        ((IRight)d). FWay (1) ; // 调用IBase. FWay
    }
}


上例中,方法IBase.FWay在派生的接口ILeft中被Ileft的成员方法FWay覆盖了。所以对d. FWay (1)的调用实际上调用了。虽然从IBase-> IRight-> IDerived这条继承路径上来看,ILeft.FWay方法是没有被覆盖的。我们只要记住这一点:一旦成员被覆盖以后,所有对其的访问都被覆盖以后的成员"拦截"了。

类对接口的实现


前面我们已经说过,接口定义不包括方法的实现部分。接口可以通过类或结构来实现。我们主要讲述通过类来实现接口。用类来实现接口时,接口的名称必须包含在类定义中的基类列表中。

下面的例子给出了由类来实现接口的例子。其中ISequence为一个队列接口,提供了向队列尾部添加对象的成员方法Add( ),IRing 为一个循环表接口,提供了向环中插入对象的方法Insert(object obj),方法返回插入的位置。类RingSquence 实现了接口ISequence 和接口IRing。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>using System ;
interface ISequence {
    object Add( ) ;
}
interface ISequence {
    object Add( ) ;
}
interface IRing {
    int Insert(object obj) ;
}
class RingSequence: ISequence, IRing
{
    public object Add( ) {…}
    public int Insert(object obj) {…}
}


如果类实现了某个接口,类也隐式地继承了该接口的所有父接口,不管这些父接口有没有在类定义的基类表中列出。看下面的例子:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>using System ;
interface IControl {
	void Paint( );
}
interface ITextBox: IControl {
	void SetText(string text);
}
interface IListBox: IControl {
	void SetItems(string[] items);
}
interface IComboBox: ITextBox, IListBox { }


这里,接口IcomboBox继承了ItextBox和IlistBox。类TextBox不仅实现了接口ITextBox,还实现了接口ITextBox 的父接口IControl。

前面我们已经看到,一个类可以实现多个接口。再看下面的例子:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IDataBound {
	void Bind(Binder b);
}
public class EditBox: Control, IControl, IDataBound {
	public void Paint( );
	public void Bind(Binder b) {...}
}


类EditBox从类Control中派生并且实现了Icontrol和IdataBound。在前面的例子中接口Icontrol中的Paint方法和IdataBound接口中的Bind方法都用类EditBox中的公共成员实现。C#提供一种实现这些方法的可选择的途径,这样可以使执行这些的类避免把这些成员设定为公共的。接口成员可以用有效的名称来实现。例如,类EditBox可以改作方法Icontrol.Paint和IdataBound.Bind来来实现。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>public class EditBox: IControl, IDataBound {
	void IControl.Paint( ) {...}
	void IDataBound.Bind(Binder b) {...}
}


因为通过外部指派接口成员实现了每个成员,所以用这种方法实现的成员称为外部接口成员。外部接口成员可以只是通过接口来调用。例如,Paint方法中EditBox的实现可以只是通过创建Icontrol接口来调用。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>class Test {
	static void Main( ) {
		EditBox editbox = new EditBox( );
		editbox.Paint( );	//错误: EditBox 没有Paint 事件
		IControl control = editbox;
		control.Paint( );	// 调用 EditBox的Paint事件
	}
}


上例中,类EditBox 从Control 类继承并同时实现了IControl and IDataBound 接口。EditBox 中的Paint方法来自IControl接口,Bind方法来自IDataBound接口,二者在EditBox类中都作为公有成员实现。当然,在C# 中我们也可以选择不作为公有成员实现接口。

如果每个成员都明显地指出了被实现的接口,通过这种途径被实现的接口我们称之为显式接口成员(explicit interface member)。 用这种方式我们改写上面的例子:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>public class EditBox: IControl, IDataBound {
    void IControl.Paint( ) {…}
    void IDataBound.Bind(Binder b) {…}
}


显式接口成员只能通过接口调用。例如:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>class CTest {
    static void Main( ) {
        EditBox editbox = new EditBox( ) ;
        editbox.Paint( ) ; //错误:不同的方法
        IControl control = editbox;
        control.Paint( ) ; //调用 EditBox的Paint方法
    }
}



上述代码中对editbox.Paint()的调用是错误的,因为editbox本身并没有提供这一方法。control.Paint( )是正确的调用方式。

注释:接口本身不提供所定义的成员的实现,它仅仅说明这些成员,这些成员必须依靠实现接口的类或其它接口的支持。

知道了怎样访问接口,我们还要知道怎样实现接口

接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。在前面的文章中,我们已经对C#接口的概念,如何定义接口以及如何对接口进行访问等问题进行了详细的讨论。在这些知识的基础上,本文我们将来了解实现接口的方法。

显式实现接口成员



为了实现接口,类可以定义显式接口成员执行体(Explicit interface member

implementations)。显式接口成员执行体可以是一个方法、一个属性、一个事件或者是一个索引指示器的定义,定义与该成员对应的全权名应保持一致。

<ccid_nobr></ccid_nobr>

<ccid_code></ccid_code>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 就是显式接口成员执行体。

说明:

· 不能在方法调用、属性访问以及索引指示器访问中通过全权名访问显式接口成员执行体。事实上,显式接口成员执行体只能通过接口的实例,仅仅引用接口的成员名称来访问。

· 显式接口成员执行体不能使用任何访问限制符,也不能加上abstract, virtual, override或static 修饰符。

· 显式接口成员执行体和其他成员有着不同的访问方式。因为不能在方法调用、属性访问以及索引指示器访问中通过全权名访问,显式接口成员执行体在某种意义上是私有的。但它们又可以通过接口的实例访问,也具有一定的公有性质。

· 只有类在定义时,把接口名写在了基类列表中,而且类中定义的全权名、类型和返回类型都与显式接口成员执行体完全一致时,显式接口成员执行体才是有效的,例如:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>class Shape: ICloneable {
    object ICloneable.Clone( ) {…}
    int IComparable.CompareTo(object other) {…}
}


使用显式接口成员执行体通常有两个目的:

· 因为显式接口成员执行体不能通过类的实例进行访问,这就可以从公有接口中把接口的实现部分单独分离开。如果一个类只在内部使用该接口,而类的使用者不会直接使用到该接口,这种显式接口成员执行体就可以起到作用。

· 显式接口成员执行体避免了接口成员之间因为同名而发生混淆。如果一个类希望对名称和返回类型相同的接口成员采用不同的实现方式,这就必须要使用到显式接口成员执行体。如果没有显式接口成员执行体,那么对于名称和返回类型不同的接口成员,类也无法进行实现。

下面的定义是无效的,因为Shape 定义时基类列表中没有出现接口IComparable。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>class Shape: ICloneable
{
    object ICloneable.Clone( ) {…}
}
class Ellipse: Shape
{
    object ICloneable.Clone( ) {…}
}


在Ellipse中定义ICloneable.Clone是错误的,因为Ellipse即使隐式地实现了接口ICloneable,ICloneable仍然没有显式地出现在Ellipse定义的基类列表中。

接口成员的全权名必须对应在接口中定义的成员。如下面的例子中,Paint的显式接口成员执行体必须写成IControl.Paint。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>程序清单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 方法:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>public float Length( ) {
   return lengthInches ;
}
public float Width( ){
   return widthInches;
}
float IMetricDimensions.Length( ) {
   return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
   return widthInches * 2.54f ;
}


这种情况下,可以从类实例访问英制单位,而从接口实例访问公制单位:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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( )) ;


继承接口实现



接口具有不变性,但这并不意味着接口不再发展。类似于类的继承性,接口也可以继承和发展。

注意:接口继承和类继承不同,首先,类继承不仅是说明继承,而且也是实现继承;而接口继承只是说明继承。也就是说,派生类可以继承基类的方法实现,而派生的接口只继承了父接口的成员方法说明,而没有继承父接口的实现,其次,C#中类继承只允许单继承,但是接口继承允许多继承,一个子接口可以有多个父接口。

接口可以从零或多个接口中继承。从多个接口中继承时,用":"后跟被继承的接口名字,多个接口名之间用","分割。被继承的接口应该是可以访问得到的,比如从private类型或internal类型的接口中继承就是不允许的。接口不允许直接或间接地从自身继承。和类的继承相似,接口的继承也形成接口之间的层次结构。

请看下面的例子:

<ccid_nobr></ccid_nobr>

<ccid_code></ccid_code>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方法。

一个类继承了所有被它的基本类提供的接口实现程序。

不通过显式的实现一个接口,一个派生类不能用任何方法改变它从它的基本类继承的接口映射。例如,在声明中:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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将会有下面的影响。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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( ) ;


但是,当一个接口方法被映射到一个类中的虚拟方法,派生类就不可能覆盖这个虚拟方法并且改变接口的实现函数。例如,把上面的声明重新写为:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IControl {
	void Paint( ) ;
}
class Control: IControl {
	public virtual void Paint( ) {...}
}
class TextBox: Control {
	public override void Paint( ) {...}
}


就会看到下面的结果:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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( );


由于显式接口成员实现程序不能被声明为虚拟的,就不可能覆盖一个显式接口成员实现程序。一个显式接口成员实现程序调用另外一个方法是有效的,而另外的那个方法可以被声明为虚拟的以便让派生类可以覆盖它。例如:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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的实现程序进行特殊化。
重新实现接口



我们已经介绍过,派生类可以对基类中已经定义的成员方法进行重载。类似的概念引入到类对接口的实现中来,叫做接口的重实现(re-implementation)。继承了接口实现的类可以对接口进行重实现。这个接口要求是在类定义的基类列表中出现过的。对接口的重实现也必须严格地遵守首次实现接口的规则,派生的接口映射不会对为接口的重实现所建立的接口映射产生任何影响。

下面的代码给出了接口重实现的例子:

<ccid_nobr></ccid_nobr>

<ccid_code></ccid_code>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 之上。

在接口的重实现时,继承而来的公有成员定义和继承而来的显式接口成员的定义参与到接口映射的过程。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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。前面我们说过,类在实现一个接口时,同时隐式地实现了该接口的所有父接口。同样,类在重实现一个接口时同时,隐式地重实现了该接口的所有父接口。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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。

映射接口



类必须为在基类表中列出的所有接口的成员提供具体的实现。在类中定位接口成员的实现称之为接口映射(interface mapping )。

映射,数学上表示一一对应的函数关系。接口映射的含义也是一样,接口通过类来实现,那么对于在接口中定义的每一个成员,都应该对应着类的一个成员来为它提供具体的实现。

类的成员及其所映射的接口成员之间必须满足下列条件:

· 如果A和B都是成员方法,那么A和B的名称、类型、形参表(包括参数个数和每一个参数的类型)都应该是一致的。

· 如果A和B都是属性,那么A和B的名称、类型应当一致,而且A和B的访问器也是类似的。但如果A不是显式接口成员执行体,A允许增加自己的访问器。

· 如果A和B都是时间那么A和B的名称、类型应当一致。

· 如果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:

<ccid_nobr></ccid_nobr>

<ccid_code></ccid_code>interface Interface1 {
    void F( ) ;
}
class Class1 {
    public void F( ) { }
    public void G( ) { }
}
class Class2: Class1, Interface1 {
    new public void G( ) {}
}


注意:接口的成员包括它自己定义的成员,而且包括该接口所有父接口定义的成员。在接口映射时,不仅要对接口定义体中显式定义的所有成员进行映射,而且要对隐式地从父接口那里继承来的所有接口成员进行映射。

在进行接口映射时,还要注意下面两点:

· 在决定由类中的哪个成员来实现接口成员时,类中显式说明的接口成员比其它成员优先实现。

· 使用Private、protected和static修饰符的成员不能参与实现接口映射。例如:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface ICloneable {
    object Clone( ) ;
}
class C: ICloneable {
    object ICloneable.Clone( ) {…}
    public object Clone( ) {…}
}


例子中成员ICloneable.Clone称为接口ICloneable的成员Clone的实现者,因为它是显式说明的接口成员,比其它成员有着更高的优先权。

如果一个类实现了两个或两个以上名字、类型和参数类型都相同的接口,那么类中的一个成员就可能实现所有这些接口成员:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IControl {
    void Paint( ) ;
}
interface IForm {
    void Paint( ) ;
}
class Page: IControl, IForm {
    public void Paint( ) {…}
}


这里,接口IControl和IForm的方法Paint都映射到了类Page中的Paint方法。当然也可以分别用显式的接口成员分别实现这两个方法:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IControl {
    void Paint( ) ;
}
interface IForm {
    void Paint( ) ;
}
class Page: IControl, IForm {
    public void IControl.Paint( ) {
        //具体的接口实现代码
    }
    public void IForm.Paint( ) {
        //具体的接口实现代码
    }
}


上面的两种写法都是正确的。但是如果接口成员在继承中覆盖了父接口的成员,那么对该接口成员的实现就可能必须映射到显式接口成员执行体。看下面的例子:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>interface IBase {
    int P { get; }
}
interface IDerived: IBase {
    new int P( ) ;
}


接口IDerived从接口IBase中继承,这时接口IDerived的成员方法覆盖了父接口的成员方法。因为这时存在着同名的两个接口成员,那么对这两个接口成员的实现如果不采用显式接口成员执行体,编译器将无法分辨接口映射。所以,如果某个类要实现接口IDerived,在类中必须至少定义一个显式接口成员执行体。采用下面这些写法都是合理的:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>//一:对两个接口成员都采用显式接口成员执行体来实现
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( ){
//具体的接口实现代码}
}


另一种情况是,如果一个类实现了多个接口,这些接口又拥有同一个父接口,这个父接口只允许被实现一次。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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组件常包括下面几个方面的问题:

· 创建 COM 对象。

· 确定 COM 接口是否由对象实现。

· 调用 COM 接口上的方法。

· 实现可由 COM 客户端调用的对象和接口。

创建 COM 类包装

要使 C# 代码引用COM 对象和接口,需要在 C# 中包含 COM 接口的定义。完成此操作的最简单方法是使用 TlbImp.exe(类型库导入程序),它是一个包括在 .NET 框架 SDK 中的命令行工具。TlbImp 将 COM 类型库转换为 .NET 框架元数据,从而有效地创建一个可以从任何托管语言调用的托管包装。用 TlbImp 创建的 .NET 框架元数据可以通过 /R 编译器选项包括在 C# 内部版本中。如果使用 Visual Studio 开发环境,则只需添加对 COM 类型库的引用,将为您自动完成此转换。

TlbImp 执行下列转换:

· COM coclass 转换为具有无参数构造函数的 C# 类。

· COM 结构转换为具有公共字段的 C# 结构。

检查 TlbImp 输出的一种很好的方法是运行 .NET 框架 SDK 命令行工具 Ildasm.exe(Microsoft 中间语言反汇编程序)来查看转换结果。

虽然 TlbImp 是将 COM 定义转换为 C# 的首选方法,但也不是任何时候都可以使用它(例如,在没有 COM 定义的类型库时或者 TlbImp 无法处理类型库中的定义时,就不能使用该方法)。在这些情况下,另一种方法是使用 C# 属性在 C# 源代码中手动定义 COM 定义。创建 C# 源映射后,只需编译 C# 源代码就可产生托管包装。

执行 COM 映射需要理解的主要属性包括:

· ComImport:它将类标记为在外部实现的 COM 类。

· Guid:它用于为类或接口指定通用唯一标识符 (UUID)。

· InterfaceType,它指定接口是从 IUnknown 还是从 IDispatch 派生。

· PreserveSig,它指定是否应将本机返回值从 HRESULT 转换为 .NET 框架异常。

声明 COM coclass

COM coclass 在 C# 中表示为类。这些类必须具有与其关联的 ComImport 属性。下列限制适用于这些类:

· 类不能从任何其他类继承。

· 类不能实现任何接口。

· 类还必须具有为其设置全局唯一标识符 (GUID) 的 Guid 属性。

以下示例在 C# 中声明一个 coclass:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>// 声明一个COM类 FilgraphManager
 [ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")] 
class FilgraphManager
{ }


C# 编译器将添加一个无参数构造函数,可以调用此构造函数来创建 COM coclass 的实例。

创建 COM 对象

COM coclass 在 C# 中表示为具有无参数构造函数的类。使用 new 运算符创建该类的实例等效于在 C# 中调用 CoCreateInstance。使用以上定义的类,就可以很容易地实例化此类:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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#程序:

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>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 对象的程序。

<ccid_nobr></ccid_nobr>
<ccid_code></ccid_code>程序清单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  
    { //列出接口成
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值