跳至页脚内容
Iron Academy Logo
学习 C#
学习 C#

其他类别

了解 C# OOP

Tim Corey
49分41秒

继承和接口是面向对象编程(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 对面向对象编程中的继承和接口的解释,为创建可维护和灵活的代码提供了一条清晰的途径。 他通过展示常见的陷阱并提供精细的界面设计,确保开发人员能够就如何有效地构建应用程序做出明智的决定。

想要更深入地了解这些概念并观看它们的实际应用,请观看 Tim 的完整视频。 他的频道简直是编程教程的宝库。 别错过!

Hero Worlddot related to 了解 C# OOP
Hero Affiliate related to 了解 C# OOP

分享您的所爱,赚取更多收入

您为使用 .NET、C#、Java、Python 或 Node.js 的开发人员创建内容吗?将您的专业知识转化为额外收入!

钢铁支援团队

我们每周 5 天,每天 24 小时在线。
聊天
电子邮件
打电话给我