ポリモーフィズム

ポリモーフィズムは、カプセル化と継承に次ぐ、オブジェクト指向プログラミングの第 3 の柱と言われることがよくあります。 ポリモーフィズムは、ギリシャ語で "多形" を意味し、次の 2 つの側面を持っています。

  • メソッド パラメーター、コレクション、配列などに渡された派生クラスのオブジェクトは、実行時に基底クラスのオブジェクトとして扱われることがあります。 このポリモーフィズムが発生すると、オブジェクトの宣言された型はその実行時の型と同じではなくなります。
  • 基底クラスでは、"virtualメソッド" を定義して実行できます。派生クラスでそれをオーバーライドすると、独自の定義と実装を提供できます。 実行時には、クライアント コードがメソッドを呼び出したとき、CLR によってオブジェクトの実行時の型が検索され、仮想メソッドのオーバーライドが呼び出されます。 ソース コード内で、基底クラスでメソッドを呼び出し、そのメソッドの派生クラス版を実行することができます。

仮想メソッドを使用すると、関連するオブジェクトのグループを同一の方法で扱うことができます。 たとえば、描画サーフェイスにさまざまな種類の図形を作成できる描画アプリケーションがあるとします。 コンパイル時には、ユーザーがどのような種類の図形を作成するかわかりません。 しかし、アプリケーションでは、作成されたさまざまな種類の図形を追跡し、ユーザーのマウス操作に応じて更新する必要があります。 ポリモーフィズムを使用すると、2 つの基本的な手順でこの問題を解決できます。

  1. 各図形クラスが共通の基底クラスから派生するようなクラス階層を作成します。
  2. 仮想メソッドを使用して、基底クラスの 1 つのメソッドを呼び出すことで、派生クラスの適切なメソッドが呼び出されるようにします。

まず、Shape という基底クラスと、RectangleCircleTriangle などの派生クラスを作成します。 Shape クラスで Draw という仮想メソッドを定義し、各派生クラスでそれをオーバーライドして、そのクラスが表す特定の図形を描画します。 List<Shape> オブジェクトを作成し、CircleTriangleRectangle をそれに追加します。

public class Shape
{
    // A few example members
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }

    // Virtual method
    public virtual void Draw()
    {
        Console.WriteLine("Performing base class drawing tasks");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        // Code to draw a circle...
        Console.WriteLine("Drawing a circle");
        base.Draw();
    }
}
public class Rectangle : Shape
{
    public override void Draw()
    {
        // Code to draw a rectangle...
        Console.WriteLine("Drawing a rectangle");
        base.Draw();
    }
}
public class Triangle : Shape
{
    public override void Draw()
    {
        // Code to draw a triangle...
        Console.WriteLine("Drawing a triangle");
        base.Draw();
    }
}

描画サーフェイスを更新するには、foreach ループを使用してリストを反復処理し、リスト内の各 Shape オブジェクトの Draw メソッドを呼び出します。 リスト内の各オブジェクトの宣言された型は Shape ですが、呼び出されるのは実行時の型 (それぞれの派生クラスでオーバーライドされたメソッド) になります。

// Polymorphism at work #1: a Rectangle, Triangle and Circle
// can all be used wherever a Shape is expected. No cast is
// required because an implicit conversion exists from a derived
// class to its base class.
var shapes = new List<Shape>
{
    new Rectangle(),
    new Triangle(),
    new Circle()
};

// Polymorphism at work #2: the virtual method Draw is
// invoked on each of the derived classes, not the base class.
foreach (var shape in shapes)
{
    shape.Draw();
}
/* Output:
    Drawing a rectangle
    Performing base class drawing tasks
    Drawing a triangle
    Performing base class drawing tasks
    Drawing a circle
    Performing base class drawing tasks
*/

C# では、すべての型がポリモーフィックです。これは、ユーザー定義型を含むすべての型が Object から派生するためです。

ポリモーフィズムの概要

仮想メンバー

派生クラスが基底クラスから継承されると、基底クラスのすべてのメンバーが含まれます。 基底クラスで宣言されているすべての動作は、派生クラスの一部です。 これにより、派生クラスのオブジェクトを基底クラスのオブジェクトとして扱うことができます。 アクセス修飾子 (publicprotectedprivate など) は、これらのメンバーが派生クラスの実装からアクセスできるかどうかを決定します。 仮想メソッドを使用すると、デザイナーは派生クラスの動作についてさまざまな選択肢を持つことができます。

  • 派生クラスでは基底クラスの仮想メンバーがオーバーライドされ、新しい動作を定義できます。
  • 派生クラスでは最も近い基底クラス メソッドがオーバーライドされることなく継承され、既存の動作が維持されますが、さらに派生したクラスではメソッドをオーバーライドできるようになります。
  • 派生クラスでは、基底クラスの実装を隠ぺいするメンバーの非仮想実装を新しく定義できます。

