Skip to content

C# - 继承

继承是面向对象编程(OOP)的基本支柱。它允许一个类继承另一个类(称为基类或父类)的字段和方法。继承的类称为派生类(或子类)。这种机制促进了代码的复用性,有助于建模现实世界中的“is-a”(是)关系,从而创建更具组织性和可维护性的应用程序。

通过使用继承,你可以创建一个定义通用功能的通用基类,然后创建更具体的派生类来添加或修改这些功能。例如,Car(汽车)是一种Vehicle(交通工具),而Bicycle(自行车)是一种Vehicle。Vehicle将是基类,而Car和Bicycle将是派生类。

在 C# 中,一个类只能从一个基类继承(单继承),但可以实现多个接口。这种设计避免了与多重类继承相关的复杂性和歧义问题,例如“菱形问题”。

创建派生类的语法很简单。你指定派生类名,后跟一个冒号(:),然后是基类名。

// 基类
public class BaseClass
{
// 基类的成员
}
// 派生类
public class DerivedClass : BaseClass
{
// 派生类的成员
}

让我们使用现代 C# 属性和构造函数,对经典的 Shape(形状)和 Rectangle(矩形)示例进行现代化改造。属性提供了一种比公共字段或简单的 getter/setter 方法更灵活、更安全的数据暴露方式。

using System;
nnamespace InheritanceExample
{
// 带属性的基类
public class Shape
{
// 宽度和高度的自动实现属性
public double Width { get; set; }
public double Height { get; set; }
public void DisplayDimensions()
{
Console.WriteLine($"Width: {Width}, Height: {Height}");
}
}
// 派生类
public class Rectangle : Shape
{
// Rectangle 特有的方法
public double GetArea() => Width * Height;
}
public class Program
{
public static void Main(string[] args)
{
var rect = new Rectangle();
// 设置从 Shape 继承的属性
rect.Width = 5.5;
rect.Height = 3.0;
rect.DisplayDimensions();
// 调用派生类的方法
Console.WriteLine($"Area: {rect.GetArea()}");
}
}
}

当上述代码被编译并执行时,会产生以下结果:

Width: 5.5, Height: 3.0
Area: 16.5

派生类构造函数必须调用其基类的构造函数。如果你不显式调用基类构造函数,编译器将尝试调用基类的默认(无参数)构造函数。要调用特定的基类构造函数,你需要在派生类的构造函数声明中使用 base 关键字。

这是一个示例,展示了 Tabletop 类如何使用构造函数初始化其 Rectangle 基类。

using System;
namespace BaseInitialization
{
public class Rectangle
{
public double Length { get; }
public double Width { get; }
// 基类构造函数
public Rectangle(double length, double width)
{
Length = length;
Width = width;
}
public double GetArea() => Length * Width;
public virtual void Display()
{
Console.WriteLine($"Length: {Length}");
Console.WriteLine($"Width: {Width}");
Console.WriteLine($"Area: {GetArea()}");
}
}
public class Tabletop : Rectangle
{
public decimal CostPerSquareUnit { get; set; }
// 调用基类构造函数的派生类构造函数
public Tabletop(double length, double width, decimal costPerSquareUnit) : base(length, width)
{
CostPerSquareUnit = costPerSquareUnit;
}
public decimal GetTotalCost() => (decimal)GetArea() * CostPerSquareUnit;
// 覆写 Display 方法以添加更多信息
public override void Display()
{
base.Display(); // 调用基类的 Display 方法
Console.WriteLine($"Total Cost: {GetTotalCost():C}"); // :C 格式化为货币
}
}
public class Program
{
public static void Main(string[] args)
{
var table = new Tabletop(4.5, 7.5, 70m); // 'm' 后缀表示 decimal 类型
table.Display();
}
}
}

当上述代码被编译并执行时,会产生以下结果:

Length: 4.5
Width: 7.5
Area: 33.75
Total Cost: $2,362.50

派生类可以提供其从基类继承的方法的自己的实现。要覆写一个方法,基类方法必须用 virtual、abstract 或 override 关键字标记。然后,派生类方法必须使用 override 关键字。

这个示例演示了多态性,其中一个 Animal 变量可以持有 Dog 对象,并且调用 MakeSound() 会执行 Dog 类中被覆写的方法。

using System;
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Some generic animal sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof! Woof!");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
public class Program
{
public static void Main()
{
Animal myAnimal = new Animal();
Animal myDog = new Dog(); // Dog 是一种 Animal
Animal myCat = new Cat(); // Cat 是一种 Animal
myAnimal.MakeSound();
myDog.MakeSound();
myCat.MakeSound();
}
}

执行后,该程序会输出:

Some generic animal sound
Woof! Woof!
Meow

你可以通过用 sealed 关键字标记一个类来阻止它被用作基类。这对于不打算被扩展的类很有用。例如,.NET 中的 string 类就是 sealed 的。

public sealed class FinalClass
{
public void ShowMessage()
{
Console.WriteLine("This class cannot be inherited.");
}
}
// 以下行会导致编译错误:
// public class AttemptToDerive : FinalClass { } // 错误: “AttemptToDerive”: 无法从密封类型“FinalClass”派生

尽管 C# 不支持从多个类继承,但一个类可以实现多个接口。接口定义了一个契约——一组实现类必须提供的公共方法和属性。这允许你以安全灵活的方式实现多态性和代码复用。

最佳实践: 当你只需要定义一组能力契约时(例如 ISerializable、IComparable),优先考虑实现接口而非类继承。

在这里,ElectricCar 类继承自 Vehicle,并且还实现了 IDrivable 和 IChargeable 接口,代表了一辆现代电动汽车。

using System;
// 可驾驶物品的接口
public interface IDrivable
{
void Start();
void Stop();
void Drive();
}
// 可充电物品的接口
public interface IChargeable
{
int BatteryLevel { get; }
void Charge();
}
// 基类
public abstract class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
}
// 实现多个接口的派生类
public class ElectricCar : Vehicle, IDrivable, IChargeable
{
public int BatteryLevel { get; private set; } = 50;
public void Start()
{
Console.WriteLine("Electric car started silently.");
}
public void Stop()
{
Console.WriteLine("Regenerative braking engaged. Car stopped.");
}
public void Drive()
{
if (BatteryLevel > 0)
{
Console.WriteLine("Driving the car, whirring sound.");
BatteryLevel -= 5;
}
else
{
Console.WriteLine("Battery is empty!");
}
}
public void Charge()
{
Console.WriteLine("Charging...");
BatteryLevel = 100;
Console.WriteLine("Battery fully charged.");
}
}
public class Program
{
public static void Main()
{
var myTesla = new ElectricCar { Make = "Tesla", Model = "Model Y" };
myTesla.Start();
myTesla.Drive();
Console.WriteLine($"Current battery: {myTesla.BatteryLevel}%");
myTesla.Charge();
Console.WriteLine($"Current battery: {myTesla.BatteryLevel}%");
myTesla.Stop();
}
}

当上述代码被编译并执行时,会产生类似于以下的结果:

Electric car started silently.
Driving the car, whirring sound.
Current battery: 45%
Charging...
Battery fully charged.
Current battery: 100%
Regenerative braking engaged. Car stopped.