次の方法で共有


C# と .NET での継承

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

[前提条件]

インストール手順

Windows では、この WinGet 構成ファイル を使用して、すべての前提条件をインストールします。 既に何かがインストールされている場合、WinGet はその手順をスキップします。

  1. ファイルをダウンロードし、ダブルクリックして実行します。
  2. 使用許諾契約書を読み、y と入力し、同意を求められたら Enter キーを選択します。
  3. タスク バーで点滅するユーザー アカウント制御 (UAC) プロンプトが表示された場合は、インストールを続行します。

他のプラットフォームでは、これらの各コンポーネントを個別にインストールする必要があります。

  1. .NET SDK のダウンロード ページから推奨インストーラーをダウンロードし、ダブルクリックして実行します。 ダウンロード ページでプラットフォームが検出され、プラットフォームの最新のインストーラーが推奨されます。
  2. Visual Studio Code のホーム ページから最新のインストーラーをダウンロードし、ダブルクリックして実行します。 このページではプラットフォームも検出され、リンクはシステムに適している必要があります。
  3. C# DevKit 拡張機能ページの [インストール] ボタンをクリックします。 これで Visual Studio Code が開き、拡張機能をインストールするか有効にするかを確認するメッセージが表示されます。 [インストール] を選択します。

例を実行する

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

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

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

  3. サンプルのコードをコピーしてコード エディターに貼り付けます。

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

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

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

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

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

背景: 継承とは

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

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

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

  • クラスの静的データを初期化する静的コンストラクター。

  • インスタンス コンストラクター。クラスの新しいインスタンスを作成するために呼び出します。 各クラスは、独自のコンストラクターを定義する必要があります。

  • ファイナライザー。クラスのインスタンスを破棄するためにランタイムのガベージ コレクターによって呼び出されます。

基底クラスの他のすべてのメンバーは派生クラスによって継承されますが、参照可能かどうかはアクセシビリティによって異なります。 メンバーのアクセシビリティは、次のように派生クラスの可視性に影響します。

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

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

  • パブリック メンバーは派生クラスで表示され、派生クラスのパブリック インターフェイスの一部です。 パブリック継承メンバーは、派生クラスで定義されている場合と同様に呼び出すことができます。 次の例では、クラス AMethod1 という名前のメソッドを定義し、クラス 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 "<member> は、継承されたメンバー <member> をオーバーライドできません。これは、仮想メンバー、抽象メンバー、またはオーバーライドとしてマークされていないためです。

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

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

場合によっては、派生クラスが基底クラスの実装をオーバーライド する必要があります抽象キーワードでマークされた基底クラスメンバーは、派生クラスがオーバーライドする必要があります。 次の例をコンパイルしようとすると、コンパイラ エラー CS0534 "<class> は継承された抽象メンバー <member>" を実装しません。これは、クラス BA.Method1の実装を提供しないためです。

public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

継承は、クラスとインターフェイスにのみ適用されます。 その他の型カテゴリ (構造体、デリゲート、列挙型) は継承をサポートしていません。 これらの規則により、次の例のようなコードをコンパイルしようとすると、コンパイラ エラー CS0527 "Type 'ValueType' in interface list is not an interface" が生成されます。エラー メッセージは、構造体が実装するインターフェイスを定義できますが、継承はサポートされていないことを示します。

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" を返します。

  • パブリック インスタンス Equals(Object) メソッド、パブリック静的 Equals(Object, Object) メソッド、パブリック静的 ReferenceEquals(Object, Object) メソッドの 2 つのオブジェクトの等価性をテストする 3 つのメソッド。 既定では、これらのメソッドは参照の等価性をテストします。つまり、等しい場合は、2 つのオブジェクト変数が同じオブジェクトを参照する必要があります。

  • パブリック GetHashCode メソッド。ハッシュされたコレクションで型のインスタンスを使用できるようにする値を計算します。

  • GetType型を表すType オブジェクトを返すパブリック SimpleClass メソッド。

  • プロテクト Finalize メソッド。オブジェクトのメモリがガベージ コレクターによって回収される前にアンマネージ リソースを解放するように設計されています。

  • 現在のオブジェクトの浅い複製を作成するプロテクト MemberwiseClone メソッド。

暗黙的な継承のため、SimpleClass クラスで定義されているメンバーであるかのように、SimpleClass オブジェクトから継承されたメンバーを呼び出すことができます。 たとえば、次の例では、SimpleClass.ToStringSimpleClassから継承した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
構造 体 ValueTypeObject
イーナム EnumValueTypeObject
デリゲート MulticastDelegateDelegateObject

