1. 继承概述

1.1 定义与作用

继承是面向对象编程中一个核心的概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。在C#中,继承通过关键字:来实现,子类可以继承父类的所有非私有成员,包括字段、属性、方法和索引器等。

继承的主要作用是实现代码的复用,通过继承,子类可以继承父类的已有功能,并且可以对其进行扩展或修改,从而避免重复编写相同的代码。例如,假设有一个Animal类,它包含一些基本的属性和方法,如NameAgeMakeSound。如果要创建一个Dog类和一个Cat类,它们都具有Animal类的特征,那么就可以让Dog类和Cat类继承Animal类,这样Dog类和Cat类就自动拥有了NameAgeMakeSound等属性和方法,而无需重新定义这些成员。

继承还可以实现多态,多态是指同一个操作作用于不同的对象时,可以有不同的解释和不同的执行结果。在C#中,通过继承和虚方法(virtual)或抽象方法(abstract),可以实现多态。例如,Animal类中的MakeSound方法可以被声明为虚方法,然后在Dog类和Cat类中分别重写(override)该方法,这样就可以实现多态。当调用MakeSound方法时,根据对象的实际类型,会调用相应类中的MakeSound方法。

2. 基类与派生类

2.1 基类定义

在C#中,基类是被继承的类,它定义了一组通用的属性和方法,这些成员可以被一个或多个派生类继承。基类的设计应该具有通用性和可扩展性,以便能够满足不同派生类的需求。

例如,定义一个Animal基类,它包含所有动物共有的属性和行为。代码如下:

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Animal(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public virtual void MakeSound()
    {
        Console.WriteLine("This animal makes a sound.");
    }
}
  • Animal类有两个属性:NameAge,分别表示动物的名称和年龄。

  • 构造函数Animal(string name, int age)用于初始化这些属性。

  • MakeSound方法被声明为virtual,表示这是一个可以被派生类重写的虚方法,用于实现多态。

2.2 派生类声明

派生类是通过继承基类而创建的类,它继承了基类的所有非私有成员,并且可以添加新的成员或重写继承来的成员。在C#中,派生类的声明使用:关键字,后面跟上基类的名称。

例如,定义一个Dog派生类,它继承自Animal基类,并且添加了一些特定于狗的属性和行为。代码如下:

public class Dog : Animal
{
    public string Breed { get; set; } // 狗的品种

    public Dog(string name, int age, string breed) : base(name, age)
    {
        Breed = breed;
    }

    public override void MakeSound()
    {
        Console.WriteLine("The dog barks.");
    }
}
  • Dog类继承自Animal类,使用:关键字。

  • Dog类添加了一个新的属性Breed,表示狗的品种。

  • 构造函数Dog(string name, int age, string breed)调用了基类的构造函数base(name, age),用于初始化继承来的NameAge属性。

  • MakeSound方法被重写为override,实现了多态。当调用Dog对象的MakeSound方法时,会输出狗的叫声,而不是基类中的默认实现。

通过这种方式,派生类可以继承基类的通用功能,并且可以根据自己的需求进行扩展和定制。

3. 构造函数与继承

3.1 基类构造函数调用

在C#中,当派生类被实例化时,基类的构造函数会首先被调用,以确保基类的成员被正确初始化。这是通过在派生类的构造函数中使用base关键字来实现的。base关键字用于调用基类的构造函数,确保基类的属性和状态被正确设置。

例如,在前面的Dog类中,构造函数Dog(string name, int age, string breed)通过base(name, age)调用了基类Animal的构造函数,将nameage参数传递给基类的构造函数,从而初始化了继承来的NameAge属性。

public class Dog : Animal
{
    public string Breed { get; set; }

    public Dog(string name, int age, string breed) : base(name, age)
    {
        Breed = breed;
    }

    public override void MakeSound()
    {
        Console.WriteLine("The dog barks.");
    }
}
  • base(name, age):调用基类Animal的构造函数,初始化NameAge属性。

  • Breed = breed:初始化派生类特有的属性Breed

如果没有显式调用基类的构造函数,C#编译器会自动调用基类的无参数构造函数(如果存在)。但如果基类没有无参数构造函数,或者需要传递参数来初始化基类的成员,就必须显式调用基类的构造函数。

3.2 派生类构造函数实现

派生类的构造函数不仅要初始化自己的成员,还需要确保基类的成员被正确初始化。这通常是通过在派生类的构造函数中调用基类的构造函数来实现的。派生类的构造函数可以有自己的参数列表,并且可以添加自己的初始化逻辑。

