クラス

【C#】継承と変数の上書き: newキーワードとvirtualプロパティで安全にフィールドを隠蔽する方法

C#の継承では変数を直接上書きできず、子クラスで同名フィールドを宣言すると親の値を隠すだけです。

参照を親型にキャストすると親の変数が現れ、意図しない動作を招きやすいのでnewキーワードで隠蔽を明示し、動的な切り替えが必要な場合はvirtualプロパティやメソッドを使うほうが安全です。

C#の継承の基礎知識

クラスとオブジェクトの復習

C#において、クラスはオブジェクト指向プログラミングの基本的な構成要素です。

クラスは、データとそのデータに対して行える操作(メソッド)をひとまとめにした設計図のようなものであり、実際にプログラム内で利用されるときには「オブジェクト」と呼ばれる具体的な実体になります。

例えば、「車」というクラスを定義すると、そのクラスは車の属性(色、速度、メーカーなど)や動作(走る、止まる、曲がるなど)を記述します。

実際に車を表すオブジェクトを作成するには、そのクラスのインスタンス化を行います。

// 車クラスの定義
public class Car
{
    public string Color { get; set; }
    public int Speed { get; set; }
    public void Drive()
    {
        Console.WriteLine($"{Color}の車が時速{Speed}キロで走っています。");
    }
}
// オブジェクトの生成
Car myCar = new Car();
myCar.Color = "赤";
myCar.Speed = 60;
myCar.Drive(); // 出力: 赤の車が時速60キロで走っています。

この例では、Carクラスが設計図となり、myCarがその具体的なインスタンス(オブジェクト)です。

クラスは複数のオブジェクトを生成でき、各オブジェクトは独立した状態を持ちます。

フィールドとプロパティの違い

C#では、クラスのデータを保持するために「フィールド」と「プロパティ」の二つの要素が使われます。

これらは似ているようで異なる役割を持ち、適切に使い分けることが重要です。

フィールド

フィールドは、クラス内に直接定義される変数です。

クラスの状態を保持するために使われ、アクセス修飾子(publicprivateなど)を付けて外部からのアクセス範囲を制御します。

public class Person
{
    public string name; // 直接アクセス可能なフィールド
}

ただし、フィールドは外部から直接アクセスできるため、データの整合性やカプセル化の観点からはあまり推奨されません。

プロパティ

プロパティは、フィールドの値にアクセスするための「ゲッター(getter)」と「セッター(setter)」を持つ特殊なメンバーです。

これにより、値の取得や設定の際に追加の処理を挟むことができ、カプセル化やデータの検証が容易になります。

public class Person
{
    private string name; // 実体となるフィールド
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}

また、C#では自動実装プロパティも利用でき、フィールドの定義を省略して簡潔に書くことも可能です。

public class Person
{
    public string Name { get; set; } // 自動実装プロパティ
}
項目フィールドプロパティ
役割直接データを保持する変数データへのアクセスと操作を制御するメンバー
アクセス制御publicprivateで制御可能getsetでアクセスを制御
利点シンプルで高速カプセル化や検証処理が容易
推奨使用例内部処理や限定的な用途外部からのアクセスやデータの整合性確保に適用

このように、クラス設計においてはフィールドとプロパティの使い分けが重要です。

特に、外部からアクセスさせる必要がある場合は、プロパティを用いることで安全性と柔軟性を高めることができます。

フィールド隠蔽のメカニズム

同名フィールド宣言の動作

C#では、親クラスと子クラスに同じ名前のフィールドを宣言した場合、子クラスのフィールドは親クラスのフィールドを「隠す」形になります。

これを「フィールドの隠蔽」と呼びます。

例えば、親クラスにpublic int value;と定義し、子クラスでも同じ名前のpublic int value;を宣言すると、子クラスのインスタンスからvalueにアクセスしたときには、子クラスのvalueが優先されます。

public class Parent
{
    public int value = 10;
}
public class Child : Parent
{
    public int value = 20; // 親クラスのvalueを隠す
}
public class Program
{
    public static void Main()
    {
        Child child = new Child();
        Console.WriteLine(child.value); // 出力: 20
    }
}