継承と "is a" 関係

通常、継承は、基底クラスと 1 つ以上の派生クラスの間の "is a" リレーションシップを表すために使用されます。この場合、派生クラスは基底クラスの特殊なバージョンです。派生クラスは基底クラスの型です。 たとえば、 Publication クラスは任意の種類のパブリケーションを表し、 Book クラスと Magazine クラスは特定の種類のパブリケーションを表します。

クラスまたは構造体は、1 つ以上のインターフェイスを実装できます。 インターフェイスの実装は、多くの場合、単一継承の回避策として、または構造体で継承を使用する方法として提示されますが、インターフェイスとその実装型の間に継承とは異なるリレーションシップ ("can do" リレーションシップ) を表現することを目的としています。 インターフェイスは、機能のサブセット (等しいかどうかをテストする機能、オブジェクトを比較または並べ替える機能、カルチャに依存する解析と書式設定をサポートする機能など) を定義します。この機能は、インターフェイスが実装型で使用できるようにします。

"is a" は、型とその型の特定のインスタンス化との関係も表します。 次の例では、 Automobile は、 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 Company によって製造された自動車を表すために 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 クラスも定義します。 この例を簡単に拡張して、 MagazineJournalNewspaperArticleなどの他の派生クラスを定義できます。

基底 Publication クラス

