C#基础-继承、多态与接口

来源:互联网 发布:饿了么商家版for mac 编辑:程序博客网 时间:2024/05/02 04:18

继承(Inheritance)是指类能够从它的父类中继承其中所有的数据和功能。或者说,使用继承可以使类能够自动拥有其父类中的数据和功能,并能随心所欲地使用或调用这些数据和功能。通过继承,程序员能够直接享用他人或自己事先写好的功能,而不必从头开始、从无到有地去编写类;也可以将这个类作为基类,以用于生成一个新的类。

OOP和继承是程序设计的一个重要方面,随着编程经历和经验的丰富,我们会逐步认识到使用OOP和继承是编写所有大型应用程序的最佳方式。

在C#中,作为基础、被继承的类称为基类(Base Class),继承自别的类的子类称为扩充类(Derived Class)。
C#语言提供了两种实现继承的方法:类继承和接口继承。类继承制允许单一继承,即只有一个基类。因为单一继承已经能够满足大多数面向对象应用程序开发上的要求,也有效地降低了复杂性。如果必须使用多重继承,可以使用接口继承来实现。
需要注意的是,继承是非常有用的编程概念,但使用不当也会带来一些负面的效果。下列情况下可以使用类继承:
1)扩充类与基类的关系是“属于”关系,而不是“具有”关系。“具有”关系的类不适合使用类继承,因为这样可能会继承不适当的属性和方法。
2)可以重用基类的代码。例如,如果一个数据库中有多个结构相同或相似的表,对每一个表都设计添加、删除、修改等处理显然既费时又容易出错,这时使用类继承就是比较好的选择。
3)需要将相同的类和方法应用到不同的数据类型。这时可以利用重写基类中的某些方法来实现。
4)类层次结构相当浅,而且其他开发人员不可能添加太多的级别。继承最适合于相对较浅的类层次结构。一般来说,应将层次结构限制在六级或更少级别。
5)需要通过更改基类对派生类进行全局更改。继承的一个最强大的功能是在基类中进行的更改将自动传播到派生类中。例如更新一个方法的实现,从而几十甚至上百个派生类都可以自动使用该新代码。但是,一般情况下,应避免更改基类成员的名称或类型,因为这样容易导致使用原成员的扩充类出现问题。

1、实现类继承的方法

要声明一个类是从另一个类继承而来的,可以使用下面的语法:
class 扩充类名称 : 基类名称
{
  ......
}

扩充类继承了基类所有的元素,包括定义在基类中数据的定义(不是数据值)和方法。但是要注意,构造函数则排除在外,不会被继承下来。
【例】类和继承
using System;public class Test{  public test()  {    Console.WriteLine("hello,Test.");  }}public class Test1:Test{  public Test1()  {    Console.WriteLine("hello,Test1.");  }  public static void Main()  {    Test1 t = new Test1();  }}
类Test1继承自类Test,当创建Test1的实例时,会首先调用基类的构造函数,然后调用扩充类的构造函数,因此输出结果为:
hello,Test.
hello,Test1.
不过,扩充类并不一定能够用到基类中所定义的所有数据和方法,如基类的public成员将会成为扩充类的public成员,public表示允许外部类自由的调用;而基类的private成员则只会被基类本身的成员存取,扩充类无法调用基类的private成员。

2、重写基类的方法

如果基类提供的功能不能满足要求,而且基类允许重写,则可以在扩充类中重新定义基类的方法。在基类中,如果想让某个方法或者事件被扩充类重写,可以使用修饰符virtual表明。例如:
public virtual void myMethod()
{
  ......
}

这样,在扩充类中就可以使用修饰符override重写该方法或者事件了。例如:
public override void myMethod()
{
  ......
}

在C#中,定义的方法默认都是非虚拟的(non-virtual),即不允许重写这些方法。但是基类中的方法使用了virtual修饰符以后,该方法就变成了虚拟方法,也就是说,可以在扩充类中重写该方法了。但是必须在扩充类中用override表明是重写基类中同名的方法。
使用虚拟方法与重写方法时,需要注意下面几个方面:
1)虚拟方法不能声明为静态(static)的。因为静态的方法是应用在类这一层次的,而面向对象的多态性只能在对象上运作,所以无法在类中使用。
2)virtual不能和private一起使用。因为声明为private就无法在扩充类中重写了。
3)重写方法的名称、参数个数、类型以及返回值都必须和虚拟方法的一致。

【例】重写基类的方法

