第十阶段:继承 – 代码重用与 “is-a” 关系

QcrTiMo 发布于 9 天前 10 次阅读


什么是继承?为什么要使用继承?

想象一下现实世界中的分类:

  • “狗” 是一种 “动物”。
  • “汽车” 是一种 “交通工具”。
  • “学生” 和 “老师” 都属于 “人”。

“狗” 继承了 “动物” 的基本特征(比如需要呼吸、会动),并有自己独特的特征(比如会汪汪叫)。“汽车” 继承了 “交通工具” 的特征(比如可以移动),并有自己的特征(比如有引擎、轮子)。

继承 (Inheritance) 在面向对象编程中模拟了这种 "is-a" (是一种) 的关系。它允许你创建一个新类(称为子类、派生类 Derived Class),这个新类可以自动获得另一个现有类(称为父类、基类 Base Class、超类 Superclass) 的非私有 (non-private) 成员(字段、属性、方法)。

使用继承的主要好处:

  • 代码重用 (Code Reuse): 父类中定义的通用属性和方法可以被所有子类共享,无需在每个子类中重复编写。
  • 建立层次结构 (Hierarchy): 清晰地表达类之间的关系,使代码结构更符合逻辑,更易于理解。
  • 可扩展性 (Extensibility): 可以在不修改现有父类的情况下,通过创建子类来扩展功能。
  • 多态的基础 (Foundation for Polymorphism): 继承是实现多态的前提(我们后面会细讲)。

 如何在 C# 中实现继承

在 C# 中,实现继承非常简单,使用冒号 : 紧跟在子类名称后面,然后是父类的名称。

基本语法:

class ChildClassName : ParentClassName
{
    // 子类特有的成员(字段、属性、方法)
    // 子类会自动继承父类中 public 和 protected 的成员
}

重要规则:

  • C# 只支持单继承 (Single Inheritance),意味着一个类只能直接继承自一个父类。但是,一个父类可以被多个子类继承。
  • 继承是传递的。如果 C 继承自 B,B 继承自 A,那么 C 会同时拥有 B 和 A 的非私有成员。
  • 所有 C# 类都隐式地直接或间接继承自 System.Object 类。Object 类提供了一些所有对象都应该有的基本方法,如 ToString(), Equals(), GetHashCode()。

示例:动物和狗

我们先定义一个 Animal 基类:

// Animal.cs
using System;
namespace InheritanceDemo
{
    // 基类 / 父类
    public class Animal
    {
        // 公共属性,可以被子类继承
        public string Name { get; set; }
        public int Age { get; set; }
        // 公共方法,可以被子类继承
        public void Eat()
        {
            Console.WriteLine($"{Name} 正在吃东西...");
        }
        public void Sleep()
        {
            Console.WriteLine($"{Name} 正在睡觉...");
        }
        // 构造函数 (父类的构造函数不会被子类直接继承,但子类需要调用它)
        public Animal(string name, int age)
        {
            Console.WriteLine("Animal 构造函数被调用!");
            this.Name = name;
            this.Age = age;
        }
        // 无参构造函数 (如果需要的话)
        // public Animal()
        // {
        //     Console.WriteLine("Animal 无参构造函数被调用!");
        //     Name = "未命名";
        //     Age = 0;
        // }
    }
}

现在,我们定义一个 Dog 类,让它继承自 Animal:

// Dog.cs
using System;
namespace InheritanceDemo
{
    // 派生类 / 子类 Dog 继承自 Animal
    public class Dog : Animal
    {
        // Dog 类特有的属性
        public string Breed { get; set; } // 品种
        // Dog 类特有的方法
        public void Bark()
        {
            // 子类可以直接访问从父类继承来的 public 成员
            Console.WriteLine($"{Name} ({Breed}) 正在汪汪叫!");
        }
        // --- 子类构造函数 ---
        // 子类需要负责调用父类的构造函数来初始化继承来的成员
        // 使用 : base(...) 语法来调用父类的构造函数
        public Dog(string name, int age, string breed) : base(name, age) // 调用父类 Animal 的带参构造函数
        {
            Console.WriteLine("Dog 构造函数被调用!");
            // 初始化子类自己特有的属性
            this.Breed = breed;
        }
        // 如果父类有无参构造函数,子类构造函数可以不显式调用 base(),
        // 编译器会自动调用父类的无参构造函数。
        // 但如果父类只有带参构造函数(像我们上面的 Animal),
        // 那么子类的所有构造函数都必须显式调用 base(...) 来匹配父类的某个构造函数。
    }
}