例如,假设有一个Cat类,它继承自Animal类,并且添加了一个特定于猫的属性ColorCat类的构造函数需要初始化Color属性,并且调用基类的构造函数来初始化NameAge属性。

public class Cat : Animal
{
    public string Color { get; set; }

    public Cat(string name, int age, string color) : base(name, age)
    {
        Color = color;
    }

    public override void MakeSound()
    {
        Console.WriteLine("The cat meows.");
    }
}
  • Cat(string name, int age, string color):派生类的构造函数,包含自己的参数列表。

  • base(name, age):调用基类的构造函数,初始化继承来的NameAge属性。

  • Color = color:初始化派生类特有的属性Color

派生类的构造函数可以有多个重载版本,以支持不同的初始化需求。例如,可以为Cat类添加一个无参数构造函数,提供默认值:

public class Cat : Animal
{
    public string Color { get; set; }

    public Cat() : base("Unknown", 0)
    {
        Color = "Unknown";
    }

    public Cat(string name, int age, string color) : base(name, age)
    {
        Color = color;
    }

    public override void MakeSound()
    {
        Console.WriteLine("The cat meows.");
    }
}
  • Cat():无参数构造函数,调用基类的构造函数时提供默认值"Unknown"0

  • Cat(string name, int age, string color):带参数的构造函数,调用基类的构造函数时传递具体的参数。

通过这种方式,派生类的构造函数可以灵活地初始化自己的成员,同时确保基类的成员被正确初始化。

4. 方法重写与多态

4.1 方法重写语法

在C#中,方法重写是继承机制中的一个重要特性,它允许派生类根据自己的需求重新实现从基类继承来的方法。方法重写的关键字是override,它必须与基类中被声明为virtualabstract的方法相对应。

以下是一个方法重写的示例代码:

public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("This animal makes a sound.");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("The dog barks.");
    }
}
  • 基类Animal中的MakeSound方法被声明为virtual,表示这是一个可以被派生类重写的虚方法。

  • 派生类Dog通过override关键字重写了MakeSound方法,提供了自己的实现。

方法重写需要满足以下规则:

  • 派生类中的方法必须与基类中被重写的方法具有相同的方法签名,包括方法名、参数列表和返回类型。

  • 派生类中的方法必须使用override关键字来明确表示这是一个重写的方法。

  • 基类中的方法必须被声明为virtualabstract,才能被派生类重写。

  • 如果基类中的方法被声明为sealed,那么它不能被派生类重写。

4.2 多态实现

多态是指同一个操作作用于不同的对象时,可以有不同的解释和不同的执行结果。在C#中,通过继承和方法重写可以实现多态。

以下是一个多态的示例代码:

public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("This animal makes a sound.");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("The dog barks.");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("The cat meows.");
    }
}
  • 基类Animal中的MakeSound方法被声明为virtual,表示这是一个可以被派生类重写的虚方法。

  • 派生类DogCat分别重写了MakeSound方法,提供了自己的实现。

在运行时,根据对象的实际类型,会调用相应类中的MakeSound方法。例如:

Animal animal1 = new Dog();
Animal animal2 = new Cat();

animal1.MakeSound(); // 输出:The dog barks.
animal2.MakeSound(); // 输出:The cat meows.
  • animal1animal2都是Animal类型的引用,但它们实际指向的对象分别是DogCat

  • 当调用MakeSound方法时,根据对象的实际类型,会调用相应类中的MakeSound方法,这就是多态的实现。

多态的优点包括:

  • 提高代码的可扩展性和可维护性,可以通过添加新的派生类来扩展功能,而无需修改现有的代码。

  • 提高代码的复用性,可以使用基类的引用指向派生类的对象,从而实现代码的通用性。

  • 提高代码的灵活性,可以根据不同的需求动态地选择对象的行为。

5. 继承中的访问修饰符

5.1 public成员

在C#中,public访问修饰符表示成员是公开的,可以在任何地方被访问。当一个成员被声明为public时,它可以在基类和派生类中被自由访问。

例如,基类Animal中的NameAge属性被声明为public

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }
}

派生类Dog可以访问这些public成员,并且可以对它们进行操作:

public class Dog : Animal
{
    public void PrintInfo()
    {
        Console.WriteLine($"Name: {Name}, Age: {Age}");
    }
}
  • NameAge属性在派生类Dog中可以被直接访问。

  • 派生类可以对这些public成员进行读取和修改操作。

public成员的优点是访问性高,可以在任何地方被访问和使用。但这也可能导致一些问题,例如外部代码可能会意外地修改这些成员的值,从而影响程序的稳定性和安全性。

