什么是继承?为什么要使用继承?
想象一下现实世界中的分类:
- “狗” 是一种 “动物”。
- “汽车” 是一种 “交通工具”。
- “学生” 和 “老师” 都属于 “人”。
“狗” 继承了 “动物” 的基本特征(比如需要呼吸、会动),并有自己独特的特征(比如会汪汪叫)。“汽车” 继承了 “交通工具” 的特征(比如可以移动),并有自己的特征(比如有引擎、轮子)。
继承 (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) 来实现:
- 在父类中,将希望被子类重写的方法标记为 virtual。这表示该方法可以被子类提供一个新的实现。
- 在子类中,使用 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 类中重写的版本。这就是多态的基础。
到你实践了!
- 创建 Shape 基类:
- 定义一个 Shape 类,包含一个 protected string Color 属性和一个 public virtual void Draw() 方法,该方法打印 "正在绘制一个[颜色]的形状"。
- 添加一个构造函数 public Shape(string color) 初始化颜色。
- 创建 Circle 子类:
- 让 Circle 继承自 Shape。
- 添加一个 public double Radius 属性。
- 添加一个构造函数 public Circle(string color, double radius),调用父类构造函数并初始化半径。
- 使用 override 重写 Draw() 方法,打印 "正在绘制一个半径为[半径]的[颜色]圆形"。
- 创建 Rectangle 子类:
- 让 Rectangle 继承自 Shape。
- 添加 public double Width 和 public double Height 属性。
- 添加一个构造函数 public Rectangle(string color, double width, double height)。
- 使用 override 重写 Draw() 方法,打印 "正在绘制一个宽[宽度]高[高度]的[颜色]矩形"。
- 在 Program.cs 中:
- 创建 Circle 和 Rectangle 对象。
- 调用它们的 Draw() 方法,观察输出是否是各自重写后的版本。
- 尝试访问它们的 Color 属性(应该可以访问)。
Comments NOTHING