この例では、child.valueは子クラスのvalueを参照しています。

親クラスのvalueにはアクセスできません。

newキーワードでの明示的隠蔽

親クラスと子クラスで同名のフィールドを定義する際には、newキーワードを付けて明示的に隠蔽を示すことが推奨されます。

これにより、意図的に親クラスのフィールドを隠すことを明示でき、コードの可読性や保守性が向上します。

public class Parent
{
    public int value = 10;
}
public class Child : Parent
{
    public new int value = 20; // 明示的に隠蔽を示す
}
public class Program
{
    public static void Main()
    {
        Child child = new Child();
        Console.WriteLine(child.value); // 出力: 20
    }
}

newキーワードを付けることで、コンパイラはこのフィールドが親クラスのフィールドを隠すためのものであると認識し、警告も出なくなります。

コンパイル時の警告とエラー

親クラスと子クラスに同名のフィールドを定義し、newキーワードを付けずに隠蔽しようとすると、コンパイラは警告を出します。

これは、意図的な隠蔽であることを明示しない場合に、誤解やバグの原因となる可能性があるためです。

public class Parent
{
    public int value = 10;
}
public class Child : Parent
{
    public int value = 20; // 警告: 'Child.value'は親クラスの'Parent.value'を隠します。'new'キーワードを使用してください。
}

この警告を解消するには、newキーワードを付ける必要があります。

public class Child : Parent
{
    public new int value = 20; // 警告解消
}

また、親クラスのフィールドにprivateprotected修飾子が付いている場合、子クラスから直接アクセスできません。

これにより、隠蔽の動作やアクセス範囲が制御されます。

派生クラスから親クラスのフィールドへアクセスする方法

baseキーワードの使い方

baseキーワードを使うと、親クラスのメンバーにアクセスできます。

特に、親クラスのコンストラクタやメソッド、プロパティにアクセスしたい場合に便利です。

ただし、baseキーワードはフィールドの隠蔽に対しても有効で、親クラスのフィールドにアクセスすることも可能です。

public class Parent
{
    public int value = 10;
}
public class Child : Parent
{
    public new int value = 20;
    public void ShowParentValue()
    {
        Console.WriteLine($"親クラスのvalue: {base.value}"); // 親クラスのvalueにアクセス
    }
}
public class Program
{
    public static void Main()
    {
        Child child = new Child();
        child.ShowParentValue(); // 出力: 親クラスのvalue: 10
    }
}

この例では、base.valueを使って親クラスのvalueにアクセスしています。

キャストによるアクセス

親クラスのフィールドにアクセスしたい場合、キャストを使って親クラスの型に変換する方法もあります。

public class Parent
{
    public int value = 10;
}
public class Child : Parent
{
    public new int value = 20;
}
public class Program
{
    public static void Main()
    {
        Child child = new Child();
        Parent parentRef = (Parent)child; // キャストして親クラスの型に変換
        Console.WriteLine(parentRef.value); // 出力: 10
    }
}

この方法では、childParent型にキャストすることで、親クラスのvalueにアクセスできます。

ただし、キャストは型の安全性に注意が必要です。

メソッド・プロパティのオーバーライドとの比較

virtualとoverrideの仕組み

C#において、メソッドやプロパティの多態性(ポリモーフィズム)を実現するためには、virtualoverrideキーワードを使用します。

virtualは親クラスのメンバーに付けて、そのメンバーが派生クラスでオーバーライド可能であることを示します。

一方、overrideは派生クラスで親クラスのvirtualメンバーを上書きするために使います。

具体的には、親クラスのメソッドにvirtualを付けると、そのメソッドは派生クラスでoverrideを使って再定義できるようになります。

public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("動物が鳴きます");
    }
}
public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("ワンワン");
    }
}

この例では、AnimalクラスのSpeakメソッドはvirtualであり、Dogクラスでoverrideして具体的な動作を定義しています。