5.2 protected成员

protected访问修饰符表示成员只能在基类和派生类中被访问,不能被外部代码访问。这为类的成员提供了一定程度的封装性,同时允许派生类访问基类的成员。

例如,基类Animal中定义了一个protected方法InternalSound

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    protected void InternalSound()
    {
        Console.WriteLine("This is an internal sound.");
    }
}

派生类Dog可以访问这个protected方法:

public class Dog : Animal
{
    public void MakeSound()
    {
        InternalSound();
        Console.WriteLine("The dog barks.");
    }
}
  • InternalSound方法在派生类Dog中可以被访问和调用。

  • 但这个方法不能被外部代码直接访问,只能在基类和派生类中使用。

protected成员的优点是提供了更好的封装性,防止外部代码直接访问类的内部实现细节,同时允许派生类继承和使用这些成员。这有助于维护代码的结构和安全性,同时保持类的可扩展性。

6. 继承的限制与规则

6.1 单继承限制

在C#中,继承是单继承的,这意味着一个类只能继承自一个基类。例如,不能同时让一个类继承自AnimalVehicle两个基类。这种单继承的设计简化了语言的复杂性,避免了多重继承可能导致的歧义和冲突,如菱形继承问题。

尽管C#不支持多重继承,但它提供了接口(interface)来弥补这一限制。接口允许一个类实现多个接口,从而实现类似多重继承的功能。接口定义了一组方法和属性的契约,但不提供具体实现。派生类必须实现接口中定义的所有成员。

例如,定义一个IMovable接口:

public interface IMovable
{
    void Move();
}

然后让Dog类实现这个接口:

public class Dog : Animal, IMovable
{
    public string Breed { get; set; }

    public Dog(string name, int age, string breed) : base(name, age)
    {
        Breed = breed;
    }

    public override void MakeSound()
    {
        Console.WriteLine("The dog barks.");
    }

    public void Move()
    {
        Console.WriteLine("The dog runs.");
    }
}
  • Dog类继承自Animal类,并且实现了IMovable接口。

  • Move方法是IMovable接口的实现,提供了狗的移动行为。

通过接口,C#在单继承的基础上提供了灵活的扩展能力,允许一个类同时具备多个接口定义的行为,而不会引入多重继承的复杂性。

6.2 构造函数与析构函数

构造函数

在继承中,构造函数的调用顺序是先调用基类的构造函数,再调用派生类的构造函数。这是为了确保基类的成员在派生类初始化之前已经被正确初始化。派生类的构造函数通过base关键字显式调用基类的构造函数。

例如:

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Animal(string name, int age)
    {
        Console.WriteLine("Animal constructor called.");
        Name = name;
        Age = age;
    }
}

public class Dog : Animal
{
    public string Breed { get; set; }

    public Dog(string name, int age, string breed) : base(name, age)
    {
        Console.WriteLine("Dog constructor called.");
        Breed = breed;
    }
}
  • 当创建一个Dog对象时,首先调用Animal的构造函数,输出Animal constructor called.

  • 然后调用Dog的构造函数,输出Dog constructor called.

如果没有显式调用基类的构造函数,C#编译器会尝试调用基类的无参数构造函数。如果基类没有无参数构造函数,编译器会报错。

析构函数

析构函数用于在对象被销毁时释放资源。在继承中,析构函数的调用顺序与构造函数相反,即先调用派生类的析构函数,再调用基类的析构函数。这是因为派生类可能在基类的基础上添加了额外的资源,需要先释放这些资源,再释放基类的资源。

例如:

public class Animal
{
    public Animal()
    {
        Console.WriteLine("Animal constructor called.");
    }

    ~Animal()
    {
        Console.WriteLine("Animal destructor called.");
    }
}

public class Dog : Animal
{
    public Dog()
    {
        Console.WriteLine("Dog constructor called.");
    }

    ~Dog()
    {
        Console.WriteLine("Dog destructor called.");
    }
}
  • 当创建一个Dog对象时,首先调用Animal的构造函数,输出Animal constructor called.

  • 然后调用Dog的构造函数,输出Dog constructor called.

  • Dog对象被销毁时,首先调用Dog的析构函数,输出Dog destructor called.

  • 然后调用Animal的析构函数,输出Animal destructor called.

析构函数的调用顺序确保了派生类和基类的资源都被正确释放,避免了资源泄漏。

7. 继承的实践案例

7.1 场景描述

