理解 C# 面向對象編程
繼承和介面是物件導向程式設計(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 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) 所解釋的那樣,租車公司決定將卡車添加到他們的車隊中,這帶來了新的要求。 他創建了一個繼承自父類別 RentalVehicle 的 RentalTruck 類別。
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) 強調當繼承變得不符合邏輯時,它如何導致混亂且凌亂的程式碼庫。 例如,不應將帆船視為具有引擎的 RentalVehicle。這說明了繼承的限制以及對更佳設計方法的需求。
使用介面轉向更佳設計
為了解決這些問題,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; }
}
為陸地車輛創建基類
然後,Tim 創建了一個陸地車輛的基類,將車輛租賃的概念與車輛本身分開。
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 創建了汽車和卡車類別,這些類別繼承自 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) 解釋了如何單獨處理 RentalSailboat 類別,方法是實施 IRental 介面,而不是從 LandVehicle 繼承。 這種做法有助於避免與不適當繼承相關的陷阱。
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" }
};
這一設計允許對租賃進行循環 management 並訪問共用屬性,如 PricePerDay 和 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 建議不要為了共用程式碼而過度使用繼承,因為這會導致程式碼庫的複雜且不靈活。 相反,他建議使用介面與組合以達到所需的結果,而不將"是"的關係推向其邏輯界限之外。
結論
Tim Corey 對 OOP 中繼承和介面的解釋提供了一條創建可維護和靈活的程式碼的清晰路徑。 透過展示常見的陷阱和提供改進的介面設計,他確保開發者可以在有效構建應用程式結構時做出明智的決定。