これにより、Animal型の変数にDogのインスタンスを代入しても、Speakを呼び出すとDogの実装が実行されます。

Animal myDog = new Dog();
myDog.Speak(); // 出力: ワンワン

この仕組みは、実行時に呼び出すメソッドの実体を決定する「実行時バインディング(動的バインディング)」によって動作します。

実行時バインディングと名前隠蔽の違い

virtualoverrideを使ったメソッドのオーバーライドは、「実行時バインディング」によって動作します。

つまり、プログラムの実行時に、実際のオブジェクトの型に基づいて呼び出すメソッドが決定されるため、親クラスの型であっても子クラスのオーバーライドされたメソッドが呼び出されます。

一方、フィールドの隠蔽は「名前隠蔽(シャドウイング)」と呼ばれ、コンパイル時に決定される「静的バインディング」によって動作します。

親クラスと子クラスに同名のフィールドがある場合、変数の型に基づいてアクセスされるフィールドが決まるため、実行時の型に関係なく、コンパイル時に決まったフィールドが参照されます。

例を見てみましょう。

public class Parent
{
    public int value = 10;
}
public class Child : Parent
{
    public new int value = 20;
}
public class Program
{
    public static void Main()
    {
        Parent obj = new Child();
        Console.WriteLine(obj.value); // 出力は10
    }
}

この例では、objの型はParentなので、valueは親クラスのフィールドが参照されます。

これは、フィールドはvirtualoverrideの仕組みを持たないためです。

フィールドではオーバーライドできない理由

C#では、フィールドはvirtual修飾子を付けてオーバーライドすることができません。

これは、フィールドが「データの格納場所」であり、メソッドのような動作を持つものではないためです。

具体的な理由は以下の通りです。

  • 静的バインディング:フィールドはコンパイル時にアクセス先が決定されるため、実行時の型に基づく動的な振る舞いを持ちません
  • 設計上の制約:C#の設計では、ポリモーフィズムを実現するためには、メソッドやプロパティを使うことが推奨されており、フィールドはその対象外とされています
  • 安全性と明確性:フィールドのオーバーライドは、コードの理解や保守性を低下させる可能性があるため、言語仕様で禁止されています

そのため、フィールドの値を動的に切り替えたい場合は、virtualなメソッドやvirtualなプロパティを用いる必要があります。

これにより、親クラスと子クラス間での振る舞いの差異を安全かつ明確に表現できるのです。

安全なフィールド設計パターン

公開フィールドのリスク

公開フィールドは、クラスの外部から直接アクセスできるため、データの整合性や一貫性を保つのが難しくなります。

例えば、値の設定や取得に制約を設けたい場合でも、直接アクセスされるため、その制御ができません。

public class Person
{
    public int age; // 公開フィールド
}
public class Program
{
    public static void Main()
    {
        Person person = new Person();
        person.age = -5; // 不適切な値も設定可能
        Console.WriteLine(person.age); // 出力: -5
    }
}

この例では、ageに負の値を設定できてしまい、データの整合性が損なわれる可能性があります。

こうしたリスクを避けるために、公開フィールドの使用は推奨されません。

カプセル化とアクセサの導入

カプセル化の原則に従い、フィールドはprivateprotectedにし、アクセスにはpublicなプロパティやメソッドを用います。

これにより、値の検証や制約を設けることができ、データの整合性を保つことが可能です。

public class Person
{
    private int age; // 非公開フィールド
    public int Age
    {
        get { return age; }
        set
        {
            if (value >= 0)
            {
                age = value;
            }
            else
            {
                throw new ArgumentException("年齢は0以上でなければなりません");
            }
        }
    }
}
public class Program
{
    public static void Main()
    {
        Person person = new Person();
        person.Age = 25; // 正常な値
        Console.WriteLine(person.Age); // 出力: 25
        // person.Age = -5; // 例外発生
    }
}

この例では、Ageプロパティのsetアクセサ内で値の検証を行い、不適切な値の設定を防いでいます。

これにより、クラスの内部状態を安全に管理できます。

sealedによる継承制限

