C# と .NET での継承

このチュートリアルでは、C# での継承について説明します。 継承は、オブジェクト指向プログラミング言語の一機能であり、特定の機能 (データおよび動作) を提供する基底クラスを定義し、その機能を継承またはオーバーライドする派生クラスを定義することができます。

前提条件

例の実行

このチュートリアル内の例を作成して実行するには、コマンド ラインの dotnet ユーティリティを使用します。 それぞれの例について、次の手順に従います。

  1. 例を格納するディレクトリを作成します。

  2. コマンド プロンプトで dotnet new コンソール コマンドを入力し、新しい .NET Core プロジェクトを作成します。

  3. 例にあるコードをコピーして、コード エディターに貼り付けます。

  4. コマンド ラインから dotnet restore コマンドを入力し、プロジェクトの依存関係を読み込みまたは復元します。

    復元を必要とするすべてのコマンド (dotnet newdotnet builddotnet rundotnet testdotnet publishdotnet pack など) によって暗黙的に実行されるため、dotnet restore を実行する必要がなくなりました。 暗黙的な復元を無効にするには、--no-restore オプションを使用します。

    dotnet restoreなどの、明示的な復元が意味のある一部のシナリオや、復元が行われるタイミングを明示的に制御する必要があるビルド システムでは、dotnet restore は引き続き有用なコマンドです。

    NuGet フィードの管理方法については、dotnet restore のドキュメントをご覧ください。

  5. dotnet run コマンドを入力して、例をコンパイルし実行します。

背景: 継承とは何か

継承とは、オブジェクト指向プログラミングの基本的な属性の 1 つです。 親クラスの動作を再利用 (継承)、拡張、または変更する子クラスを定義することができます。 メンバーの継承元となるクラスを、基底クラスと呼びます。 基底クラスのメンバーを継承するクラスを、派生クラスと呼びます。

C# と .NET は単一継承のみをサポートしています。 つまり、1 つのクラスは、1 つのクラスからしか継承できないことになります。 ただし継承は推移的であり、一連の型の継承階層を定義することができます。 たとえば、D 型は C 型から継承でき、この `C` 型は B 型から継承され、この `B` 型は基底クラスである A 型から継承されます。 継承が推移的であるため、A 型のメンバーは D 型で使用できます。

基底クラスのすべてのメンバーが、派生クラスによって継承されるわけではありません。 以下のメンバーは継承されません。

他のすべての基底クラスのメンバーは派生クラスに継承されますが、それらが表示されるどうかはアクセシビリティに依存します。 メンバーのアクセシビリティは、次のとおり、派生したクラスの可視性に影響します。

  • プライベート メンバーは、基底クラスで入れ子になっている派生クラスでのみ表示されます。 それ以外の場合、派生クラスでは表示されません。 次の例では、A.BA から派生した入れ子になったクラスで、CA から派生しています。 プライベートの 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
    
  • プロテクト メンバーは派生クラスでのみ表示されます。

  • 内部メンバーは、基底クラスと同じアセンブリ内にある派生クラスでのみ表示されます。 基底クラスとは別のアセンブリにある派生クラスでは、表示されません。

  • パブリック メンバーは派生クラスで表示され、派生クラスのパブリック インターフェイスの一部です。 パブリックの継承されたメンバーは、派生クラスで定義された場合と同様に呼び出すことができます。 次の例では、クラス AMethod1 という名前のメソッドを定義し、クラス B がクラス A から継承します。 そこでこの例は、Method1B 上のインスタンス メソッドであるかのように呼び出します。

    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>" ("継承されたメンバー が virtual、abstract、または override でマークされていないため、 でオーバーライドすることができません") が生成されます。

public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

場合によっては、派生クラスは基底クラスの実装をオーバーライドする必要がありますabstract キーワードでマークされた基底クラスのメンバーは、派生クラスによってオーバーライドされる必要があります。 次の例をコンパイルしようとすると、コンパイラ エラー CS0534 (「<クラス> は継承抽象メンバー <メンバー> を実装しません。」) が生成されます。これは、クラス BA.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 メソッドを呼び出しますが、これは SimpleClassObject から継承しています。

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 ValueTypeObject
enum EnumValueTypeObject
delegate MulticastDelegateDelegateObject

継承と "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 クラスも定義します。 この例を拡張して、簡単に MagazineJournalNewspaper、および Article などの他の派生クラスを定義することができます。

基底 Publication クラス

