本文讨论C#中的接口与实现、抽象类、接口和类的继承,以及对象类型的判断和转换等内容。
接口(interface)可以看作是一系列类的定义标准,在接口中可以指定这些类必须实现的属性、方法、事件和索引器;通过接口定义的成员在类中的访问级别都是公共的(public),但在接口中定义成员时不需要使用访问级别的关键字。此外,定义接口类型需要使用interface关键字,接口名称习惯上使用大写字母I开始。下面的代码演示了接口的定义,并通过类来实现接口。
using System; namespace csfx_demo { // interface I1 { string Id { get; set; } void M1(); } // class C1 : I1 { public string Id { get; set; } public void M1() { Console.WriteLine("C1.M1, ID:{0}", Id); } } // class Program { static void Main(string[] args) { C1 c = new C1() { Id = "123" }; c.M1(); } } }
代码中首先定义了I1接口,其中包含了Id属性和M1()方法。接下来,使用C1类实现此接口,在C1类名后的冒号(:)在这里的含义为实现(implement)。C1类中,M1()方法中会显示所在类和方法名,以及Id属性的值。最后,在Main()方法中创建C1类的实例并赋值到c对象,设置Id属性为"123",并调用M1()方法显示信息,执行代码会显示“C1.M1, ID:123”。
定义新的接口时,可以从现有接口类型继承,一个接口可以同时继承多个接口类型;下面的代码演示了相关应用。
using System; namespace csfx_demo { // interface I1 { string Id { get; set; } void M1(); } // interface I2 { string Name { get; set; } void M2(); } // interface I3 : I1, I2 { void M3(); } // class C1 : I3 { public string Id { get; set; } public string Name { get; set; } public void M1() { Console.WriteLine("C1.M1, ID:{0}", Id); } // public void M2() { Console.WriteLine("C1.M2, Name:{0}", Name); } // public void M3() { Console.WriteLine("C1.M3"); } } // class Program { static void Main(string[] args) { C1 c = new C1() { Id = "123", Name = "abc" }; c.M1(); c.M2(); c.M3(); } } }
代码中,首先定义了I1和I2接口,然后定义的I3接口会同时继承I1和I2接口,在I3接口名称后的冒号(:)含义为继承(inherit),继承的多个接口类型使用逗号分隔。接下来的代码,使用C1类实现I3接口,需要实现I1、I2和I3接口中的所有成员。执行代码结果如下图所示
类可以同时实现多个接口,此时需要注意不同接口类型中的同名成员,如下面的代码。
using System; namespace csfx_demo { // interface I1 { string Id { get; set; } void M1(); } // interface I2 { void M1(); void M2(); } // class C1 : I1, I2 { public string Id { get; set; } // public void M1() { Console.WriteLine("I1(I2).M1, Id:{0}", Id); } // public void M2() { Console.WriteLine("I2.M2"); } } // class Program { static void Main(string[] args) { C1 c = new C1() { Id = "123" }; c.M1(); c.M2(); } } }
代码中的C1类同时实现了I1和I2两个接口类型,需要实现Id属性,M1()和M2()方法;这里应注意,I1和I2接口中都包含了M1()方法。
通过继承,可以最大限度的实现代码的重复使用,像接口的继承一样,当一个类继承某个类时使用冒号(:)声明。需要注意的是,类只能继承一个类,不能同时继承多个类。在类的继承关系中,继承类称为子类,被继承的类称为基类(也称为父类、超类)。下面的代码演示了类的继承。
using System; namespace csfx_demo { // class C1 { public string Id { get; set; } // public void M1() { Console.WriteLine("C1.M1"); } // public virtual void M2() { Console.WriteLine("C1.M2"); } } // class C1a : C1 { new public void M1() { base.M1(); Console.WriteLine("C1a.M1, Id:{0}", Id); } // public override void M2() { base.M2(); Console.WriteLine("C1a.M2"); } } // class Program { static void Main(string[] args) { C1a c = new C1a() { Id = "123" }; c.M1(); c.M2(); } } }
代码中,首先定义了C1类,它包含Id属性、M1()和M2()方法,其中,M2()方法定义为虚拟(virtual)方法,可以在子类中重写。
C1a类定义为C1类的子类,此时C1类称为C1a类的基类。由于C1类中的M1()方法不是虚拟(virtual)方法,也不是抽象(abstract)方法(稍后讨论),在C1a类中重新实现方法时需要使用new关键字覆盖基类中的定义,其中,使用base关键字调用了C1类中的M1()方法。由于C1类中的M2()方法定义为虚拟方法,可以在C1a类中使用override关键字重写它,方法中还使用base关键字调用了基类中的M2()方法;请注意,子类中可以调用基类中除了私有(private)成员以外的其它成员,即可以调用protected、public、internal关键字定义的成员。
Main()方法中创建了C1a类的实例并赋值给c对象,然后调用了M1()和M2()方法,执行结果如下图所示。
某些情况下,有些类不允许实例化,或者其中有些成员暂时无法确认具体的实现,此时可以将类定义为抽象类,没有具体实现的成员定义为抽象成员;请注意,抽象成员必须定义在抽象类中。C#中定义抽象类和抽象成员都需要使用abstract关键字,如下面的代码。
using System; namespace csfx_demo { // abstract class C1 { public string Id { get; set; } // public abstract void M1(); // public virtual void M2() { Console.WriteLine("C1.M2"); } } // class C1a : C1 { public override void M1() { Console.WriteLine("C1a.M1, Id:{0}", Id); } // } // class Program { static void Main(string[] args) { C1a c = new C1a(); c.M1(); c.M2(); } } }
代码中,首先定义了抽象类C1,其中的M1()方法定义为抽象方法,请注意,抽象方法不需要使用花括号定义方法体;M2()方法定义为虚拟(virtual)方法。接下来的C1a类继承于C1类,其中使用override关键字重写了M1()方法。执行结果如下图的所示。
对于抽象类中的抽象成员和虚拟成员,都可以在子类中使用override关键字重写,如下面的代码。
using System; namespace csfx_demo { // abstract class C1 { public string Id { get; set; } // public abstract void M1(); // public virtual void M2() { Console.WriteLine("C1.M2"); } } // class C1a : C1 { public override void M1() { Console.WriteLine("C1a.M1, Id:{0}", Id); } // public override void M2() { base.M2(); Console.WriteLine("C1a.M2"); } } // class Program { static void Main(string[] args) { C1a c = new C1a(); c.M1(); c.M2(); } } }
代码执行结果如下图所示。
抽象类型和接口都可以做为一系列子类的标准,但抽象类是一个具体的类型,相比之下,组件传递时使用接口会更加灵活,就像生活中的电源插座接口、USB接口等,只要设备匹配就能灵活应用。此外,接口成员没有具体的实现,抽象类则允许实现一些成员,在子类中可以直接使用这些成员。
除了使用对象的GetType()方法或typeof运算符获取类型的Type对象,还可以使用is运算符判断对象是否可以转换为指定的类型,运算结果为bool类型,可以转换为目标类型时返回true,否则返回false。下面的代码演示了is运算符的使用。
using System; namespace csfx_demo { // abstract class C1 { public string Id { get; set; } // public abstract void M1(); // public virtual void M2() { Console.WriteLine("C1.M2"); } } // class C1a : C1 { public override void M1() { Console.WriteLine("C1a.M1, Id:{0}", Id); } } // class Program { static void Main(string[] args) { C1a c = new C1a(); Console.WriteLine(c is C1a); // True Console.WriteLine(c is C1); // True Console.WriteLine(c is Object); // True Console.WriteLine(c is String); // False } } }
代码中,c对象定义为C1a类的实例,而C1a类定义为C1类的子类,使用is运算符时,对象不但是其定义类型的实例,同样也可以视作其基类的实例,而C#和.NET Framework环境中,所有类的终极基类是Object,所以,第三个输出同样是True,对象的ToString()、Equals()、GetType()方法都源自Object类。最后一个输出,由于C1a类的实例无法直接转换为String类型,所以会输出False。
如果创建类时没有指定基类,默认会继承Object类,所以,本例中C1a类的继承关系为:C1a继承于C1,C1继承于Object;而Object类是唯一没有基类的类。
as运算符可以转换引用类型对象或可空类型变量的类型。将子类对象转换为基类类型后,对象不能调用基类中的虚拟(virtual)和抽象(abstract)成员。下面的代码演示了as运算符的应用。
using System; namespace csfx_demo { // abstract class C1 { public string Id { get; set; } // public abstract void M1(); // public virtual void M2() { Console.WriteLine("C1.M2"); } // public void M3() { Console.WriteLine("C1.M3"); } } // class C1a : C1 { public override void M1() { Console.WriteLine("C1a.M1, Id:{0}", Id); } // public override void M2() { base.M2(); Console.WriteLine("C1a.M2"); } // new public void M3() { Console.WriteLine("C1a.M3"); } } // class Program { static void Main(string[] args) { C1a c = new C1a(); (c as C1).M1(); (c as C1).M2(); (c as C1).M3(); } } }
代码中,C1类中的M1()方法定义为抽象方法,M2()方法定义为虚拟方法,M3()方法为普通实例方法。C1a类中重写了M1()和M2()方法。最后,在Main()方法中定义c对象为C1a类的实例,然后通过as运算符转换为C1类型,可以看到,转换类型后的对象调用的M1()和M2()方法实际仍为C1a类中的实现,而M3()方法则是C1类中的实现,即使在C1a类中使用new关键字覆盖C1类中的M3()方法,此处依然会调用C1类的M3()方法。