sealedキーワードをクラスやメソッドに付与することで、その継承やオーバーライドを禁止できます。

これにより、クラスの設計意図を明確にし、不用意な継承やオーバーライドによるバグや予期しない動作を防止します。

public sealed class FinalClass
{
    public void DoSomething()
    {
        Console.WriteLine("これ以上継承できません");
    }
}
// 以下はコンパイルエラー
// public class DerivedClass : FinalClass { }

また、sealedはメソッドにも適用でき、virtualなメソッドを継承先でオーバーライドさせたくない場合に有効です。

public class BaseClass
{
    public virtual void Display()
    {
        Console.WriteLine("基底クラス");
    }
}
public class DerivedClass : BaseClass
{
    public sealed override void Display()
    {
        Console.WriteLine("派生クラス");
    }
}
// さらに派生クラスでオーバーライドはできません
// public class SubDerived : DerivedClass
// {
//     public override void Display() { } // コンパイルエラー
// }

イミュータブルオブジェクトの応用

イミュータブル(不変)オブジェクトは、一度作成された後に状態を変更できないオブジェクトです。

これにより、スレッドセーフな設計や、予期しない状態の変更を防止できます。

イミュータブルオブジェクトの典型例は、すべてのフィールドをreadonlyにし、値をコンストラクタで設定するパターンです。

public class ImmutablePoint
{
    public readonly int X;
    public readonly int Y;
    public ImmutablePoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}
public class Program
{
    public static void Main()
    {
        ImmutablePoint point = new ImmutablePoint(10, 20);
        // point.X = 30; // コンパイルエラー
        Console.WriteLine($"X: {point.X}, Y: {point.Y}"); // 出力: X: 10, Y: 20
    }
}

また、C# 9.0以降では、record型を用いることで、より簡潔にイミュータブルなデータ構造を定義できます。

public record Point(int X, int Y);

このように、イミュータブルオブジェクトは、状態の変更を防ぎ、コードの安全性と予測可能性を高めるために有効な設計パターンです。

virtualプロパティで柔軟性を確保

基本的な実装手順

virtualプロパティを用いることで、親クラスのプロパティの振る舞いを派生クラスで変更できる柔軟性を持たせることが可能です。

実装の基本的な流れは以下の通りです。

まず、親クラスのプロパティにvirtual修飾子を付けて定義します。

これにより、そのプロパティは派生クラスでoverrideして上書きできるようになります。

public class Animal
{
    public virtual string Name
    {
        get { return "動物"; }
        set { /* 何もしない、または基本的な処理 */ }
    }
}

次に、派生クラスでoverrideを用いて親クラスのvirtualプロパティを上書きします。

public class Dog : Animal
{
    public override string Name
    {
        get { return "犬"; }
        set { /* 必要に応じて処理を追加 */ }
    }
}

このようにして、親クラスのインスタンスをAnimal型で保持していても、実際に生成されたオブジェクトがDogであれば、Nameプロパティの呼び出しはDogの実装が動的に呼び出されます。

Animal myPet = new Dog();
Console.WriteLine(myPet.Name); // 出力: 犬

抽象クラスとインターフェースの使い分け

virtualプロパティは、親クラスにおいて基本的な振る舞いを提供しつつ、必要に応じて派生クラスで上書きしたい場合に適しています。

一方、抽象クラスやインターフェースは、より厳格な設計や契約を定めるために用います。

  • 抽象クラスは、共通の実装とともに、必須のメソッドやプロパティを定義し、派生クラスに実装を強制します。abstract修飾子を用いて、実装の一部または全部を抽象化できます
public abstract class Shape
{
    public abstract double Area { get; }
}
  • インターフェースは、純粋に契約だけを定め、実装は派生クラスに任せます。複数のインターフェースを実装できるため、多重継承のような柔軟性があります
public interface IShape
{
    double Area { get; }
}

virtualプロパティは、これらの抽象化の仕組みと併用して、基本的な振る舞いを提供しつつ、必要に応じて派生クラスで詳細な実装を行う場面で有効です。

パフォーマンスとデバッグの視点