Publication クラスを設計するにあたり、次のように、設計についていくつか決定する必要があります。

  • どのメンバーを基底クラス Publication に含めるか、Publication メンバーがメソッドの実装を提供するかどうか、Publication をその派生クラスのテンプレートとなる抽象基底クラスとするかどうか。

    ここで、Publication クラスはメソッドの実装を提供します。 「抽象基底クラスとその派生クラスの設計」セクションには、抽象基底クラスを使用して、派生クラスがオーバーライドする必要があるメソッドを定義する例が含まれています。 派生クラスは、その派生型に適した任意の実装を提供することができます。

    コードを再利用する機能 (つまり、複数の派生クラスが基底クラスのメソッドの宣言と実装を共有し、それらをオーバーライドする必要がないこと) は、非抽象基底クラスの利点です。 そこで、コードが Publication 型の複数または非常に特殊化されたものによって共有される可能性が高い場合は、メンバーを Publication に追加します。 基底クラスの実装を効率的に提供できていないと、基底クラスでの単一の実装で済むところを、派生クラスでほぼ同一のメンバーの実装を行わなければいけないことになります。 複数の箇所で重複するコードを保守する必要が生じ、バグを引き起こす元となりえます。

    コードの再利用を最大化し、同時に論理的で直感的な継承階層を作成するために、Publication クラスには必ず、すべてもしくはほとんどの出版物に共通したデータと機能のみを含めるようにします。 そして派生クラスは、それ自身が表す特定の種類の出版物に固有のメンバーを実装します。

  • クラス階層をどの程度まで拡張すべきか。 1 つの基底クラスと 1 つまたは複数の派生クラスではなく、3 つ以上のクラス階層を作るかどうか。 たとえば、PublicationPeriodical の基底クラスになり得ますが、この Periodical は MagazineJournal、および Newspaper の基底クラスです。

    この例では、Publication クラスと 1 つの派生クラス Book という小さな階層を使用します。 この例を簡単に拡張して、MagazineArticle など、Publication から派生する多くの追加のクラスを作成できます。

  • 基底クラスのインスタンス化に意味があるのか。 意味がなければ、そのクラスには abstract キーワードを適用します。 それ以外の場合、Publication クラスはそのクラス コンストラクターを呼び出すことによってインスタンス化することができます。 abstract キーワードでマークされたクラスを、そのクラス コンストラクターへの直接呼び出しによってインスタンス化しようとすると、C# コンパイラはエラー CS0144 "Cannot create an instance of the abstract class or interface." ("抽象クラスまたはインターフェイスのインスタンスを作成できません") を生成します。リフレクションを使用してクラスをインスタンス化しようとすると、そのリフレクション メソッドは MemberAccessException をスローします。

    既定では、基底クラスはそのクラス コンストラクターを呼び出すことによってインスタンス化することができます。 クラス コンストラクターを明示的に定義する必要はありません。 基底クラスのソース コード内に存在しない場合、C# コンパイラは自動的に既定の (パラメーターなしの) コンストラクターを提供します。

    この例では、Publication クラスを Publication としてマークして、インスタンス化できないようにします。 abstract メソッドなしの abstract クラスは、このクラスが、いくつかの具象クラス (BookJournal など) で共有される抽象概念を表すことを示します。

  • 派生クラスで、特定のメンバーの基底クラス実装を継承する必要があるかどうか、基底クラス実装をオーバーライドするオプションがあるかどうか、または実装を提供する必要があるかどうか。 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 つの読み取り専用プロパティ PublisherType。 これらの値はもともと 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 クラスの関係を表しています。

The Object and Publication classes

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 つの読み取り専用の PriceCurrency のプロパティ。 これらの値は、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 の関係を表しています。

Publication and Book classes

これで、次の例に示すように、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 クラスです。 これは、派生クラスで同じ機能が共有されるものの、それらの派生クラスではその機能が異なる方法で実装されることを示します。

次の例では、AreaPerimeter という 2 つのプロパティを定義する、Shape という名前の抽象基底クラスを定義します。 クラスを abstract キーワードでマークするだけでなく、インスタンス メンバーもそれぞれ abstract キーワードでマークされます。 ここで ShapeObject.ToString メソッドもオーバーライドして、完全修飾名ではなく、その型の名前を返します。 そして GetAreaGetPerimeter の 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 から、いくつかのクラスを派生させることができます。 次の例では、SquareRectangle、および Circle の 3 つのクラスを定義します。 これらのクラスはそれぞれ、その図形に一意の数式を使用して面積と周を計算します。 一部の派生クラスも、Rectangle.DiagonalCircle.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