using System;class A{  int x=1;  public virtual void PrintFields()  {    Console.WriteLine("x={0}",x);  }}class B:A{  int y=2;  public override void PrintFields()  {    base.PrintFields();    Console.WriteLine("y={0}",y);  }  public static void Main()  {    B me = new B();    me.PrintFields();  }}
输出结果为:
x=1
y=2
在类B中调用base.PrintFields()就调用了在类A中声明的PrintFields方法。基访问禁用了虚拟调用机制,它只是简单地将那个重写了的基方法视为非虚拟方法。
注意:只有使用override修饰符时,才能重写基类的方法。否则,在继承的类中声明一个与基类方法同名的方法会隐藏基类的方法。例如:
【例】容易引起混乱的方法声明
using System;class A{  public virtual void F(){}}class B:A{  public virtual void F(){}  public static void Main()  {    B me=new B();    me.F();  }}
由于B中的F方法不包含override修饰符,因此B中的F方法不重写A中的F方法。相反,B中的F方法会隐藏A中的方法,但是由于该声明中没有显式地声明隐藏基类的方法,因此,编译后在B中的F方法下面会有一条蓝色的下划线,当鼠标移动到F方法上时,会显示一条警告信息。

3、隐藏基类的方法

在扩充类中,可以使用new关键字来隐藏基类的方法,即使用一个完全不同的方法取代旧的方法。
与方法重写不同的是,使用new关键字时并不要求基类中的方法声明为virtual,只要在扩充类的方法前声明为new,就可以隐藏对应基类的方法。
什么情况下需要这样做呢?比如你现在需要根据要求重新设计基类中的某个方法,该基类是在一年前由另一组人员设计的,并且已经交给用户使用,可是当时他们在该方法前并没有加virtual关键字,你也没有这些源代码。这种情况下显然既不能使用overide重写基类的方法,又无法直接修改基类的方法,这时就需要隐藏基类的方法。
【例】隐藏基类的方法
using System;public class Hello{  public void SayHello()  {    Console.WriteLine("这是基类!");  }}public class NewHello:Hello{  public new noid SayHello()  {    Console.WriteLine("这是扩充类!");  }  public static void Main()  {    Hello b=new Hello();    b.SayHello();    NewHello d=new NewHello();    d.SayHello();  }}
输出结果为:
这是基类!
这是扩充类!

4、使用sealed防止类被继承

有些情况下,可能不想让其他类继承这个类,这时可以使用sealed关键字,即通过sealed防止类被其他类继承。
同样,sealed关键字也可以限制基类中的方法被扩充类重写。例如,下面的程序代码是错误的。
【例】使用sealed防止某些类被继承
using System;public class Hello{  public sealed void SayHello()  {    Console.WriteLine("这是基类!");  }}public class NewHello:Hello{  public new void SayHello()  {    Console.WriteLine("这是扩充类!");  }  public static void Main()  {    NewHello me=new NewHello();    me.SayHello();  }}

因为SayHello已经用sealed限制为不能被继承,所以在NewHello类中试图隐藏SayHello方法是错误的。

5、版本控制

用C#编写方法时,如果要在扩充类中重写基类的方法,需要用override声明;如果要隐藏基类的方法,需要用new声明,这就是C#进行版本控制的依据。
在C#中,所有的方法默认都是非虚拟的,调用非虚拟方法时不会受到版本的影响,不管是调用基类的方法还是调用扩充类的方法,都会和设计者预期的一样执行实现的程序代码。相比之下,虚拟方法的实现部分可能会因扩充类的重写而影响执行结果。也就是说,在执行时期调用虚拟方法时,它会自动判断应该调用哪个方法。比如,如果基类中声明一个虚拟方法,而扩充类的方法中使用了override关键字,则执行时会调用扩充类的方法。如果扩充类的方法没有使用override关键字,则调用基类的方法。而没有声明为virtual的非虚拟方法,则在编译时就确定了欲调用哪个方法了。
下面举例说明C#实现版本控制的方法
【例】版本控制-使用new修饰符
using System;namespace Version1{  class A  {    public void Method()    {      Console.WriteLine("A.Method");    }  }  class B:A  {    public new void Method()    {      Console.WriteLine("B.Method");    }  }  class versionControl  {    public static void Main()    {      A a=new A();      B b=new B();      A c=b;      a.Method();      b.Method();      c.Method();    }  }}
输出结果为:
A.Method
B.Method
A.Method
例子中先建立了A类的对象a,因此可以直接调用Method方法,并打印出相关的字符串。
B对象声明为B类型,当调用b.Method方法时,实际上是调用定义在B类中的方法。
C对象声明为A类型,却初始化为B类的对象。当调用c.Method方法时,由于c对象声明为A类型,因此c便视为A类型对象,调用的将是A类中的Method方法。
C#在执行时期调用声明为virtual虚拟方法时,会动态地决定要调用的方法是定义在基类的方法,还是定义在扩充类中的方法。实际上是根据下面的原则来判断的:调用继承的最后实现(most derived implementation)部分的方法。
【例】版本控制-使用virtual与new
using System;namespace version2{  class A  {    public virtual void Method()    {      Console.WriteLine("A.Method");    }  }  class B:A  {    public new virtual void Method()    {      Console.WriteLine("B.Method");    }  }  class versionControl  {    public static void Main()    {      A a = new A();      B b = new B();      A c = b;      a.Method();      b.Method();      c.Method();    }  }}
输出结果为:
A.Method
B.Method
A.Method
在这个例子中,c对象仍然声明为A类,并且初始化为B类的对象。当调用c.Method方法时,C#会先检查A类的Method,然后发现Method使用virtual关键字,于是进一步检查B类。而B类的Method方法也使用virtual关键字,表示B类的Method和A类的Method没有重写关系,但是B类的Method方法使用new关键字,表示这是一个全新的方法,只是和基类的Method同名罢了。因此,最后被调用的是A类的Method方法。
【例】版本控制-使用virtual、new与override
using System;namespace Version3{  class A  {    public virtual void Method()    {      Console.WriteLine("A.Method");    }  }  class B:A  {    public new virtual void Method()    {      Console.WriteLine("B.Method");    }  }  class C:B  {    public override void Method()    {      Console.WriteLine("C.Method");    }  }  class versionControl  {    public static void Main()    {      A a = new A();      B b = new C();      A c = b;      a.Method();      b.Method();      c.Method();    }  }}
输出结果为:
A.Method
C.Method
A.Method
在这个例子中,b对象仍然声明为B类,但初始化为C类的对象。当调用b.Method时,C#会先检查B类的Method。发现Method使用了new修饰符,说明B类的Method隐藏了A类的Method,但是由于B类的Method方法又实用了virtual修饰符,并且b初始化为C类的对象,于是进一步检查C类的Method。C类的Method方法是用override关键字,表示重写了B类的Method方法,根据找寻调用继承的最后实现部分的方法的原则,因此最后被调用的是C类的Method方法。

