理解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(Don't Repeat Yourself)原則,因為未重複任何邏輯—僅是屬性聲明。
實現租賃帆船類
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" }
};此設計允許迴圈遍歷租賃並存取共通屬性如 CurrentRenter, 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中繼承與介面的解釋,提供了建立可維護與靈活的程式碼的明確道路。 通過展示常見陷阱並提供使用介面的精煉設計,他確保開發者可以做出明智的決策來有效地構建程式。