virtualプロパティの使用は、動的ディスパッチ(実行時バインディング)を伴います。

これにより、呼び出しごとに仮想テーブル(vtable)を参照し、適切なメソッドやプロパティの実装を決定します。

パフォーマンス面では、virtual呼び出しは静的呼び出しに比べて若干遅くなることがあります。

ただし、現代のコンパイラやランタイムでは、その差は非常に小さく、ほとんどのアプリケーションでは気にする必要はありません。

デバッグの観点では、virtualプロパティの多態性により、実行時にどの実装が呼び出されるかを追跡するのが難しくなる場合があります。

特に、複雑な継承階層や多重継承のような設計では、デバッグ時に実際の呼び出し先を理解するために、スタックトレースやデバッガの設定を工夫する必要があります。

また、virtualメンバーのオーバーライドを行うと、親クラスの実装と異なる動作をするため、意図しない動作を引き起こす可能性もあります。

これを避けるために、sealed overrideを用いて、特定の派生クラスでのオーバーライドを制限することも検討します。

よくある落とし穴とデバッグ方法

キャスト後の値が変わらない原因

キャストを行った後に値が期待通りに変わらない場合、その原因の一つはキャストの種類と対象の型の関係にあります。

特に、親クラスの型にキャストした場合、実際のオブジェクトの派生クラスの情報は失われ、親クラスのインターフェースだけが見える状態になります。

例えば、以下の例を見てみましょう。

public class Parent
{
    public int Value { get; set; } = 10;
}
public class Child : Parent
{
    public new int Value { get; set; } = 20;
}
public class Program
{
    public static void Main()
    {
        Child child = new Child();
        Parent parentRef = (Parent)child; // キャスト
        parentRef.Value = 30; // 親クラスのValueに設定
        Console.WriteLine(child.Value); // 出力: 20
        Console.WriteLine(parentRef.Value); // 出力: 30
    }
}

この例では、parentRef.Valueに値を設定しても、child.Valueは変わりません。

これは、ChildクラスのValuenewキーワードで隠蔽されたフィールドであり、親クラスのValueとは別のものだからです。

解決策:キャストではなく、親クラスのインターフェースやプロパティを通じてアクセスし、virtualoverrideを使った継承設計にすることが望ましいです。

また、値の変更が反映されない場合は、値の設定や取得のタイミング、またはreadonlyconst修飾子の使用も確認しましょう。

Shadowingとオーバーロードの混同

Shadowing(シャドウイング)とオーバーロードは、似たような用語ですが、全く異なる概念です。

  • Shadowing(隠蔽):親クラスと子クラスで同じ名前のメンバー(フィールドやメソッド)を定義した場合、子クラスのメンバーが親クラスのメンバーを「隠す」こと。これにより、親クラスのメンバーは子クラスのインスタンスからはアクセスできなくなります
public class Parent
{
    public void Method()
    {
        Console.WriteLine("親クラスのメソッド");
    }
}
public class Child : Parent
{
    public new void Method()
    {
        Console.WriteLine("子クラスのメソッド");
    }
}
  • オーバーロード:同じメソッド名でありながら、引数の型や数を変えることで複数のメソッドを定義すること。コンパイル時に呼び出すメソッドが決定されます
public class Calculator
{
    public int Add(int a, int b) => a + b;
    public double Add(double a, double b) => a + b;
}

混同しやすいポイント:Shadowingは継承関係において親子間のメンバーの名前の重複による隠蔽であり、オーバーロードは同一クラス内でのメソッドの多重定義です。

デバッグのポイント:Shadowingが原因で期待したメソッドやフィールドが呼び出されない場合は、baseキーワードやキャストを使って正しいメンバーにアクセスしているか確認しましょう。

リフレクションでのフィールド取得時の注意点

