Earn More by Sharing What You Love
Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!

Tim Corey
49分41秒
继承和接口是面向对象编程(OOP)不可或缺的组成部分。 Tim Corey 在他的视频" C# 中的继承与接口:面向对象编程"中,详细解释了何时使用继承以及何时选择接口。
本文作为 Tim Corey 视频的综合指南。 它详细分解了视频中提供的关键概念、示例和代码解释,重点介绍了继承和接口之间的区别以及何时使用哪一个。
Tim 在 (0:00) 首先强调了区分继承和接口的重要性。 他强调,需要了解何时使用每种概念才能取得最佳效果。 他的目标是通过例子来证明这一点,首先是错误地使用单继承,然后进行纠正。
在 (1:08) 处,Tim 使用 .NET 5 创建了一个简单的控制台应用程序。他将该项目命名为"OODemoApp",并解释说其主要目标是演示概念,而不是创建可用于生产的代码。
Tim 在 (1:55) 深入探讨了继承的基础知识。 他将继承定义为一种机制,其中基类的属性和方法被派生类继承。 他强调,继承不应该仅仅用于代码重用和共享,而应该用于建立逻辑上的"是"关系。
要点:
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 Property RentalId As Integer
Public Property CurrentRenter As String
Public Property PricePerDay As Decimal
Public Property NumberOfPassengers As Integer
Public Sub StartEngine()
Console.WriteLine("Turn key to ignition setting")
Console.WriteLine("Turn key to on")
End Sub
Public Sub StopEngine()
Console.WriteLine("Turn key to off")
End Sub
End ClassTim 在 (10:15) 解释了不当使用继承会导致哪些问题。 他强调,如果继承被滥用,会导致代码难以管理和扩展。 他不建议仅仅为了共享代码而使用继承。
Tim 在 10:45 添加了汽车类型枚举。他创建了一个名为 Enums.cs 的新类文件,将所有枚举集中在一个地方。 此枚举将有助于区分不同的汽车类型。
// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}' Enums.vb
Public Enum CarType
Hatchback
Sedan
Compact
End Enum然后,他向 RentalCar 类添加了一个属性来指定汽车类型。
public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}Public Class RentalCar
Inherits RentalVehicle
Public Property Style As CarType
' Other properties and methods
End Class正如 Tim 在 (12:27) 所解释的那样,租赁公司决定在其车队中增加卡车,这带来了新的要求。 他创建了一个名为 RentalTruck 的类,该类继承自父类 RentalVehicle。
public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}Public Class RentalTruck
Inherits RentalVehicle
Public Property Style As TruckType
' Other properties and methods
End Class然后他定义了一个新的卡车类型枚举。
// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}' Enums.vb
Public Enum TruckType
ShortBed
LongBed
End EnumTim 在 (15:28) 强调,仅仅因为两个房产名称相同并不意味着它们是相同的。 他以 Style 属性为例来说明这一点,该属性可以表示不同的枚举值(CarType 表示轿车,TruckType 表示卡车)。
租赁公司扩大了其船队规模,新增了船只。 Tim 通过创建一个 RentalBoat 类来演示如何处理这个问题。 起初看来,这似乎可行,因为船只与汽车和卡车有一些共同的特性。
public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}Public Class RentalBoat
Inherits RentalVehicle
' Properties and methods specific to boats
End Class帆船的引入带来了一项挑战,因为帆船没有发动机。 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
Inherits RentalVehicle
Public Overrides Sub StartEngine()
Throw New NotImplementedException("I do not have an engine to start")
End Sub
Public Overrides Sub StopEngine()
Throw New NotImplementedException("I do not have an engine to stop")
End Sub
End ClassTim 建议将基类中的 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 MustInherit Class RentalVehicle
' Common properties
Public Overridable Sub StartEngine()
Console.WriteLine("Engine started")
End Sub
Public Overridable Sub StopEngine()
Console.WriteLine("Engine stopped")
End Sub
End ClassTim 在 (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
Inherits RentalVehicle
Public Overrides Sub StartEngine()
Throw New NotImplementedException("I do not have an engine to start")
End Sub
Public Overrides Sub StopEngine()
Throw New NotImplementedException("I do not have an engine to stop")
End Sub
End ClassTim 在 (24:06) 强调,当继承不再具有逻辑意义时,它会导致代码库变得复杂混乱。 例如,帆船不应被视为带有发动机的租赁车辆。这表明了继承的局限性以及采用更优设计方法的必要性。
为了解决这些问题,Tim 建议采用使用界面的更好设计。 他首先创建了一个名为"BetterOODemo"的新控制台应用程序项目,以演示改进后的方法。
Tim 引入了 IRental 接口,用于封装所有出租房共有的属性。
public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}Public Interface IRental
Property RentalId As Integer
Property CurrentRenter As String
Property PricePerDay As Decimal
End Interface然后,蒂姆创建了一个陆地车辆的基础类,将车辆租赁的概念与车辆本身分开。
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 MustInherit Class LandVehicle
Public Property NumberOfPassengers As Integer
Public Overridable Sub StartEngine()
Console.WriteLine("Engine started")
End Sub
Public Overridable Sub StopEngine()
Console.WriteLine("Engine stopped")
End Sub
End Class通过将基础车辆类重命名为 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
Inherits LandVehicle
Implements IRental
Public Property RentalId As Integer Implements IRental.RentalId
Public Property CurrentRenter As String Implements IRental.CurrentRenter
Public Property PricePerDay As Decimal Implements IRental.PricePerDay
Public Property Style As CarType
End Class
Public Class Truck
Inherits LandVehicle
Implements IRental
Public Property RentalId As Integer Implements IRental.RentalId
Public Property CurrentRenter As String Implements IRental.CurrentRenter
Public Property PricePerDay As Decimal Implements IRental.PricePerDay
Public Property Style As TruckType
End Class这种设计保持了清晰的关注点分离,确保每个类只具有与其类型相关的属性和方法。
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
Implements IRental
Public Property RentalId As Integer
Public Property CurrentRenter As String
Public Property PricePerDay As Decimal
' Additional properties and methods specific to sailboats
End ClassTim 创建了一个列表来管理不同类型的租赁,利用 IRental 界面存储各种租赁类型,包括卡车、帆船和汽车。
List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};Dim rentals As New List(Of IRental) From {
New Truck With {.CurrentRenter = "Truck Renter"},
New Sailboat With {.CurrentRenter = "Sailboat Renter"},
New Car With {.CurrentRenter = "Car Renter"}
}此设计允许循环遍历租赁,并访问诸如RentalId等常见属性。
foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}For Each rental In rentals
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}")
Next为访问不同租赁类型的特定属性和方法,蒂姆演示了如何使用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
}
}For Each rental In rentals
If TypeOf rental Is Truck Then
Dim truck As Truck = CType(rental, Truck)
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}")
ElseIf TypeOf rental Is Sailboat Then
Dim sailboat As Sailboat = CType(rental, Sailboat)
' Access sailboat-specific properties
ElseIf TypeOf rental Is Car Then
Dim car As Car = CType(rental, Car)
' Access car-specific properties
End If
NextTim强调,使用接口可以为未来的变化提供灵活性。 例如,增加新的租赁类型,如水箱或电视机,不会扰乱现有的结构。
Tim 建议不要过度使用继承进行代码共享,因为这会导致代码库变得复杂且缺乏灵活性。 相反,他建议利用接口和组合来实现预期结果,而无需将"is-a"关系延伸到其逻辑范围之外。
Tim Corey 对面向对象编程中的继承和接口的解释,为创建可维护和灵活的代码提供了一条清晰的途径。 他通过展示常见的陷阱并提供精细的界面设计,确保开发人员能够就如何有效地构建应用程序做出明智的决定。
Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!
Join our newsletter, you’ll get exclusive access on article updates. We value your privacy