Publication クラスを設計する際には、いくつかの設計上の決定を行う必要があります。

  • 基底 Publication クラスに含めるメンバー、および Publication メンバーがメソッド実装を提供するかどうか、または Publication が派生クラスのテンプレートとして機能する抽象基底クラスであるかどうかを示します。

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

    コードを再利用する機能 (つまり、複数の派生クラスが基底クラス メソッドの宣言と実装を共有し、オーバーライドする必要はありません) は、非抽象基底クラスの利点です。 そのため、コードが一部または最も特殊なPublication型によって共有される可能性が高い場合は、メンバーをPublicationに追加する必要があります。 基底クラスの実装を効率的に提供できない場合は、基底クラスの 1 つの実装ではなく、派生クラスでほぼ同じメンバー実装を提供する必要があります。 重複したコードを複数の場所に保持する必要がある場合は、バグの原因になる可能性があります。

    コードの再利用を最大化し、論理的で直感的な継承階層を作成するには、すべてまたはほとんどのパブリケーションに共通するデータと機能のみを Publication クラスに含めるようにする必要があります。 派生クラスは、そのクラスが表す特定の種類のパブリケーションに固有のメンバーを実装します。

  • クラス階層を拡張する距離。 単に基底クラスと 1 つ以上の派生クラスではなく、3 つ以上のクラスの階層を開発しますか? たとえば、 PublicationPeriodical の基底クラスであり、 MagazineJournalNewspaperの基底クラスになります。

    たとえば、 Publication クラスと 1 つの派生クラス ( Book) の小さな階層を使用します。 この例を簡単に拡張して、PublicationMagazineなど、Articleから派生する多数の追加クラスを作成できます。

  • 基底クラスをインスタンス化するのが理にかなっているかどうか。 そうでない場合は、 抽象 キーワードをクラスに適用する必要があります。 それ以外の場合は、クラス コンストラクターを呼び出すことによって、 Publication クラスをインスタンス化できます。 クラス コンストラクターへの直接呼び出しによって、 abstract キーワードでマークされたクラスをインスタンス化しようとすると、C# コンパイラによってエラー CS0144 "抽象クラスまたはインターフェイスのインスタンスを作成できません" が生成されます。リフレクションを使用してクラスをインスタンス化しようとすると、リフレクション メソッドは MemberAccessExceptionをスローします。

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

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

  • 派生クラスが特定のメンバーの基底クラスの実装を継承する必要があるかどうか、基底クラスの実装をオーバーライドするオプションがあるかどうか、または実装を提供する必要があるかどうか。 抽象キーワードを使用して、派生クラスに実装を強制します。 仮想キーワードを使用して、派生クラスが基底クラス メソッドをオーバーライドできるようにします。 既定では、基底クラスで定義されているメソッドはオーバーライド できません

    Publication クラスにはabstractメソッドはありませんが、クラス自体はabstract

  • 派生クラスが継承階層の最後のクラスを表し、それ自体を追加の派生クラスの基底クラスとして使用できないかどうか。 既定では、どのクラスも基底クラスとして機能できます。 sealed キーワードを適用して、クラスが追加のクラスの基底クラスとして機能できないことを示すことができます。 sealed クラスからの派生を試みると、コンパイラ エラー CS0509 "cannot derive from sealed type <typeName>" ("シール型 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 がスローされます。

  • パブリッシャー関連のメンバー

    PublisherTypeの 2 つの読み取り専用プロパティ。 値は、もともと Publication クラス コンストラクターの呼び出しによって提供されます。

  • 出版関連のメンバー

    PublishGetPublicationDateの 2 つのメソッドで、発行日を設定して返します。 Publish メソッドは、プライベート published フラグを呼び出すときにtrueに設定し、プライベート datePublished フィールドに引数として渡された日付を割り当てます。 GetPublicationDate メソッドは、published フラグがfalseされている場合は文字列 "NYP" を返し、datePublished場合はtrue フィールドの値を返します。

  • 著作権関連のメンバー

    Copyrightメソッドは、著作権者の名前と著作権の年を引数として受け取り、CopyrightNameおよびCopyrightDateプロパティに割り当てます。

  • ToString メソッドのオーバーライド

    型が Object.ToString メソッドをオーバーライドしない場合は、型の完全修飾名が返されます。これは、1 つのインスタンスを別のインスタンスと区別する際にほとんど使用されません。 Publication クラスは、Object.ToString プロパティの値を返すTitleをオーバーライドします。

次の図は、基底 Publication クラスとその暗黙的に継承された Object クラスの関係を示しています。

Object と Publication のクラス

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 つの Book コンストラクターは、3 つの共通パラメーターを共有します。 タイトルパブリッシャーの 2 つは、Publication コンストラクターのパラメーターに対応します。 3 つ目は author で、パブリックの変更できない Author プロパティに格納されます。 1 つのコンストラクターには isbn パラメーターが含まれています。これは、 ISBN 自動プロパティに格納されます。

    最初のコンストラクターでは、 この キーワードを使用して他のコンストラクターを呼び出します。 コンストラクターチェイニングは、コンストラクターを定義する一般的なパターンです。 パラメーターの数が少ないコンストラクターは、パラメーターの数が最も多いコンストラクターを呼び出すときに既定値を提供します。

    2 番目のコンストラクターでは 、基本 キーワードを使用して、タイトルと発行元の名前を基底クラスのコンストラクターに渡します。 ソース コードで基底クラスコンストラクターを明示的に呼び出さない場合、C# コンパイラは基底クラスの既定のコンストラクターまたはパラメーターなしのコンストラクターの呼び出しを自動的に提供します。

  • ISBN オブジェクトの国際標準の書籍番号(一意の 10 桁または 13 桁の数字) を返す、読み取り専用のBookプロパティです。 ISBN は、 Book コンストラクターの 1 つに引数として提供されます。 ISBN は、コンパイラによって自動生成されるプライベート バッキング フィールドに格納されます。

  • 読み取り専用 Author プロパティ。 作成者名は、 Book コンストラクターの両方に引数として指定され、プロパティに格納されます。

  • 読み取り専用の価格関連プロパティがPriceCurrencyの 2 つあります。 これらの値は、 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 メソッドもオーバーライドする必要があります。このメソッドは、効率的な取得のためにランタイムがハッシュコレクションに項目を格納するために使用する値を返します。 ハッシュ コードは、等価性のテストと一致する値を返す必要があります。 2 つのObject.Equals(Object) オブジェクトの ISBN プロパティが等しい場合にtrueを返すBookをオーバーライドしたので、GetHashCode プロパティによって返される文字列のISBN メソッドを呼び出して計算されたハッシュ コードを返します。

次の図は、 Book クラスとその基底クラスである Publication の関係を示しています。

Publication クラスおよび Book クラス

次の例に示すように、 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 オブジェクトをインスタンス化しても意味がないため、抽象キーワードでクラスをマークしました。

たとえば、閉じた 2 次元幾何学的図形には、2 つのプロパティが含まれます。面積は、図形の内側の範囲です。と境界、または図形の端に沿った距離。 ただし、これらのプロパティの計算方法は、特定の図形に完全に依存します。 たとえば、円の周長 (または円周) を計算する数式は、正方形とは異なります。 Shape クラスは、abstract メソッドを持つabstract クラスです。 これは、派生クラスが同じ機能を共有していることを示しますが、それらの派生クラスは異なる方法でその機能を実装します。

次の例では、ShapeArea の 2 つのプロパティを定義する Perimeter という名前の抽象基本クラスを定義します。 クラスを abstract キーワードでマークするだけでなく、各インスタンス メンバーも abstract キーワードでマークされます。 この場合、 Shape は、 Object.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 からいくつかのクラスを派生させることができます。 次の例では、 SquareRectangleCircleの 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