c对象声明为A类,但是却初始化为B类的对象。当调用c.Method时,C#会先检查A类的Method,然后发现Method使用了virtual关键字,于是进一步检查B类的Method。由于B类的Method使用了new修饰符,说明B类的Method并没有继承A类的Method,因此最后被调用的是A类的Method方法。

6、抽象类和接口

我们知道,封装、继承与多态性是面向对象编程的三大原则。多态性是指类为名称相同的方法提供不同实现方式的能力。利用多态性,就可以调用类中的某个方法而无需考虑该方法是如何实现的。
有以下几种实现多态性的方式:
1)通过继承实现多态性。多个类可以从单个基类“继承”。通过继承,类在基类所在的同一实现中接收基类的所有方法、属性和事件。这样,便可根据需要重写基类成员以提供不同的功能。
2)通过抽象类实现多态性。抽象类本身不能被实例化,只能在扩充类中通过继承使用。抽象类的部分或全部成员不一定都要实现,但是要在继承类中全部实现。抽象类中已实现的成员仍可以被重写,并且继承类仍可以实现其他功能。
3)通过接口实现多态性。多个类可实现相同的“接口”,而单个类可以实现一个或多个接口。接口本质上是类需要如何响应的定义。接口仅声明类需要实现的方法、属性和事件,以及每个成员需要接收和返回的参数类型,而这些成员的特定实现留给实现类去完成。

6.1 抽象类

抽象类使用abstract修饰符,用于表示所修饰的类是不完整的,并且它只能用作基类。抽象类与非抽象类相比有以下主要不同之处:
1)抽象类不能直接实例化,并且对抽象类使用new运算符会产生编译时错误。虽然一些变量和值在编译时的类型可以是抽象的,但是这样的变量和值必须为null,或者含有对非抽象类的实例的引用(此非抽象类是从抽象类派生的)。
2)抽象类也可以包含抽象成员,而非抽象类不能包含抽象成员。当从抽象类派生非抽象类时,这些非抽象类必须具体实现所继承的所有抽象成员,从而重写那些抽象成员。例如:

