继承

继承是面向对象编程中的重要概念之一,也是代码复用的重要途径,下面,我们先来了解继承中的两个相对概念,即基类与子类。

基类与子类

先来看下面的代码,我们创建一个名为CCar的类。

C#
public class CCar : CAuto{
}

是不是太简单了?实际上,这个类已经是可以工作的类型了。定义CCar类时,使用:符号说明它“继承”于CAuto类,此时,CAuto为基类,又称为父类、超类;CCar类称为CAuto类的子类。

下面的代码,我们在Main()方法中测试CCar类的使用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CCar car = new CCar();
            Console.WriteLine(car.DoorCount);
        }
    }
}

执行代码会显示car对象的车门数量为4,那么,这个4是从哪来的呢?而且,我们调用的CCar()构造方法也没有创建,它又是从哪来的呢?还记得我们最初创建CAuto类时也没有创建构造方法,那么,它的默认的构造方法又是从哪来的呢?

是重要角色出场的时候了!

Object类,一个唯一没有基类的类。创建一个新类时,如果不指定基类,它默认就继承于Object类,即使指定了基类,它的根依然是Object类。这样,Object类、CAuto类和CCar类就形成了三层的继承关系。

Object类中定义了一些基本的对象操作方法,如Equals()方法、ToString()方法等;下面通过CCar类的实例来看看这两个方法的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CCar car1 = new CCar();
            CCar car2 = new CCar();
            CCar car3 = car1;
            Console.WriteLine(car1.ToString());
            Console.WriteLine(car1.Equals(car2));
            Console.WriteLine(car1.Equals(car3));
        }
    }
}

代码执行结果如下图。

本例,第一个输出是car1对象的ToString()方法返回的内容,默认情况下,它会显示类型名称,稍后可以看到如果改变它的返回内容。第二个输出是比较car1和car2对象,它们分别使用new关键字进行实例化,不是一个引用,所以是不同的对象,Equals()方法返回False。第三个输出,由于car3对象赋值为car1对象,所以是同一个引用,Equals()方法返回True。

扩展与重写

子类中,如果不定义构造方法,会默认继承基类中的无参数构造方法。回想CCar中实际调用的构造方法就是基类中的CAuto(),但这个构造方法实际调用的是CAuto(string,int)构造方法,并且将车门数量设置为4,这也是CCar类的实例DoorCount属性值默认为4的原因。

在子类中,如果定义了有参数的构造方法,则不再默认拥有无参数的构造方法,如果需要就必须再创建一个,如下面的代码,我们在CCar类中添加了两个构造方法。

C#
public class CCar : CAuto
{
    // 构造方法
    public CCar(string sModel) : base(sModel, 4) { }
    public CCar() : this("CarX") { }
}

下面的代码,在Main()方法中进行测试。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CCar car1 = new CCar();
            CCar car2 = new CCar("C19");
            Console.WriteLine(car1.Model +","+car1.DoorCount);
            Console.WriteLine(car2.Model + "," + car1.DoorCount);
        }
    }
}

代码执行结果如下图。

在子类中,我们可以添加一些新成员,此时,只要正常的定义类的成员就可以了。

当基类中的一些成员不能满足子类的需要时,可以在子类中重写这些成员,如下面的代码,我们CCar类中重写ToString()方法。

C#
public class CCar : CAuto
{
    // 构造方法
    public CCar(string sModel) : base(sModel, 4) { }
    public CCar() : this("CarX") { }
    // 重写ToString()方法
    public override string ToString()
    {
        return string.Format("轿车:型号({0}),车门数量({1})",Model,DoorCount);
    }
}

下面的代码,我们使用新的ToString()方法显示CCar实例信息。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CCar car1 = new CCar("C19");
            Console.WriteLine(car1.ToString());
        }
    }
}

代码执行结果如下图。

那么,基类中什么样的成员才可以被重写呢?一般是虚拟成员或抽象成员,其中,抽象成员必须定义在抽象类中,稍后讨论相关内容。而虚拟成员在定义时需要使用virtual关键字,需要注意的是,虚拟成员也是有着具体实现的成员,它是可以正常使用的;在子类中重写基类的虚拟成员时,使用在成员定义时使用override关键字,如前例中在CCar类中重写的ToString()方法。