在面向对象编程中,继承是实现代码复用和扩展功能的重要手段。本节将通过一个实际案例来展示继承在C#程序设计中的应用。假设我们需要开发一个简单的图形绘制程序,程序中包含多种图形,如圆形、矩形和三角形等。每种图形都有自己的属性和行为,但它们也有一些共同的特征,例如都有面积和周长等属性,都可以被绘制和移动等行为。因此,我们可以设计一个基类Shape,它定义了所有图形共有的属性和行为,然后让每种具体的图形类继承自Shape类,并根据自己的特点实现具体的行为。

7.2 代码实现

首先,我们定义一个基类Shape,它包含所有图形共有的属性和行为:

public abstract class Shape
{
    public string Color { get; set; }

    public Shape(string color)
    {
        Color = color;
    }

    public abstract double GetArea();
    public abstract double GetPerimeter();
    public abstract void Draw();
    public abstract void Move();
}
  • Shape类是一个抽象类,使用abstract关键字声明。抽象类不能被实例化,但可以被继承。

  • Color属性表示图形的颜色,是所有图形共有的属性。

  • GetAreaGetPerimeterDrawMove方法被声明为抽象方法,使用abstract关键字。抽象方法没有具体的实现,派生类必须提供这些方法的具体实现。

接下来,我们定义一个Circle类,它继承自Shape类,并实现了圆形特有的属性和行为:

public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(string color, double radius) : base(color)
    {
        Radius = radius;
    }

    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }

    public override double GetPerimeter()
    {
        return 2 * Math.PI * Radius;
    }

    public override void Draw()
    {
        Console.WriteLine($"Drawing a circle with radius {Radius} and color {Color}.");
    }

    public override void Move()
    {
        Console.WriteLine("Moving the circle.");
    }
}
  • Circle类继承自Shape类,使用:关键字。

  • Circle类添加了一个新的属性Radius,表示圆的半径。

  • 构造函数Circle(string color, double radius)调用了基类的构造函数base(color),用于初始化继承来的Color属性。

  • GetAreaGetPerimeterDrawMove方法被重写为override,提供了圆形的具体实现。

同样地,我们定义一个Rectangle类,它继承自Shape类,并实现了矩形特有的属性和行为:

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Rectangle(string color, double width, double height) : base(color)
    {
        Width = width;
        Height = height;
    }

    public override double GetArea()
    {
        return Width * Height;
    }

    public override double GetPerimeter()
    {
        return 2 * (Width + Height);
    }

    public override void Draw()
    {
        Console.WriteLine($"Drawing a rectangle with width {Width}, height {Height} and color {Color}.");
    }

    public override void Move()
    {
        Console.WriteLine("Moving the rectangle.");
    }
}
  • Rectangle类继承自Shape类,使用:关键字。

  • Rectangle类添加了两个新的属性WidthHeight,分别表示矩形的宽度和高度。

  • 构造函数Rectangle(string color, double width, double height)调用了基类的构造函数base(color),用于初始化继承来的Color属性。

  • GetAreaGetPerimeterDrawMove方法被重写为override,提供了矩形的具体实现。

最后,我们定义一个Triangle类,它继承自Shape类,并实现了三角形特有的属性和行为:

public class Triangle : Shape
{
    public double Side1 { get; set; }
    public double Side2 { get; set; }
    public double Side3 { get; set; }

    public Triangle(string color, double side1, double side2, double side3) : base(color)
    {
        Side1 = side1;
        Side2 = side2;
        Side3 = side3;
    }

    public override double GetArea()
    {
        double s = (Side1 + Side2 + Side3) / 2;
        return Math.Sqrt(s * (s - Side1) * (s - Side2) * (s - Side3));
    }

    public override double GetPerimeter()
    {
        return Side1 + Side2 + Side3;
    }

    public override void Draw()
    {
        Console.WriteLine($"Drawing a triangle with sides {Side1}, {Side2}, {Side3} and color {Color}.");
    }

    public override void Move()
    {
        Console.WriteLine("Moving the triangle.");
    }
}
  • Triangle类继承自Shape类,使用:关键字。

  • Triangle类添加了三个新的属性Side1Side2Side3,分别表示三角形的三条边的长度。

  • 构造函数Triangle(string color, double side1, double side2, double side3)调用了基类的构造函数base(color),用于初始化继承来的Color属性。

  • GetAreaGetPerimeterDrawMove方法被重写为override,提供了三角形的具体实现。

通过这个案例,我们可以看到继承在C#程序设计中的强大功能。基类Shape定义了所有图形共有的属性和行为,而派生类CircleRectangleTriangle则根据自己的特点实现了具体的行为。这种设计不仅提高了代码的复用性,还增强了代码的可扩展性和可维护性。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