了解 C# OOP
继承和接口是面向对象编程(OOP)不可或缺的组成部分。 Tim Corey 在他的视频" C# 中的继承与接口:面向对象编程"中,详细解释了何时使用继承以及何时选择接口。
本文旨在为读者提供一份全面的蒂姆·科里视频指南。 它详细分解了视频中提供的关键概念、示例和代码解释,重点介绍了继承和接口之间的区别以及何时使用哪一个。
简介
Tim 在 (0:00) 首先强调了区分继承和接口的重要性。 他强调,需要了解何时使用每种概念才能取得最佳效果。 他的目标是通过例子来证明这一点,首先是错误地使用单继承,然后进行纠正。
创建项目
在 (1:08) 处,Tim 使用 .NET 5 创建了一个简单的控制台应用程序。他将该项目命名为"OODemoApp",并解释说其主要目标是演示概念,而不是创建可用于生产的代码。
理解遗传
Tim 在 (1:55) 深入探讨了继承的基础知识。 他将继承定义为一种机制,其中基类的属性和方法被派生类继承。 他强调,继承不应该仅仅用于代码重用和共享,而应该用于建立逻辑上的"是"关系。
要点:
- Is-a 关系:确保派生类是基类的类型。 -公共逻辑:继承的类应该共享公共逻辑,而不仅仅是属性或方法签名。
示例类别:租车
Tim 在 (7:52) 创建了一个 RentalCar 类来说明继承的基本概念。 该类代表佛罗里达州迈阿密一家租赁公司的租赁车辆。
public class RentalCar
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public int NumberOfPassengers { get; set; }
public void StartEngine()
{
Console.WriteLine("Turn key to ignition setting");
Console.WriteLine("Turn key to on");
}
public void StopEngine()
{
Console.WriteLine("Turn key to off");
}
}public class RentalCar
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public int NumberOfPassengers { get; set; }
public void StartEngine()
{
Console.WriteLine("Turn key to ignition setting");
Console.WriteLine("Turn key to on");
}
public void StopEngine()
{
Console.WriteLine("Turn key to off");
}
}展示错误继承的弊端
Tim 在 (10:15) 解释了不当使用继承会导致哪些问题。 他强调,如果继承被滥用,会导致代码难以管理和扩展。 他不建议仅仅为了共享代码而使用继承。
为汽车和卡车类型添加枚举
Tim 在 10:45 添加了汽车类型枚举。他创建了一个名为 Enums.cs 的新类文件,将所有枚举集中在一个地方。 此枚举将有助于区分不同的汽车类型。
// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}然后,他向 RentalCar 类添加了一个属性来指定汽车类型。
public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}扩大范围,包括卡车
正如 Tim 在 (12:27) 所解释的那样,租赁公司决定在其车队中增加卡车,这带来了新的要求。 他创建了一个名为 RentalTruck 的类,该类继承自父类 RentalVehicle。
public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}然后他定义了一个新的卡车类型枚举。
// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}处理不同类型的房产
Tim 在 (15:28) 强调,仅仅因为两个房产名称相同并不意味着它们是相同的。 他以 Style 属性为例来说明这一点,该属性可以表示不同的枚举值(CarType 表示轿车,TruckType 表示卡车)。
向舰队引入舰艇
租赁公司扩大了其船队规模,新增了船只。 Tim 通过创建一个 RentalBoat 类来演示如何处理这个问题。 起初看来,这似乎可行,因为船只与汽车和卡车有一些共同的特性。
public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}处理帆船相关事宜
帆船的引入带来了一项挑战,因为帆船没有发动机。 Tim 在 (19:57) 处阐述了这种情况下继承的局限性。
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}利用接口解决问题
Tim 建议将基类中的 StartEngine 和 StopEngine 方法设为虚方法,以便派生类在不使用这些方法时可以进行重写。
public abstract class RentalVehicle
{
// Common properties
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}public abstract class RentalVehicle
{
// Common properties
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}不应该调用的处理方法
Tim 在 (21:56) 解释了继承类中不应该调用的方法的弊端。 例如,RentalSailboat 类没有引擎,它继承了 RentalVehicle 类的 StartEngine 和 StopEngine 方法。 如果无意中调用了这些方法,则可能会出现问题,因为它们必须抛出异常来表明它们不适用。
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}认识到继承的局限性
Tim 在 (24:06) 强调,当继承不再具有逻辑意义时,它会导致代码库变得复杂混乱。 例如,帆船不应被视为带有发动机的租赁车辆。这表明了继承的局限性以及采用更优设计方法的必要性。
迈向更优的界面设计
为了解决这些问题,Tim 建议采用使用界面的更好设计。 他首先创建了一个名为"BetterOODemo"的新控制台应用程序项目,以演示改进后的方法。
定义接口
Tim 引入了 IRental 接口,用于封装所有出租房共有的属性。
public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}创建陆地车辆基础类
然后,蒂姆创建了一个陆地车辆的基础类,将车辆租赁的概念与车辆本身分开。
public abstract class LandVehicle
{
public int NumberOfPassengers { get; set; }
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}public abstract class LandVehicle
{
public int NumberOfPassengers { get; set; }
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}通过将基础车辆类重命名为 LandVehicle,Tim 确保只有合适的车辆才能继承与发动机相关的方法。
实施轿车和卡车分类
Tim 创建了 Car 和 Truck 类,它们继承自 LandVehicle 类并实现了 IRental 接口。
public class Car : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public CarType Style { get; set; }
}
public class Truck : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public TruckType Style { get; set; }
}public class Car : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public CarType Style { get; set; }
}
public class Truck : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public TruckType Style { get; set; }
}这种设计保持了清晰的关注点分离,确保每个类只具有与其类型相关的属性和方法。
避免代码重复
Tim 在 (31:41) 讨论了避免不必要的代码重复的重要性。 他解释说,虽然 IRental 接口要求多个类中具有相同的属性,但这并不被认为违反了 DRY(不要重复自己)原则,因为没有逻辑重复——只有属性声明。
实施租赁帆船课程
Tim 在 (35:09) 解释了如何通过实现 IRental 接口而不是继承 LandVehicle 来单独处理 RentalSailboat 类。 这种方法有助于避免因继承不当而带来的种种弊端。
public class Sailboat : IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
// Additional properties and methods specific to sailboats
}public class Sailboat : IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
// Additional properties and methods specific to sailboats
}管理不同的租赁
Tim 创建了一个列表来管理不同类型的租赁,利用 IRental 界面存储各种租赁类型,包括卡车、帆船和汽车。
List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};这种设计允许循环遍历租赁项目并访问常见属性,如RentalId。
foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}铸造成特定类型
为了访问不同租赁类型的特定属性和方法,Tim演示了如何使用is关键字来检查和转换对象到各自的类型。
foreach (var rental in rentals)
{
if (rental is Truck truck)
{
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
}
else if (rental is Sailboat sailboat)
{
// Access sailboat-specific properties
}
else if (rental is Car car)
{
// Access car-specific properties
}
}foreach (var rental in rentals)
{
if (rental is Truck truck)
{
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
}
else if (rental is Sailboat sailboat)
{
// Access sailboat-specific properties
}
else if (rental is Car car)
{
// Access car-specific properties
}
}接口的灵活性
Tim强调,使用接口可以为未来的变化提供灵活性。 例如,增加新的租赁类型,如水箱或电视机,不会扰乱现有的结构。
避免过度使用继承
Tim 建议不要过度使用继承进行代码共享,因为这会导致代码库变得复杂且缺乏灵活性。 相反,他建议利用接口和组合来实现预期结果,而无需将"is-a"关系延伸到其逻辑范围之外。
结论
Tim Corey 对面向对象编程中的继承和接口的解释,为创建可维护和灵活的代码提供了一条清晰的途径。 他通过展示常见的陷阱并提供精细的界面设计,确保开发人员能够就如何有效地构建应用程序做出明智的决定。