在子类中,如果成员需要全新的实现,则可以通过new关键字隐藏基类成员,如下面的代码,我们重写CCar类,并隐藏了CAuto类中的DoorCount属性。

C#
public class CCar : CAuto
{
    // 构造方法
    public CCar(string sModel) : base(sModel, 4) { }
    public CCar() : this("CarX") { }
    // 重写ToString()方法
    public override string ToString()
    {
        return string.Format("轿车:型号({0}),车门数量({1})",Model,DoorCount);
    }
    //
    new public int DoorCount
    {
        get { return 4; }
    }
}

代码中,新的DoorCount属性变成了只读属性,并且只能有4个车门。下面的代码,测试了CAuto和CCar类中DoorCount属性的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CCar car = new CCar("C19");
            CAuto auto = new CCar("C20");
            auto.DoorCount = 5;
            Console.WriteLine(car.DoorCount);
            Console.WriteLine(auto.DoorCount);
        }
    }
}

代码执行结果如下图。

虽然car和auto对象都实例化为CCar对象,但注意它们的定义类型,car定义为CCar类型,而auto定义为CAuto类型;此时,auto的DoorCount属性会使用CAuto类中的读写属性,而car的DoorCount属性使用CCar类中的只读属性,如果给car.DoorCount属性赋值就会出现异常。

C#中的类默认是可以被继承的,但有些情况下,可能不希望类被继承,此时,只需要在定义类时添加sealed关键字即可。

抽象类与抽象成员

抽象类不能被实例化,也就是说,不能创建抽象类的对象;所以,抽象类一般用于定义一组类型的基本结构,然后,由一系列的子类来完成具体的实现。需要注意的是,当一个类中包含了抽象成员时,这个类就必须声明为抽象类。

下面的代码,我们创建一个名为CUnitBase的抽象类。

C#
public abstract class CUnitBase
{
    public string Name { get; set; }
    public abstract void MoveTo(int x, int y);
}

创建抽象类和抽象成员时需要使用abstract关键字,本例中,在CUnitBase抽象类中添加了一个非抽象属性Name和一个抽象方法MoveTo()。

下面的代码,我们创建CPlane类作为CUnitBase类的子类。

C#
using System;
public class CPlane : CUnitBase
{
    public override void MoveTo(int x, int y)
    {
        Console.WriteLine("{0}飞行到({1},{2})", Name, x, y);
    }
}

重写基类的抽象成员时,同样需要使用override关键字;下面,我们再创建一个CShip类作为CUnitBase类的子类。

C#
using System;
public class CShip : CUnitBase
{
    public override void MoveTo(int x, int y)
    {
        Console.WriteLine("{0}航行到({1},{2})", Name, x, y);
    }
}

下面的代码,我们在Main()方法中测试这些类的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CPlane j20 = new CPlane() { Name = "J20" };
            CShip n16 = new CShip() { Name = "16舰" };
            j20.MoveTo(10, 39);
            n16.MoveTo(1, 80);
        }
    }
}

代码执行结果如下图。

本例中,在创建j20和n16对象时使用了对象的初始化器,即在对象的构造方法后使用{和}指定对象的属性值,如果有多个属性,应使用逗号分隔。使用对象的初始化器,可以减少对象的引用,同时,也可以在不创建构造方法的同时,更方便地指定对象属性的数据。

访问级别

访问级别用于确定各种类型及其成员的应用范围,比如,方法中定义的变量就只能在方法内部使用;对于类的成员,可以使用的访问级别包括:

  • public,公共的,可以在类的外部,通过类(静态成员)或对象(实例成员)进行访问。
  • private,私有的,只能在类的内部使用。
  • protected,受保护的,只能在本类或其子类中使用。
  • internal,只能在当前程序集中使用。

此外,protected和internal可以配合使用,定义的成员只能在当前程序集的本类和其子类中使用。

本站内容均为原创作品,转载请注明出处,本页面网址为:http://caohuayu.com/chy/article/Article.aspx?code=cc002003