C# と .NET での継承
このチュートリアルでは、C# での継承について説明します。 継承は、オブジェクト指向プログラミング言語の一機能であり、特定の機能 (データおよび動作) を提供する基底クラスを定義し、その機能を継承またはオーバーライドする派生クラスを定義することができます。
前提条件
- Windows には Visual Studio をお勧めします。 Visual Studio のダウンロード センター ページから無料バージョンをダウンロードできます。 Visual Studio には .NET SDK が含まれます。
- また、C# DevKit で Visual Studio Code エディターを使用することもできます。 最新の .NET SDK を別にインストールする必要があります。
- 別のエディターを使う場合は、最新の .NET SDK をインストールする必要があります。
例の実行
このチュートリアル内の例を作成して実行するには、コマンド ラインの dotnet ユーティリティを使用します。 それぞれの例について、次の手順に従います。
例を格納するディレクトリを作成します。
コマンド プロンプトで dotnet new コンソール コマンドを入力し、新しい .NET Core プロジェクトを作成します。
例にあるコードをコピーして、コード エディターに貼り付けます。
コマンド ラインから dotnet restore コマンドを入力し、プロジェクトの依存関係を読み込みまたは復元します。
復元を必要とするすべてのコマンド (
dotnet new
、dotnet build
、dotnet run
、dotnet test
、dotnet publish
、dotnet pack
など) によって暗黙的に実行されるため、dotnet restore
を実行する必要がなくなりました。 暗黙的な復元を無効にするには、--no-restore
オプションを使用します。dotnet restore
などの、明示的な復元が意味のある一部のシナリオや、復元が行われるタイミングを明示的に制御する必要があるビルド システムでは、dotnet restore
は引き続き有用なコマンドです。NuGet フィードの管理方法については、
dotnet restore
のドキュメントをご覧ください。dotnet run コマンドを入力して、例をコンパイルし実行します。
背景: 継承とは何か
継承とは、オブジェクト指向プログラミングの基本的な属性の 1 つです。 親クラスの動作を再利用 (継承)、拡張、または変更する子クラスを定義することができます。 メンバーの継承元となるクラスを、基底クラスと呼びます。 基底クラスのメンバーを継承するクラスを、派生クラスと呼びます。
C# と .NET は単一継承のみをサポートしています。 つまり、1 つのクラスは、1 つのクラスからしか継承できないことになります。 ただし継承は推移的であり、一連の型の継承階層を定義することができます。 たとえば、D
型は C
型から継承でき、この `C` 型は B
型から継承され、この `B` 型は基底クラスである A
型から継承されます。 継承が推移的であるため、A
型のメンバーは D
型で使用できます。
基底クラスのすべてのメンバーが、派生クラスによって継承されるわけではありません。 以下のメンバーは継承されません。
静的コンスラクター。クラスの静的データを初期化するもの。
インスタンス コンストラクター。クラスの新しいインスタンスを作成するために呼び出すもの。 各クラスはそれ自身のコンストラクターを定義する必要があります。
ファイナライザー。ランタイムのガベージ コレクターによって呼び出され、クラスのインスタンスを破棄するもの。
他のすべての基底クラスのメンバーは派生クラスに継承されますが、それらが表示されるどうかはアクセシビリティに依存します。 メンバーのアクセシビリティは、次のとおり、派生したクラスの可視性に影響します。
プライベート メンバーは、基底クラスで入れ子になっている派生クラスでのみ表示されます。 それ以外の場合、派生クラスでは表示されません。 次の例では、
A.B
はA
から派生した入れ子になったクラスで、C
はA
から派生しています。 プライベートのA._value
フィールドは A.B で表示されます。しかしながら、C.GetValue
メソッドからコメントを削除して例をコンパイルしようとすると、コンパイラ エラー CS0122 "'A.value' is inaccessible due to its protection level." ("'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 のキーワードでマークされている必要があります。 既定では基底クラスのメンバーは virtual
としてマークされていないので、オーバーライドすることはできません。 次の例のように、非仮想メンバーをオーバーライドしようとすると、コンパイラ エラー CS0506 "<member> cannot override inherited member <member>" ("継承されたメンバー
public class A
{
public void Method1()
{
// Do something.
}
}
public class B : A
{
public override void Method1() // Generates CS0506.
{
// Do something else.
}
}
場合によっては、派生クラスは基底クラスの実装をオーバーライドする必要があります。 abstract キーワードでマークされた基底クラスのメンバーは、派生クラスによってオーバーライドされる必要があります。 次の例をコンパイルしようとすると、コンパイラ エラー 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 つのメンバーが存在することを示しています。 これらのメンバーのうちの 1 つは、パラメーターなし (既定) のコンストラクターで、C# コンパイラによって SimpleClass
型に自動的に提供されるものです。 あとの 8 つは 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" を返します。2 つのオブジェクトが等しいかどうか調べる 3 つのメソッド: パブリック インスタンス
Equals(Object)
メソッド、パブリック静的Equals(Object, Object)
メソッド、およびパブリック静的ReferenceEquals(Object, Object)
メソッド。 既定により、これらのメソッドは参照の等価性を調べます。つまり、等価であるためには、2 つのオブジェクトの変数が同じオブジェクトを参照している必要があります。パブリック
GetHashCode
メソッド。型インスタンスのハッシュされたコレクションでの使用を許可する値を計算します。パブリック
GetType
メソッド。SimpleClass
型を表す Type オブジェクトを返します。保護された 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# で作成できる型のカテゴリと、暗黙的に継承する元となる型の一覧です。 各々の基本型によって、暗黙的な派生型への継承を通して、異なるメンバーのセットが利用可能となります。
型のカテゴリ | 暗黙的な継承元 |
---|---|
class | Object |
struct | ValueType、Object |
enum | Enum、ValueType、Object |
delegate | MulticastDelegate、Delegate、Object |
継承と "is a" 関係
継承は通常、基底クラスと 1 つまたは複数の派生クラスとの "is a" 関係を表現するのに使用します。ここで、派生クラスは基底クラスの特殊化されたバージョン、つまり基底クラスの 1 つの型です。 たとえば、Publication
クラスはあらゆる種類の出版物を表しますが、Book
クラスおよび Magazine
クラスは特定の種類の出版物を表します。
注意
1 つのクラスまたは構造体は、1 つまたは複数のインターフェイスを実装できます。 インターフェイスの実装は、単一継承の回避策として、または構造体とともに継承を使用する方法として提示されることが多いですが、継承というよりは、インターフェイスとその実装型の間の別の関係 ("can do" 関係) を表すものとして意図されています。 インターフェイスは、その実装型で使用可能とする機能 (等価性を調べる機能、オブジェクトを比較または並べ替える機能、カルチャに依存した解析および書式設定のサポート機能など) のサブセットを定義します。
なお、"is a" 関係は、型とその型の特定のインスタンス化の間の関係も表します。 次の例では、Automobile
は一意の読み取り専用プロパティを 3 つ持つクラスです。自動車の製造メーカーである Make
、車種である Model
、そして製造年である Year
の 3 つです。 この 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 Motor Car 社によって製造された自動車を表すのに、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
を定義します。書籍、雑誌、新聞、ジャーナル、記事などの任意の種類の出版物を表します。さらに Publication
から派生する Book
クラスも定義します。 この例を拡張して、簡単に Magazine
、Journal
、Newspaper
、および Article
などの他の派生クラスを定義することができます。
基底 Publication クラス
Publication
クラスを設計するにあたり、次のように、設計についていくつか決定する必要があります。
どのメンバーを基底クラス
Publication
に含めるか、Publication
メンバーがメソッドの実装を提供するかどうか、Publication
をその派生クラスのテンプレートとなる抽象基底クラスとするかどうか。ここで、
Publication
クラスはメソッドの実装を提供します。 「抽象基底クラスとその派生クラスの設計」セクションには、抽象基底クラスを使用して、派生クラスがオーバーライドする必要があるメソッドを定義する例が含まれています。 派生クラスは、その派生型に適した任意の実装を提供することができます。コードを再利用する機能 (つまり、複数の派生クラスが基底クラスのメソッドの宣言と実装を共有し、それらをオーバーライドする必要がないこと) は、非抽象基底クラスの利点です。 そこで、コードが
Publication
型の複数または非常に特殊化されたものによって共有される可能性が高い場合は、メンバーをPublication
に追加します。 基底クラスの実装を効率的に提供できていないと、基底クラスでの単一の実装で済むところを、派生クラスでほぼ同一のメンバーの実装を行わなければいけないことになります。 複数の箇所で重複するコードを保守する必要が生じ、バグを引き起こす元となりえます。コードの再利用を最大化し、同時に論理的で直感的な継承階層を作成するために、
Publication
クラスには必ず、すべてもしくはほとんどの出版物に共通したデータと機能のみを含めるようにします。 そして派生クラスは、それ自身が表す特定の種類の出版物に固有のメンバーを実装します。クラス階層をどの程度まで拡張すべきか。 1 つの基底クラスと 1 つまたは複数の派生クラスではなく、3 つ以上のクラス階層を作るかどうか。 たとえば、
Publication
はPeriodical
の基底クラスになり得ますが、この Periodical はMagazine
、Journal
、およびNewspaper
の基底クラスです。この例では、
Publication
クラスと 1 つの派生クラスBook
という小さな階層を使用します。 この例を簡単に拡張して、Magazine
やArticle
など、Publication
から派生する多くの追加のクラスを作成できます。基底クラスのインスタンス化に意味があるのか。 意味がなければ、そのクラスには abstract キーワードを適用します。 それ以外の場合、
Publication
クラスはそのクラス コンストラクターを呼び出すことによってインスタンス化することができます。abstract
キーワードでマークされたクラスを、そのクラス コンストラクターへの直接呼び出しによってインスタンス化しようとすると、C# コンパイラはエラー CS0144 "Cannot create an instance of the abstract class or interface." ("抽象クラスまたはインターフェイスのインスタンスを作成できません") を生成します。リフレクションを使用してクラスをインスタンス化しようとすると、そのリフレクション メソッドは MemberAccessException をスローします。既定では、基底クラスはそのクラス コンストラクターを呼び出すことによってインスタンス化することができます。 クラス コンストラクターを明示的に定義する必要はありません。 基底クラスのソース コード内に存在しない場合、C# コンパイラは自動的に既定の (パラメーターなしの) コンストラクターを提供します。
この例では、
Publication
クラスをPublication
としてマークして、インスタンス化できないようにします。abstract
メソッドなしのabstract
クラスは、このクラスが、いくつかの具象クラス (Book
、Journal
など) で共有される抽象概念を表すことを示します。派生クラスで、特定のメンバーの基底クラス実装を継承する必要があるかどうか、基底クラス実装をオーバーライドするオプションがあるかどうか、または実装を提供する必要があるかどうか。 abstract キーワードを使用して、派生クラスで実装を提供するように強制します。 virtual キーワードを使用して、派生クラスによる基底クラス メソッドのオーバーライドを許可します。 既定では、基底クラスで定義されているメソッドはオーバーライドできません。
Publication
クラスにはabstract
メソッドはありませんが、クラス自体がabstract
になります。派生クラスが継承階層内の最後のクラスを表していて、それ自体が追加の派生クラスの基底クラスとして使用できないかどうか。 既定では、どのクラスも基底クラスとして使用できます。 sealed キーワードを適用すると、クラスが追加クラスの基底クラスとして使用できないことを示すことができます。 sealed クラスからの派生を試みると、コンパイラ エラー CS0509 "cannot derive from sealed type <typeName>" ("シール型 typeName から派生することができません") が生成されます。
この例では、派生クラスを
sealed
としてマークします。
次の例では、Publication
のソース コードを、Publication.PublicationType
プロパティに返される 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
クラスのソース コードが示すように、インスタンス コンストラクターは派生クラスのコンストラクターから直接呼び出すことができます。出版物に関する 2 つのプロパティ
Title
は読み取り専用の String プロパティで、Publication
コンストラクターを呼び出すことでその値が提供されます。Pages
は読み取り/書き込みの Int32 プロパティで、出版物の総ページ数を示します。 その値はtotalPages
というプライベート フィールドに格納されています。 正の数である必要があり、そうでなければ ArgumentOutOfRangeException がスローされます。出版社に関するメンバー
2 つの読み取り専用プロパティ
Publisher
とType
。 これらの値はもともとPublication
クラスのコンストラクターへの呼び出しによって提供されるものです。出版に関するメンバー
Publish
およびGetPublicationDate
という 2 つのメソッドは、発行日を設定して返すものです。Publish
メソッドは、呼び出されるとプライベートのpublished
フラグをtrue
に設定し、渡された日付を引数としてプライベートdatePublished
フィールドに割り当てます。GetPublicationDate
メソッドは、published
フラグがfalse
の場合に文字列 "NYP" を返し、true
の場合にdatePublished
フィールドの値を返します。著作権に関するメンバー
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
クラスは次の一意のメンバーおよびメンバー オーバーライドを定義します。
2 つのコンストラクター
2 つの
Book
コンストラクターは、共通パラメーターを 3 つ共有しています。 タイトルおよび出版社の 2 つは、Publication
コンストラクターのパラメーターに対応します。 3 つ目は著者で、パブリックの変更不可のAuthor
プロパティに格納されています。 1 つのコンストラクターには isbn パラメーターが 1 つ含まれていて、ISBN
自動プロパティに格納されています。最初のコンストラクターはこのキーワードを使用して、他のコンストラクターを呼び出します。 コンストラクター チェーンは、コンストラクターを定義する上で一般的なパターンです。 パラメーターが最も多いコンストラクターを呼び出すときに、パラメーターのより少ないコンストラクターが既定値を提供するものです。
2 番目のコンストラクターは base キーワードを使用して、基底クラスのコンストラクターにタイトルと出版社名を渡します。 ソース コードで基底クラスのコンストラクターを明示的に呼び出さない場合、C# コンパイラは、基底クラスの既定またはパラメーターなしのコンストラクターへの呼び出しを自動的に提供します。
読み取り専用の
ISBN
プロパティ。Book
オブジェクトの ISBN (一意の 10 ~ 13 桁の数字) を返します。 ISBN はBook
コンストラクターの 1 つに引数として提供されます。 ISBN は、コンパイラで自動生成されるプライベート バッキング フィールドに格納されます。読み取り専用の
Author
プロパティ。 著者名は両方のBook
コンストラクターに引数として提供され、プロパティに格納されます。価格に関する、2 つの読み取り専用の
Price
とCurrency
のプロパティ。 これらの値は、SetPrice
メソッド呼び出しで引数として提供されます。Currency
プロパティは 3 桁の ISO 通貨記号 (たとえば米ドルの場合は USD) です。 ISO 通貨記号は ISOCurrencySymbol プロパティから取得できます。 これらのプロパティは両方とも外部では読み取り専用ですが、Book
クラスのコードによって設定できます。SetPrice
メソッド。Price
プロパティおよびCurrency
プロパティの値を設定します。 これらの値は、それぞれ同じプロパティによって返されます。ToString
メソッド (Publication
から継承) へのオーバーライドと、Object.Equals(Object) メソッドおよび GetHashCode メソッド (Object から継承) へのオーバーライド。オーバーライドされない限り、Object.Equals(Object) メソッドは参照の等価性を調べます。 つまり、2 つのオブジェクト変数は同じオブジェクトを参照している場合に等価であると見なされます。 一方、
Book
クラスでは、2 つのBook
オブジェクトは同じ ISBN を持つ場合に等価であるはずです。Object.Equals(Object) メソッドをオーバーライドする場合、GetHashCode メソッドもオーバーライドする必要があります。このメソッドは、ランタイムで項目をハッシュされたコレクションに格納し効率的に取得するために使用する値を返すものです。 ハッシュ コードは、等価性のテストと一致する値を返します。 Object.Equals(Object) をオーバーライドして、2 つの
Book
オブジェクトの ISBN プロパティが等しい場合にtrue
を返すようにしたので、ISBN
プロパティによって返される文字列の GetHashCode メソッドを呼び出すことにより計算されるハッシュ コードを返します。
次の図は、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
オブジェクトをインスタンス化しても意味はないので、クラスは abstract キーワードでマークしました。
たとえば、2 次元の閉じた幾何学図形には、2 つのプロパティが含まれます。図形の内部の大きさである面積と、図形の辺に沿った長さである周です。 一方で、これらのプロパティの計算方法は、それぞれの図形によって違います。 たとえば、円周の計算式は、正方形の周の計算式とは異なります。 Shape
クラスは、abstract
メソッドがある abstract
クラスです。 これは、派生クラスで同じ機能が共有されるものの、それらの派生クラスではその機能が異なる方法で実装されることを示します。
次の例では、Area
と Perimeter
という 2 つのプロパティを定義する、Shape
という名前の抽象基底クラスを定義します。 クラスを abstract キーワードでマークするだけでなく、インスタンス メンバーもそれぞれ abstract キーワードでマークされます。 ここで Shape
は Object.ToString メソッドもオーバーライドして、完全修飾名ではなく、その型の名前を返します。 そして GetArea
と GetPerimeter
の 2 つの静的メンバーを定義し、呼び出し元で任意の派生クラスのインスタンスの面積と周を簡単に取得できるようにします。 これらのメソッドのいずれかに派生クラスのインスタンスを渡すとき、ランタイムは派生クラスのメソッド オーバーライドを呼び出します。
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
の 3 つのクラスを定義します。 これらのクラスはそれぞれ、その図形に一意の数式を使用して面積と周を計算します。 一部の派生クラスも、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
.NET