リフレクションは、実行時に型情報を動的に取得・操作できる強力な機能ですが、使用時にはいくつかの注意点があります。

  1. アクセス修飾子の制約privateprotectedなフィールドに対してリフレクションでアクセスする場合、BindingFlags.NonPublicを指定しなければアクセスできません。また、アクセス許可が必要な場合はFieldInfo.SetValueGetValueの前にFieldInfoIsAccessibleを設定する必要があります。
  2. フィールドの隠蔽と取得:親クラスと子クラスに同名のフィールドがある場合、GetFields()GetField()で取得できるフィールドは、指定したバインディングフラグに依存します。BindingFlags.DeclaredOnlyを付けると、そのクラスで宣言されたフィールドだけが取得され、継承元のフィールドは除外されます。
FieldInfo field = typeof(Derived).GetField("fieldName", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
  1. フィールドの型と値の整合性:取得したFieldInfoFieldTypeと実際の値の型が一致しているか確認し、キャストの安全性を確保しましょう。
  2. パフォーマンスの低下:リフレクションは通常のコードに比べて遅いため、頻繁に使用する場合はキャッシュを行うなどの工夫が必要です。
  3. セキュリティと安全性:リフレクションはアクセス制御を無視できるため、不用意に使用するとセキュリティリスクやバグの原因となることがあります。必要な範囲で慎重に利用しましょう。

リファクタリングの指針

フィールド隠蔽コードの検出方法

既存のコードベースにおいて、親クラスと子クラス間で同名のフィールドを定義している箇所を見つけるには、静的解析ツールやリファクタリング支援ツールを活用するのが効果的です。

Visual StudioやJetBrains RiderなどのIDEには、継承関係やフィールドの重複を検出する機能が備わっています。

具体的な検出方法は以下の通りです。

  • IDEの検索機能を使い、「親クラスのフィールド名」と「子クラスのフィールド名」を検索し、同じ名前のフィールドが複数存在している箇所を特定します
  • 継承関係のツリー表示を利用し、親クラスと子クラスのフィールド一覧を比較します
  • 静的コード解析ツール(例:RoslynアナライザやReSharper)を導入し、重複や隠蔽の可能性がある箇所を自動的に検出させます

これらの方法により、意図しないフィールド隠蔽や冗長なコードを早期に発見し、リファクタリングの対象とします。

プロパティへの置き換えステップ

フィールド隠蔽を避け、より安全で保守性の高いコードに改善するためには、フィールドをプロパティに置き換えるのが一般的です。

以下のステップで進めると効率的です。

  1. 対象のフィールドを特定:隠蔽や重複の疑いがあるフィールドをリストアップします。
  2. フィールドをprivateに変更:外部から直接アクセスされているフィールドをprivateにし、外部からのアクセスはプロパティを通じて行うようにします。
  3. プロパティを新たに定義publicなプロパティを作成し、必要に応じてgetsetに検証や制御を追加します。
// 変更前
public class Person
{
    public int age; // 直接公開されているフィールド
}
// 変更後
public class Person
{
    private int _age; // 内部フィールド
    public int Age
    {
        get { return _age; }
        set
        {
            if (value >= 0)
            {
                _age = value;
            }
            else
            {
                throw new ArgumentException("年齢は0以上にしてください");
            }
        }
    }
}
  1. 既存のコードを修正:フィールドアクセスをすべてプロパティアクセスに置き換え、動作確認を行います。
  2. 不要なフィールドの削除:古いフィールドは不要になったら削除します。

この方法により、データの整合性を保ちつつ、継承や拡張に柔軟に対応できるコードに改善します。

単体テストでの検証ポイント

リファクタリング後のコードの正確性と安全性を確保するために、単体テストは重要な役割を果たします。

特に、以下のポイントに注意してテストケースを設計します。

  • ゲッターとセッターの動作確認:プロパティに検証ロジックを追加した場合、正常系と異常系の両方をテストします
    • 正常系例:Ageに有効な値を設定し、正しく取得できるか
    • 異常系例:負の値や不正な値を設定したときに例外がスローされるか
  • 継承関係の動作確認:親クラスと子クラスのプロパティの動作が期待通りかを検証します
    • 例:親クラスのインスタンスと子クラスのインスタンスで、同じプロパティにアクセスしたときの挙動
  • リファクタリングによる副作用の検証:フィールドからプロパティへの変更に伴う動作の変化を確認します
    • 既存のビジネスロジックやユースケースに影響が出ていないかをテスト
  • 例外処理の検証:検証ロジックを持つセッターに対して、例外が適切にスローされるかを確認します

これらのポイントをカバーしたテストケースを作成し、自動化されたテストスイートに組み込むことで、リファクタリングの安全性を高め、将来的な変更にも耐えられる堅牢なコードを維持します。

実践ケーススタディ

レガシーコードの継承構造改善

既存のレガシーコードでは、しばしばフィールドの隠蔽や継承の乱用により、理解しづらくメンテナンス性の低い構造になっています。

例えば、親クラスと子クラスに同名のフィールドが存在し、意図しない動作やバグの原因となっているケースです。

改善の第一歩は、これらのフィールドをprivateにし、必要に応じてvirtualなプロパティに置き換えることです。

これにより、継承関係の明確化と、多態性の活用が可能となります。

// 改善前
public class Employee
{
    public string Name;
}
public class Manager : Employee
{
    public string Name; // 親クラスと同名のフィールド
}
// 改善後
public class Employee
{
    private string _name;
    public virtual string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}
public class Manager : Employee
{
    public override string Name
    {
        get { return base.Name; }
        set { base.Name = value; }
    }
}

このように、フィールドをプロパティに置き換えることで、継承構造の理解と拡張性が向上します。

ライブラリ提供側でのAPI設計

ライブラリやフレームワークのAPI設計においては、継承と多態性を意識した設計が重要です。

特に、公開APIのクラスやメソッドにvirtualabstractを適切に用いることで、ユーザ側の拡張やカスタマイズを容易にします。

例として、基本的な処理を提供する抽象クラスと、その拡張ポイントを設ける設計を考えます。

public abstract class DataProcessor
{
    public void Process()
    {
        LoadData();
        ProcessData();
        SaveData();
    }
    protected abstract void LoadData();
    protected abstract void ProcessData();
    protected abstract void SaveData();
}

利用者はこのクラスを継承し、必要な処理をオーバーライドします。

public class CsvDataProcessor : DataProcessor
{
    protected override void LoadData() { /* CSV読み込み処理 */ }
    protected override void ProcessData() { /* CSVデータ処理 */ }
    protected override void SaveData() { /* 保存処理 */ }
}

この設計により、APIの拡張性と柔軟性を確保しつつ、内部の処理の一貫性も維持できます。

ゲーム開発でのエンティティ継承

ゲーム開発では、多種多様なエンティティ(キャラクター、アイテム、環境オブジェクトなど)を効率的に管理するために、継承を活用します。

ただし、過度な継承は複雑さを増すため、適切な設計が求められます。

例えば、基本的なEntityクラスを定義し、そこからCharacterItemなどの派生クラスを作成します。

public class Entity
{
    public int Id { get; set; }
    public float PositionX { get; set; }
    public float PositionY { get; set; }
}
public class Character : Entity
{
    public string Name { get; set; }
    public int Health { get; set; }
}
public class Item : Entity
{
    public string ItemName { get; set; }
    public int Power { get; set; }
}

また、virtualプロパティやメソッドを用いて、各エンティティの動作や状態の拡張ポイントを設けることも有効です。

public class Entity
{
    public virtual void Update()
    {
        // 基本的な更新処理
    }
}
public class Character : Entity
{
    public override void Update()
    {
        base.Update();
        // キャラクター固有の更新処理
    }
}

この設計により、エンティティの多様性を保ちつつ、共通の動作を継承し、拡張しやすい構造を実現できます。

まとめ

この記事では、C#の継承と多態性の基本から、フィールド隠蔽やvirtualプロパティの活用方法、リファクタリングのポイントまで解説しました。

レガシーコードの改善やAPI設計、ゲーム開発の実践例を通じて、継承の適切な使い方と安全な設計手法を理解できます。

これらの知識を活用し、保守性と拡張性の高いコードを書けるようになります。

関連記事

Back to top button