派生クラスが基底クラスのメンバーをオーバーライドできるのは、基底クラスのメンバーが virtual または abstract として宣言されている場合だけです。 派生メンバーでは、override キーワードを使用して、そのメソッドが仮想呼び出しに加わることを明示的に示す必要があります。 次にコード例を示します。

public class BaseClass
{
    public virtual void DoWork() { }
    public virtual int WorkProperty
    {
        get { return 0; }
    }
}
public class DerivedClass : BaseClass
{
    public override void DoWork() { }
    public override int WorkProperty
    {
        get { return 0; }
    }
}

フィールドは仮想にできません。仮想にできるのは、メソッド、プロパティ、イベント、インデクサーに限られます。 派生クラスが仮想メンバーをオーバーライドすると、派生クラスのメンバーは、そのクラスのインスタンスが基底クラスのインスタンスとしてアクセスされるときでも呼び出されます。 次にコード例を示します。

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = B;
A.DoWork();  // Also calls the new method.

仮想メソッドとプロパティを使用すると、派生クラスは、基底クラスのメソッドの実装を使用せずに基底クラスを拡張できます。 詳細については、「Override キーワードと New キーワードによるバージョン管理」を参照してください。 1 つまたは一連のメソッドを定義し、その実装を派生クラスに任せるもう 1 つの方法として、インターフェイスがあります。

新しいメンバーで基底クラス メンバーを隠ぺいする

基底クラスのメンバーと同じ名前を持つメンバーを派生クラスに与える場合、new キーワードを使用し、基底クラス メンバーを隠ぺいできます。 new キーワードは、置き換えられるクラス メンバーの戻り値の型の前に配置します。 次にコード例を示します。

public class BaseClass
{
    public void DoWork() { WorkField++; }
    public int WorkField;
    public int WorkProperty
    {
        get { return 0; }
    }
}

public class DerivedClass : BaseClass
{
    public new void DoWork() { WorkField++; }
    public new int WorkField;
    public new int WorkProperty
    {
        get { return 0; }
    }
}

基底クラスのメンバーが隠ぺいされても、派生クラスのインスタンスを基底クラスのインスタンスに型変換することで、クライアント コードからアクセスできます。 次に例を示します。

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.

派生クラスで仮想メンバーのオーバーライドを禁止する

仮想メンバーとそれを最初に宣言したクラスの間で宣言されているクラスの数に関係なく、仮想メンバーは仮想のままになります。 クラス A で仮想メンバーが宣言され、クラス BA から派生し、クラス CB から派生した場合、クラス C では仮想メンバーが継承され、そのメンバーのオーバーライドがクラス B で宣言されたかどうかに関係なく、仮想メンバーをオーバーライドできます。 次にコード例を示します。

public class A
{
    public virtual void DoWork() { }
}
public class B : A
{
    public override void DoWork() { }
}

派生クラスでは、オーバーライドを sealed として宣言することで仮想継承を中止できます。 継承を止めるには、クラス メンバーの宣言で、override キーワードの前に sealed キーワードを指定する必要があります。 次にコード例を示します。

public class C : B
{
    public sealed override void DoWork() { }
}

上の例では、メソッド DoWorkC から派生したあらゆるクラスに対して仮想になりません。 C のインスタンスの場合、型 B や型 A に型変換されたとしてでも、依然として仮想となります。 シール メソッドは、次のコード例に示すように、new キーワードを使用して派生クラスに置き換えることができます。

public class D : C
{
    public new void DoWork() { }
}

この場合、型 D の変数を利用して DDoWork が呼び出されたとき、新しい DoWork が呼び出されます。 型 CBA の変数が D のインスタンスにアクセスする目的で使用される場合、DoWork の呼び出しは仮想継承の規則に準拠し、クラス C での DoWork の実装に呼び出しが転送されます。

派生クラスから基底クラスの仮想メンバーにアクセスする

メソッドやプロパティを置き換えたり、オーバーライドしたりした派生クラスでは、base キーワードを使用して、基底クラスのメソッドやプロパティに引き続きアクセスできます。 次にコード例を示します。

public class Base
{
    public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{
    public override void DoWork()
    {
        //Perform Derived's work here
        //...
        // Call DoWork on base class
        base.DoWork();
    }
}

詳細については、「base」を参照してください。

注意

仮想メンバーの場合、その固有の実装で base を使用して、その仮想メンバーの基底クラス実装を呼び出すことをお勧めします。 基底クラスの動作を実行できるようにすることで、派生クラスは、派生クラスに固有の動作を実装することに集中できます。 基底クラス実装を呼び出さない場合は、基底クラスの動作と互換性のある動作を派生クラスで実現する必要があります。