abstract class A{  public abstract voidF();//抽象方法,注意该方法没有实现部分}abstract class B:A{  public void G(){}}class C:B{  public override void f()  {    //实现部分  }}
在上面的代码中,抽象类A引入抽象方法F。注意,抽象方法均为虚拟方法,不需要加virtual。类B引入另一个方法G,但由于它不提供F的实现,因此B也必须声明为抽象类。类C重写了抽象方法F,并提供了一个具体实现。由于C中没有抽象成员,因此可以(但并非必须)将C声明为非抽象类。

在实现抽象类时,必须实现该类中的每一个抽象方法,而每个已实现的方法必须和抽象类中指定的方法一样,接收相同数目和类型的参数,具有同样的返回值。

6.2接口

与抽象类不同,接口是完全抽象的成员集合。接口的主要特点是只有声明部分,而没有实现部分。和类一样,接口也定义了一系列属性、方法和事件。但与类不同的是,接口并不提供实现。接口的功能完全由类来实现,并在类中被定义为单独的实体。
接口中部包含任何程序代码,也不能含有{ } 。例如,下面的写法是错误的:
public interface Itest
{
  int sun(){} //不能有{}
}

定义在接口中的方法都是public的,不能再声明。例如,下面的写法是错误的:
public interface Itest
{
  public int sun(); //不能有public声明
}

接口表示一种约定,实现接口的类必须严格按其定义来实现接口的每个方面。有了接口,就可以将功能定义为一些紧密相关成员的小组。可以在不危害现有代码的情况下,开发接口的增强型实现,从而使兼容性问题最小化。也可以在任何时候通过开发附加接口和实现来添加新功能。
虽然接口实现可以进化,但接口本身一旦被发布就不能再更改。对已发布的接口进行更改会破坏现有代码。若把接口视为约定,很明显约定双方都各有其承担的义务。接口的发布者同意不再更改该接口,接口的实现者则同意严格按设计来实现接口。
抽象类和接口的一个主要差别是:类可以实现多个接口,但仅能从一个抽象类或者任何其他类型的类继承。
选择将功能设计为接口还是抽象类有时候是一件困难的事情。抽象类是一种不能实例化而必须从中继承的类。抽象类可以完全实现,但更常见的是部分实现或者根本不实现,从而封装继承类的通用功能。
使用接口还是抽象类为组件提供多态性主要考虑一下几个方面:
1)如果预计要创建组件的多个版本,则创建抽象类。抽象类提供简单易行的方法来控制组件版本。通过更新基类,所有继承类都随更改自动更新。另一方面,为了保护为使用接口而编写的现有系统,要求接口一旦创建就不能更改。如果需要接口的新版本,必须创建一个全新的接口。
2)如果创建的功能将在大范围的完全不同的对象间使用,则使用接口。抽象类应主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能。
3)如果要设计小而简练的功能块,则使用接口;如果要设计大的功能但愿,则使用抽象类。设计优良的接口往往很小且相互独立,减少了性能问题发生的可能。
4)如果要在组件的所有实现间提供通用的已实现功能,则使用抽象类。抽象类允许部分实现类,而接口不包含任何成员的实现。
在C#中,使用interface关键字声明一个接口。
一般情况下,建议以大写的“I”开头指定接口名,表明这是一个接口。

6.3接口的声明与实现

下面的例子说明了如何声明和实现接口

【例】接口的声明与实现

namespace TestInterface{    interface Ifunction1    {        int sum(int x1, int x2);    }    interface Ifunction2    {        string str { get; set; }    }    class MyTest : Ifunction1, Ifunction2   //此处的冒号表示接口的实现    {        private string mystr;        //构造函数        public MyTest()        { }        //构造函数        public MyTest(string str)        {            mystr = str;        }        //实现接口Ifunction1中的方法        public int sum(int x1, int x2)        {            return x1 + x2;        }        //实现接口Ifunction2中的属性        public string str        {            get            {                return mystr;            }            set            {                mystr = value;            }        }    }    class MainClass    {        public static void Main()        {            //直接访问实例            MyTest a = new MyTest();            Console.WriteLine(a.sum(10, 20));            MyTest b = new MyTest("How are you");            Console.WriteLine(b.str);            //通过接口访问            Ifunction1 c = new MyTest();            Console.WriteLine(c.sum(20, 30));            Ifunction2 d = new MyTest("This is a book!");            Console.WriteLine(d.str);            Console.ReadLine();        }    }}

输出结果为:

30

How are you

50

This is a book!

6.4以显式方式实现接口

一个实现了接口的类也可以显式实现接口中的方法。对于显式实现的方法,不能通过类的实例进行访问,而必须使用接口的实例。
【例】以显式方式实现接口

namespace TestInterface{    interface Ifunction    {        int sum(int x1, int x2);    }    class MyTest : Ifunction    {        //实现接口Ifunction中的方法        int Ifunction.sum(int x1, int x2)        {            return x1 + x2;        }    }    class MainClass    {        public static void Main()        {            /*下面注释掉的两行代码为错误的访问例子,             * 如果这样写会提示“MyTest不包含对sum             * 的定义”的错误,这是因为sum是显式实             * 现接口,只能通过接口调用             */            //MyTest a = new MyTest();            //Console.WriteLine(a.sum(10, 20));            //通过接口访问实例            Ifunction b = new MyTest();            Console.WriteLine(b.sum(20, 30));            Console.ReadLine();        }    }}
输出结果为:

50

0 0
原创粉丝点击