访问修饰符与继承 (protected)

我们已经知道 public(任何地方可访问)和 private(只能在类内部访问)。继承引入了一个新的访问修饰符:

  • protected: 成员可以被定义它的类以及它的所有子类访问,但不能从类的外部直接访问。

这提供了一种在保持封装的同时,允许子类访问和操作父类内部状态的方式。

代码示例:修改 Animal 类,添加 protected 成员

// Animal.cs (修改后)
public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }
    protected string Sound { get; set; } // 动物发出的声音,设为 protected
    public Animal(string name, int age)
    {
        Console.WriteLine("Animal 构造函数被调用!");
        this.Name = name;
        this.Age = age;
        this.Sound = "某种声音"; // 给 protected 成员一个默认值
    }
    public void MakeSound()
    {
        Console.WriteLine($"{Name} 发出了 {Sound}");
    }
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
}
// Dog.cs (现在可以访问 Sound)
public class Dog : Animal
{
    public string Breed { get; set; }
    public Dog(string name, int age, string breed) : base(name, age)
    {
        Console.WriteLine("Dog 构造函数被调用!");
        this.Breed = breed;
        // 子类可以访问并修改继承来的 protected 成员
        this.Sound = "汪汪";
    }
    public void Bark()
    {
        // 也可以直接使用继承来的 protected 成员
        // Console.WriteLine($"{Name} ({Breed}) 正在发出 {Sound} 的叫声!");
        // 或者调用继承来的 public 方法,该方法内部使用了 protected 成员
        MakeSound(); // 调用父类的 MakeSound,它会使用被子类修改后的 Sound 值
    }
}

子类构造函数与 base 关键字

  • 创建子类对象时,父类的构造函数总是会先于子类的构造函数被执行。这是为了确保在子类构造函数执行之前,从父类继承来的成员已经被正确初始化。
  • 子类构造函数必须负责调用其直接父类的某个构造函数。
  • 使用冒号 : 加上 base(...) 语法来显式调用父类的带参数构造函数。括号 () 中的参数列表必须与父类某个构造函数的参数列表匹配。
  • 如果子类构造函数没有显式使用 : base(...),编译器会尝试自动调用父类的无参数构造函数 base()
  • 重要: 如果父类没有无参数构造函数(比如你只定义了带参数的构造函数),那么子类的所有构造函数都必须显式使用 : base(...) 来调用父类的某个已定义的构造函数,否则编译会报错!

使用继承创建和操作对象

现在我们可以在 Program.cs 中使用 Animal 和 Dog 类了:

// Program.cs
using System;
namespace InheritanceDemo
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 创建父类对象
            Animal genericAnimal = new Animal("某种动物", 3);
            Console.WriteLine($"动物名字: {genericAnimal.Name}");
            genericAnimal.Eat();
            genericAnimal.MakeSound(); // 输出 "某种动物 发出了 某种声音"
            Console.WriteLine("--------------------");
            // 创建子类对象
            // 注意构造函数的调用顺序:先 Animal 后 Dog
            Dog myDog = new Dog("旺财", 2, "金毛");
            // 子类对象拥有父类的所有 public/protected 成员
            Console.WriteLine($"狗的名字: {myDog.Name}"); // 继承自 Animal
            Console.WriteLine($"狗的年龄: {myDog.Age}");   // 继承自 Animal
            myDog.Eat();              // 调用继承自 Animal 的方法
            myDog.Sleep();            // 调用继承自 Animal 的方法
            // 子类对象也拥有自己的成员
            Console.WriteLine($"狗的品种: {myDog.Breed}"); // Dog 特有属性
            myDog.Bark();             // 调用 Dog 特有方法 (内部会调用父类的 MakeSound)
            // 注意:无法从外部访问 protected 成员
            // Console.WriteLine(myDog.Sound); // 编译错误!'Sound' is inaccessible due to its protection level
            Console.ReadKey();
        }
    }
}

