本教學課程介紹 C# 中的繼承。 繼承是面向物件程式設計語言的功能,可讓您定義提供特定功能(數據和行為)的基類,以及定義繼承或覆寫該功能的衍生類別。
先決條件
- 最新 .NET SDK
- Visual Studio Code 編輯器
- C# 開發套件
安裝指示
在 Windows 上,使用此 WinGet 組態檔 來安裝所有必要條件。 如果您已安裝某些專案,WinGet 將會略過此步驟。
- 下載檔案,然後按兩下以執行它。
- 閱讀許可協議,輸入 y,然後在系統提示接受時選取 [輸入]。
- 如果您在任務欄中收到閃爍的用戶帳戶控制 (UAC) 提示,請允許安裝繼續。
在其他平臺上,您必須個別安裝這些元件。
- 從 .NET SDK 下載頁面下載建議的安裝程式,然後按兩下以執行它。 下載頁面會偵測您的平臺,並建議您平臺的最新安裝程式。
- 從 Visual Studio Code 首頁下載最新的安裝程式,然後按兩下以執行它。 該頁面還會偵測您的平臺,並且應該提供適合您系統的正確連結。
- 按兩下 C# DevKit 擴充功能頁面上的 [安裝] 按鈕。 這樣會開啟 Visual Studio 程式代碼,並詢問您是否要安裝或啟用延伸模組。 選取 [安裝]。
執行範例
若要在本教學課程中建立並執行範例,您可以從命令行使用 dotnet 公用程式。 針對每個範例,請遵循下列步驟:
建立目錄來儲存範例。
在命令提示字元中輸入 dotnet new 主控台 命令,以建立新的 .NET Core 專案。
將範例中的程式代碼複製並貼到程式碼編輯器中。
從命令行輸入 dotnet restore 命令,以載入或還原專案的相依性。
您不必執行
dotnet restore,因為其會由需要進行還原的所有命令隱含執行,例如dotnet new、dotnet build、dotnet run、dotnet test、dotnet publish和dotnet pack。 若要停用隱含還原,請使用--no-restore選項。dotnet restore命令在適合進行明確還原的特定案例中仍可派上用場,例如 Azure DevOps Services 中的持續整合組建,或在需要明確控制何時進行還原的組建系統中。若要了解如何管理 NuGet 套件源,請參閱
dotnet restore文件。輸入 dotnet run 命令,以編譯和執行範例。
背景:什麼是繼承?
繼承 是面向物件程序設計的基本屬性之一。 它可讓您定義可重複使用(繼承)、擴充或修改父類別行為的子類別。 繼承成員的類別稱為 基類。 繼承基類成員的類別稱為 衍生類別。
C# 和 .NET 僅支援 單一繼承。 也就是說,類別只能繼承自單一類別。 不過,繼承是可轉移的,可讓您定義一組類型的繼承階層。 換句話說,類型 D 可以繼承自類型 C,而類型 C繼承自類型 A,而類型 A繼承自基類類型 。 因為繼承是可轉移的,因此類型 A 的成員可用於類型 D。
不是基類的所有成員都是由衍生類別繼承。 下列成員不會繼承:
靜態建構函式,其會初始化 類別的靜態數據。
實體建構函式,您會呼叫這個建構函式來建立 類別的新實例。 每個類別都必須定義自己的建構函式。
終結子,這些終結子由執行階段的垃圾收集器呼叫,用於銷毀類別的實例。
雖然基類的所有其他成員都是由衍生類別繼承的,但不論它們是否可見,都取決於其存取範圍。 成員的存取範圍會影響其衍生類別的可見性,如下所示:
Private 成員只能在其基類巢狀的衍生類別中顯示。 否則,在衍生類別中看不到它們。 在下列範例中,
A.B是衍生自A的巢狀類別,C衍生自A。 私人A._value欄位會顯示在 A.B 中。不過,如果您從C.GetValue方法中移除批注,並嘗試編譯此範例,它會產生編譯程序錯誤 CS0122:“'A._value' 由於其保護等級而無法存取。public class A { private int _value = 10; public class B : A { public int GetValue() { return _value; } } } public class C : A { // public int GetValue() // { // return _value; // } } public class AccessExample { public static void Main(string[] args) { var b = new A.B(); Console.WriteLine(b.GetValue()); } } // The example displays the following output: // 10受保護的 成員只能在衍生類別中顯示。
內部 成員只有在與基類位於相同組件的衍生類別中才可見。 衍生類別位於與基類不同的元件中,因此無法看到它們。
公用 成員在衍生類別中可見,並且是衍生類別公用介面的一部分。 公有繼承成員可以像是在衍生類別中定義的一樣被呼叫。 在下列範例中,類別
A會定義名為Method1的方法,而 類別B繼承自 類別A。 然後,此範例會呼叫Method1,就像是B上的實例方法一樣。public class A { public void Method1() { // Method implementation. } } public class B : A { } public class Example { public static void Main() { B b = new (); b.Method1(); } }
衍生類別也可以藉由提供替代實作,覆寫繼承的成員。 若要能夠覆寫成員,基類中的成員必須以 虛擬 關鍵詞標示。 根據預設,基類成員不會標示為 virtual,且無法覆寫。 嘗試覆寫非虛擬成員,如下列範例所示,會產生編譯程式錯誤 CS0506:“<成員> 無法覆寫繼承的成員 <成員>,因為它未標示為虛擬、抽象或覆寫。
public class A
{
public void Method1()
{
// Do something.
}
}
public class B : A
{
public override void Method1() // Generates CS0506.
{
// Do something else.
}
}
在某些情況下,衍生類別 必須 覆寫基類實作。 以 抽象 關鍵詞標示的基類成員需要衍生類別重寫它們。 嘗試編譯下列範例會產生編譯程式錯誤 CS0534:「<類別> 不會實作繼承的抽象成員 <成員>」,因為 類別 B 不提供 A.Method1的實作。
public abstract class A
{
public abstract void Method1();
}
public class B : A // Generates CS0534.
{
public void Method3()
{
// Do something.
}
}
繼承僅適用於類別和介面。 其他類型類別(結構、委派和列舉)不支持繼承。 由於這些規則,嘗試像下列範例一樣編譯程式代碼會產生編譯程式錯誤 CS0527:“介面清單中的類型 'ValueType' 不是介面。錯誤訊息指出,雖然您可以定義結構實作的介面,但不支持繼承。
public struct ValueStructure : ValueType // Generates CS0527.
{
}
隱含繼承
除了可能透過單一繼承取得的任何類型外,.NET 類型系統中的所有類型都隱含地繼承自 Object 或由它衍生的類型。 Object 的一般功能適用於任何類型。
若要查看隱含繼承的意義,讓我們定義新的類別,SimpleClass,這隻是空的類別定義:
public class SimpleClass
{ }
然後,您可以使用反映(這可讓您檢查類型的元數據以取得該類型的相關信息),以取得屬於 SimpleClass 類型的成員清單。 雖然您尚未在 SimpleClass 類別中定義任何成員,但範例的輸出指出它實際上有 9 個成員。 其中一個成員是 C# 編譯程式自動為 SimpleClass 類型提供的無參數(或預設)建構函式。 其餘八個是 Object的成員,這是 .NET 類型系統中所有類別和介面最終都會隱含繼承的類型。
using System.Reflection;
public class SimpleClassExample
{
public static void Main()
{
Type t = typeof(SimpleClass);
BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
MemberInfo[] members = t.GetMembers(flags);
Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
foreach (MemberInfo member in members)
{
string access = "";
string stat = "";
var method = member as MethodBase;
if (method != null)
{
if (method.IsPublic)
access = " Public";
else if (method.IsPrivate)
access = " Private";
else if (method.IsFamily)
access = " Protected";
else if (method.IsAssembly)
access = " Internal";
else if (method.IsFamilyOrAssembly)
access = " Protected Internal ";
if (method.IsStatic)
stat = " Static";
}
string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
Console.WriteLine(output);
}
}
}
// The example displays the following output:
// Type SimpleClass has 9 members:
// ToString (Method): Public, Declared by System.Object
// Equals (Method): Public, Declared by System.Object
// Equals (Method): Public Static, Declared by System.Object
// ReferenceEquals (Method): Public Static, Declared by System.Object
// GetHashCode (Method): Public, Declared by System.Object
// GetType (Method): Public, Declared by System.Object
// Finalize (Method): Internal, Declared by System.Object
// MemberwiseClone (Method): Internal, Declared by System.Object
// .ctor (Constructor): Public, Declared by SimpleClass
Object 類別的隱含繼承可讓 SimpleClass 類別使用這些方法:
公用
ToString方法會將SimpleClass物件轉換成其字串表示法,會傳回完整型別名稱。 在此情況下,ToString方法會傳回字串 「SimpleClass」。。測試兩個物件相等的三種方法:公用實例
Equals(Object)方法、公用靜態Equals(Object, Object)方法,以及公用靜態ReferenceEquals(Object, Object)方法。 根據預設,這些方法會測試參考相等性;也就是說,若要相等,兩個物件變數必須參考相同的物件。公用
GetHashCode方法,該方法計算出一個值,使得型別的實例可以在哈希集合中使用。public
GetType方法,會傳回代表 Type 型別的SimpleClass物件。受保護的 Finalize 方法,其設計目的是在垃圾收集器回收物件記憶體之前釋放非受控資源。
受保護的 MemberwiseClone 方法,這個方法會建立目前 對象的淺層複製品。
由於隱含繼承,您可以從 SimpleClass 物件呼叫任何繼承的成員,就好像它實際上是在 SimpleClass 類別中定義的成員一樣。 例如,下列範例會呼叫 SimpleClass.ToString 方法,SimpleClass 繼承自 Object。
public class EmptyClass
{ }
public class ClassNameExample
{
public static void Main()
{
EmptyClass sc = new();
Console.WriteLine(sc.ToString());
}
}
// The example displays the following output:
// EmptyClass
下表列出您可以在 C# 中建立的類型類別,以及其隱含繼承的來源類型。 每個基底類型都會讓一組不同的成員透過繼承提供給隱式衍生類型。
| 類型類別 | 隱含繼承自 |
|---|---|
| 班級 | Object |
| 結構體 | ValueType、Object |
| 列舉 | Enum、ValueType、Object |
| 委託 | MulticastDelegate、Delegate、Object |
繼承和「是一種」關係
一般而言,繼承是用來表示基類與一或多個衍生類別之間的「是」關聯性,其中衍生類別是基類的特殊版本;衍生類別是基類的類型。 例如,Publication 類別代表任何類型的發行集,而 Book 和 Magazine 類別則代表特定類型的發行集。
備註
類別或結構可以實作一或多個介面。 雖然介面的實作經常被視為單一繼承或結構使用繼承的權宜之計,但它的目的是表達介面與其實作型別之間的不同關係,即「能執行」的關係,而非繼承。 介面定義了一組功能子集(例如測試相等性、比較或排序物件的能力,或支援對文化特性敏感的剖析與格式化),並將這些功能提供給實作該介面的類型。
請注意,“is a” 也會表示類型與該類型之特定具現化之間的關聯性。 在下列範例中,Automobile 是具有三個唯一隻讀屬性的類別:Make汽車製造商:Model,汽車種類:和 Year,其製造年。 您的 Automobile 類別也有建構函式,其自變數會指派給屬性值,而且它會覆寫 Object.ToString 方法,以產生可唯一識別 Automobile 實例的字元串,而不是 Automobile 類別。
public class Automobile
{
public Automobile(string make, string model, int year)
{
if (make == null)
throw new ArgumentNullException(nameof(make), "The make cannot be null.");
else if (string.IsNullOrWhiteSpace(make))
throw new ArgumentException("make cannot be an empty string or have space characters only.");
Make = make;
if (model == null)
throw new ArgumentNullException(nameof(model), "The model cannot be null.");
else if (string.IsNullOrWhiteSpace(model))
throw new ArgumentException("model cannot be an empty string or have space characters only.");
Model = model;
if (year < 1857 || year > DateTime.Now.Year + 2)
throw new ArgumentException("The year is out of range.");
Year = year;
}
public string Make { get; }
public string Model { get; }
public int Year { get; }
public override string ToString() => $"{Year} {Make} {Model}";
}
在此情況下,您不應該依賴繼承來表示特定的汽車品牌和型號。 例如,您不需要定義 Packard 類型來代表 Packard 汽車公司製造的汽車。 相反地,您可以使用傳遞至其類別建構函式的適當值來建立 Automobile 物件來表示它們,如下列範例所示。
using System;
public class Example
{
public static void Main()
{
var packard = new Automobile("Packard", "Custom Eight", 1948);
Console.WriteLine(packard);
}
}
// The example displays the following output:
// 1948 Packard Custom Eight
根據繼承的 is-a 關係,最適合應用於基類和衍生類別,這些衍生類別會將其他成員新增至基類,或者需要基類中未提供的額外功能。
設計基類和衍生類別
讓我們看看設計基類及其衍生類別的程式。 在本節中,您將定義基類 Publication,代表任何類型的出版物,例如書籍、雜誌、報紙、期刊、文章等。您也會定義衍生自 Book的 Publication 類別。 您可以輕鬆地擴充範例來定義其他衍生類別,例如 Magazine、Journal、Newspaper和 Article。
基底「Publication」類別
在設計 Publication 類別時,您需要做出數個設計決策:
要包含在基底
Publication類別中的成員,以及Publication成員是否提供方法實作,還是Publication是抽象基類,可作為其衍生類別的範本。在此情況下,
Publication類別會提供方法實作。 設計抽象基類及其衍生類別 區段包含使用抽象基類定義衍生類別必須覆寫的方法範例。 衍生類別是免費的,提供任何適合衍生型別的實作。重複使用程式代碼的能力(也就是說,多個衍生類別共用基類方法的宣告和實作,而不需要覆寫它們)是非抽象基類的優點。 因此,如果他們的程式碼可能會被某些或大多數專用
Publication類型共享,您應該將成員新增至Publication。 如果您無法有效率地提供基類實作,您最終必須在衍生類別中提供基本上完全相同的成員實作,而不是基類中的單一實作。 需要在多個位置維護重複的程序代碼,是 Bug 的潛在來源。為了最大化程式代碼重複使用和建立邏輯和直覺式繼承階層,您想要確定您只包含
Publication類別中所有或大部分發行集通用的數據和功能。 衍生類別接著會實作成員,這些成員對於它們所代表的特定發行集類型而言是唯一的。類別階層應擴充到什麼程度。 您要開發三個或多個類別的階層,而不只是基類和一或多個衍生類別? 例如,
Publication可以是Periodical的基類,而Periodical則是Journal、Newspaper和 的基類。針對您的範例,您將使用
Publication類別和單一衍生類別的小型階層,Book。 您可以輕鬆地擴充此範例,以建立衍生自Publication的其他類別,例如Magazine和Article。具現化基類是否合理。 如果沒有,您應該將 抽象 關鍵詞 套用至類別。 否則,您可以藉由呼叫類別建構函式來具現化您的
Publication類別。 如果嘗試透過直接呼叫類別建構函式來具現化以abstract關鍵詞標示的類別,C# 編譯程式會產生錯誤 CS0144:「無法建立抽象類或介面的實例」。如果嘗試使用反映具現化 類別,反映方法會擲回 MemberAccessException。根據預設,基類可以藉由呼叫其類別建構函式來具現化。 您不需要明確定義類別建構函式。 如果基類的原始碼中沒有一個 ,C# 編譯程式會自動提供預設的 (無參數) 建構函式。
針對您的範例,您會將
Publication類別標示為 抽象,使其無法具現化。 沒有任何abstract方法的abstract類別表示這個類別代表在數個具體類別之間共用的抽象概念(例如Book、Journal)。衍生類別是否必須繼承特定成員的基類實作、他們是否可以選擇覆寫基類實作,還是必須提供實作。 您使用 抽象 關鍵詞來強制衍生類別提供實作。 您可以使用 虛擬 關鍵詞,允許衍生類別覆寫基類方法。 根據預設,基類中定義的方法 不可覆蓋。
Publication類別沒有任何abstract方法,但類別本身abstract。衍生類別是否代表繼承階層中的最後一個類別,而且本身不能當做其他衍生類別的基類使用。 根據預設,任何類別都可以做為基類。 您可以套用 密封 關鍵詞,指出類別無法做為任何其他類別的基類。 嘗試衍生自密封型別所產生的編譯程序錯誤 CS0509:「無法衍生自密封型別 <typeName>」。
例如,您會將衍生類別標示為
sealed。
下列範例顯示 Publication 類別的原始程式碼,以及 PublicationType 屬性所傳回的 Publication.PublicationType 列舉。 除了從 Object 繼承的成員外,Publication 類別定義了以下獨特成員及成員覆寫:
public enum PublicationType { Misc, Book, Magazine, Article };
public abstract class Publication
{
private bool _published = false;
private DateTime _datePublished;
private int _totalPages;
public Publication(string title, string publisher, PublicationType type)
{
if (string.IsNullOrWhiteSpace(publisher))
throw new ArgumentException("The publisher is required.");
Publisher = publisher;
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("The title is required.");
Title = title;
Type = type;
}
public string Publisher { get; }
public string Title { get; }
public PublicationType Type { get; }
public string? CopyrightName { get; private set; }
public int CopyrightDate { get; private set; }
public int Pages
{
get { return _totalPages; }
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
_totalPages = value;
}
}
public string GetPublicationDate()
{
if (!_published)
return "NYP";
else
return _datePublished.ToString("d");
}
public void Publish(DateTime datePublished)
{
_published = true;
_datePublished = datePublished;
}
public void Copyright(string copyrightName, int copyrightDate)
{
if (string.IsNullOrWhiteSpace(copyrightName))
throw new ArgumentException("The name of the copyright holder is required.");
CopyrightName = copyrightName;
int currentYear = DateTime.Now.Year;
if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
CopyrightDate = copyrightDate;
}
public override string ToString() => Title;
}
建構函式
因為
Publication類別是abstract,所以無法直接從程式代碼具現化,如下列範例所示:var publication = new Publication("Tiddlywinks for Experts", "Fun and Games", PublicationType.Book);不過,其實例建構函式可以直接從衍生類別建構函式呼叫,正如
Book類別的原始程式碼所示。兩個出版相關屬性
Title是只讀 String 屬性,其值是藉由呼叫Publication建構函式來提供。Pages是一個讀寫 Int32 屬性,表示出版物的總頁數。 此值會儲存在名為totalPages的私人欄位中。 它必須是正數,否則將拋出 ArgumentOutOfRangeException。與發行者相關的成員
兩個唯讀屬性,
Publisher和Type。 這些值最初是由呼叫Publication類別建構函式所提供。出版相關成員
兩種方法,
Publish和GetPublicationDate,設定並傳回發行日期。Publish方法會在呼叫私用published旗標時將私用published旗標設定為datePublished,並將傳遞給它的日期指派為私用 欄位的自變數。 如果GetPublicationDate旗標是published,則false方法會傳回字串 “NYP”,如果datePublished,則傳回true域的值。版權相關成員
Copyright方法採用著作權人名稱和著作權年度為自變數,並將它們指派給CopyrightName和CopyrightDate財產。ToString方法的覆寫如果類型未覆寫 Object.ToString 方法,它將傳回類型的完整名稱,這名稱在區分不同實例時難以使用。
Publication類別會覆寫 Object.ToString,以傳回Title屬性的值。
下圖說明基底 Publication 類別與其隱含繼承 Object 類別之間的關聯性。
Book 類別
Book 類別將書籍表示為特殊類型的出版物。 下列範例顯示 Book 類別的原始程式碼。
using System;
public sealed class Book : Publication
{
public Book(string title, string author, string publisher) :
this(title, string.Empty, author, publisher)
{ }
public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
{
// isbn argument must be a 10- or 13-character numeric string without "-" characters.
// We could also determine whether the ISBN is valid by comparing its checksum digit
// with a computed checksum.
//
if (!string.IsNullOrEmpty(isbn))
{
// Determine if ISBN length is correct.
if (!(isbn.Length == 10 | isbn.Length == 13))
throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
if (!ulong.TryParse(isbn, out _))
throw new ArgumentException("The ISBN can consist of numeric characters only.");
}
ISBN = isbn;
Author = author;
}
public string ISBN { get; }
public string Author { get; }
public decimal Price { get; private set; }
// A three-digit ISO currency symbol.
public string? Currency { get; private set; }
// Returns the old price, and sets a new price.
public decimal SetPrice(decimal price, string currency)
{
if (price < 0)
throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
decimal oldValue = Price;
Price = price;
if (currency.Length != 3)
throw new ArgumentException("The ISO currency symbol is a 3-character string.");
Currency = currency;
return oldValue;
}
public override bool Equals(object? obj)
{
if (obj is not Book book)
return false;
else
return ISBN == book.ISBN;
}
public override int GetHashCode() => ISBN.GetHashCode();
public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}
除了從 Publication 繼承的成員外,Book 類別定義了以下獨特成員及成員覆寫:
兩個建構函式
這兩個
Book建構函式會共用三個通用參數。 兩個 標題 和 發行者,對應於Publication建構函式的參數。 第三個是 作者,它會被儲存到公用不可變的Author屬性。 其中一個建構函式包含 isbn 參數,該參數會儲存在ISBNauto 屬性中。第一個建構函式會使用 這個 關鍵詞來呼叫其他建構函式。 建構函式鏈結是定義建構函式的常見模式。 當呼叫具有最大參數數目的建構函式時,具有較少參數的建構函式會提供預設值。
第二個建構函式會使用 基底 關鍵詞,將標題和發行者名稱傳遞至基類建構函式。 如果您未在原始程式碼中明確呼叫基類建構函式,C# 編譯程式會自動提供對基類預設或無參數建構函式的呼叫。
只讀
ISBN屬性,會傳回Book物件的國際標準書籍編號,這是唯一的 10 位或 13 位數字。 ISBN 會以自變數的形式提供給其中一個Book建構函式。 ISBN 會儲存在編譯程式自動產生的私人支援欄位中。唯讀
Author屬性。 作者名稱會以自變數的形式提供給Book建構函式,並儲存在屬性中。兩個唯讀價格相關屬性,
Price和Currency。 其值會在SetPrice方法呼叫中以自變數的形式提供。Currency屬性是三位數的 ISO 貨幣符號(例如美元是 USD)。 您可以從 ISOCurrencySymbol 屬性擷取 ISO 貨幣符號。 這兩個屬性都是外部只讀的,但兩者都可以由Book類別中的程式碼設定。SetPrice方法,可設定Price和Currency屬性的值。 這些值會由這些相同的屬性傳回。重寫
ToString方法(從Publication繼承),以及 Object.Equals(Object) 和 GetHashCode 方法(從 Object繼承)。除非被覆寫,否則 Object.Equals(Object) 方法會測試參考相等性。 也就是說,如果兩個物件變數參考相同的物件,則會被視為相等。 另一方面,在
Book類別中,如果兩個Book物件具有相同的 ISBN,則應該相等。當您覆寫 Object.Equals(Object) 方法時,您也必須覆寫 GetHashCode 方法,這個方法會傳回執行階段用來在雜湊集合中儲存專案以便有效檢索的值。 哈希碼應該會傳回與相等測試一致的值。 由於您覆寫了 Object.Equals(Object),以便在兩個
true物件的 ISBN 屬性相等時傳回Book,因此您會藉由呼叫 GetHashCode 屬性所傳回字串的ISBN方法來計算並傳回哈希碼。
下圖說明 Book 類別與其基類 Publication之間的關聯性。
您現在可以具現化 Book 對象、同時叫用其唯一和繼承的成員,並將它當做自變數傳遞給預期類型為 Publication 或 類型 Book之參數的方法,如下列範例所示。
public class ClassExample
{
public static void Main()
{
var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
"Public Domain Press");
ShowPublicationInfo(book);
book.Publish(new DateTime(2016, 8, 18));
ShowPublicationInfo(book);
var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
$"{((Publication)book).Equals(book2)}");
}
public static void ShowPublicationInfo(Publication pub)
{
string pubDate = pub.GetPublicationDate();
Console.WriteLine($"{pub.Title}, " +
$"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
}
}
// The example displays the following output:
// The Tempest, Not Yet Published by Public Domain Press
// The Tempest, published on 8/18/2016 by Public Domain Press
// The Tempest and The Tempest are the same publication: False
設計抽象基類及其衍生類別
在上一個範例中,您已定義基類,該基類提供一些方法的實作,以允許衍生類別共用程序代碼。 不過,在許多情況下,基類預期不會提供實作。 相反地,基類是 抽象類, 宣告 抽象方法;它可作為範本,定義每個衍生類別必須實作的成員。 一般而言,在抽象基類中,每個衍生類型的實作都對該類型而言是唯一的。 您使用抽象關鍵詞標記類別,因為實例化 Publication 物件沒有意義,雖然該類別確實提供了出版物通用功能的實作。
例如,每個封閉的二維幾何圖形都包含兩個屬性:區域、圖形的內部範圍;和 周長,或沿著圖形邊緣的距離。 不過,計算這些屬性的方式完全取決於特定圖形。 例如,計算圓形周長(或周長)的公式與正方形的周長不同。
Shape 類別是具有 abstract 方法的 abstract 類別。 這表示衍生類別共用相同的功能,但這些衍生類別會以不同的方式實作該功能。
下列範例會定義名為 Shape 的抽象基類,定義兩個屬性:Area 和 Perimeter。 除了使用 abstract 關鍵詞標記 類別之外,每個實例成員也會以 abstract 關鍵詞標記。 在此情況下,Shape 也會覆寫 Object.ToString 方法以傳回型別的名稱,而不是其完整限定名稱。 而且它會定義兩個靜態成員,GetArea 和 GetPerimeter,讓呼叫端輕鬆地擷取任何衍生類別實例的區域和周邊。 當您將衍生類別的實例傳遞給這些方法中的任一個時,執行階段會呼叫衍生類別的方法覆寫。
public abstract class Shape
{
public abstract double Area { get; }
public abstract double Perimeter { get; }
public override string ToString() => GetType().Name;
public static double GetArea(Shape shape) => shape.Area;
public static double GetPerimeter(Shape shape) => shape.Perimeter;
}
然後,您可以從代表特定圖形的 Shape 衍生某些類別。 下列範例會定義三個類別:Square、Rectangle和 Circle。 每個都會使用該特定圖形的唯一公式來計算區域和周邊。 某些衍生類別也會定義屬性,例如 Rectangle.Diagonal 和 Circle.Diameter,這些屬性是它們所代表圖形特有的。
using System;
public class Square : Shape
{
public Square(double length)
{
Side = length;
}
public double Side { get; }
public override double Area => Math.Pow(Side, 2);
public override double Perimeter => Side * 4;
public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
}
public class Rectangle : Shape
{
public Rectangle(double length, double width)
{
Length = length;
Width = width;
}
public double Length { get; }
public double Width { get; }
public override double Area => Length * Width;
public override double Perimeter => 2 * Length + 2 * Width;
public bool IsSquare() => Length == Width;
public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
}
public class Circle : Shape
{
public Circle(double radius)
{
Radius = radius;
}
public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);
public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);
// Define a circumference, since it's the more familiar term.
public double Circumference => Perimeter;
public double Radius { get; }
public double Diameter => Radius * 2;
}
下列範例使用衍生自 Shape的物件。 它會建立衍生自 Shape 的物件陣列,並呼叫 Shape 類別的靜態方法,從而封裝回傳的 Shape 屬性值。 執行階段會從衍生類型的覆寫屬性中擷取值。 此範例也會將陣列中的每個 Shape 物件轉換成其衍生類型,如果轉換成功,則會擷取該特定子類別的屬性 Shape。
using System;
public class Example
{
public static void Main()
{
Shape[] shapes = { new Rectangle(10, 12), new Square(5),
new Circle(3) };
foreach (Shape shape in shapes)
{
Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
$"perimeter, {Shape.GetPerimeter(shape)}");
if (shape is Rectangle rect)
{
Console.WriteLine($" Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
continue;
}
if (shape is Square sq)
{
Console.WriteLine($" Diagonal: {sq.Diagonal}");
continue;
}
}
}
}
// The example displays the following output:
// Rectangle: area, 120; perimeter, 44
// Is Square: False, Diagonal: 15.62
// Square: area, 25; perimeter, 20
// Diagonal: 7.07
// Circle: area, 28.27; perimeter, 18.85