方法重写 (override 与 virtual) 简介 (为多态铺垫)

有时,子类可能需要提供一个与父类方法同名但行为不同的实现。例如,Animal 有一个 MakeSound() 方法,但不同的动物叫声不同,Dog 类应该发出 "汪汪" 声,Cat 类应该发出 "喵喵" 声。

这可以通过方法重写 (Method Overriding) 来实现:

  1. 父类中,将希望被子类重写的方法标记为 virtual。这表示该方法可以被子类提供一个新的实现。
  2. 子类中,使用 override 关键字来声明一个与父类 virtual 方法签名完全相同(名称、参数列表、返回类型都相同)的方法,并提供新的实现。

代码示例 (初步了解,多态时会细讲):

// Animal.cs (修改 MakeSound 为 virtual)
public class Animal
{
    // ... 其他成员 ...
    protected string Sound { get; set; } = "某种声音";
    // 将 MakeSound 标记为 virtual,允许子类重写
    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} 发出了 {Sound}");
    }
    // ...
}
// Dog.cs (重写 MakeSound)
public class Dog : Animal
{
    // ... 其他成员 ...
    public Dog(string name, int age, string breed) : base(name, age)
    {
        this.Breed = breed;
        // this.Sound = "汪汪"; // 可以不用在这里设置了,直接在重写的方法里体现
    }
    // 使用 override 重写父类的 virtual 方法 MakeSound
    public override void MakeSound()
    {
        // 提供 Dog 特有的实现
        Console.WriteLine($"{Name} ({Breed}) 正在汪汪叫!");
        // 如果需要,仍然可以调用父类的原始实现:
        // base.MakeSound(); // 会输出 "旺财 (金毛) 发出了 某种声音"
    }
    // Bark 方法现在可以直接调用重写后的 MakeSound
    public void Bark()
    {
        MakeSound();
    }
}
// Cat.cs (另一个子类)
public class Cat : Animal
{
    public Cat(string name, int age) : base(name, age) { }
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 正在喵喵叫!");
    }
}

现在,调用 myDog.MakeSound() 会直接执行 Dog 类中重写的版本。这就是多态的基础。

到你实践了!

  1. 创建 Shape 基类:
    • 定义一个 Shape 类,包含一个 protected string Color 属性和一个 public virtual void Draw() 方法,该方法打印 "正在绘制一个[颜色]的形状"。
    • 添加一个构造函数 public Shape(string color) 初始化颜色。
  2. 创建 Circle 子类:
    • 让 Circle 继承自 Shape。
    • 添加一个 public double Radius 属性。
    • 添加一个构造函数 public Circle(string color, double radius),调用父类构造函数并初始化半径。
    • 使用 override 重写 Draw() 方法,打印 "正在绘制一个半径为[半径]的[颜色]圆形"。
  3. 创建 Rectangle 子类:
    • 让 Rectangle 继承自 Shape。
    • 添加 public double Width 和 public double Height 属性。
    • 添加一个构造函数 public Rectangle(string color, double width, double height)。
    • 使用 override 重写 Draw() 方法,打印 "正在绘制一个宽[宽度]高[高度]的[颜色]矩形"。
  4. 在 Program.cs 中:
    • 创建 Circle 和 Rectangle 对象。
    • 调用它们的 Draw() 方法,观察输出是否是各自重写后的版本。
    • 尝试访问它们的 Color 属性(应该可以访问)。
斯哈斯哈斯哈,佳代子啊啊啊啊啊啊ᕕ(◠ڼ◠)ᕗᕕ(◠ڼ◠)ᕗᕕ(◠ڼ◠)ᕗᕕ(◠ڼ◠)ᕗᕕ(◠ڼ◠)ᕗ
最后更新于 2025-05-19