C#: 多型

1. 多態性の概要

多態性は、オブジェクト指向プログラミングの中核となる機能の一つであり、単一のインターフェースが、異なるオブジェクトに対して異なる振る舞いを実現する能力を指します。C# では、多態性には次の 2 つの形態があります:

このレッスンでは、オブジェクト指向設計において最も重要な機能である「実行時ポリモーフィズム」に焦点を当てます。


2. 仮想メソッドとオーバーライド

基底クラスはvirtualを使用してメソッドをオーバーライド可能としてマークし、派生クラスはoverrideを使用して新しい実装を提供します。実行時には、どのメソッドが呼び出されるかは、オブジェクトの実際の型(変数の宣言型ではなく)によって決まります。これが動的ディスパッチです。

▶ サンプル

CSHARP
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal makes a sound");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Cat meows");
    }
}

class Program
{
    static void Main()
    {
        Animal a = new Dog();
        a.Speak();

        Animal b = new Cat();
        b.Speak();
    }
}
▶ 試してみよう
TEXT 📖 参照専用
Dog barks
Cat meows

変数はAnimalとして宣言されていますが、実際のオブジェクトはDogCatです。実行時には、オーバーライドされた各バージョンが呼び出されます。これがポリモーフィズムの核心的な効果です。


3. 実用例:統一されたインターフェース、異なる実装

ポリモーフィズムの最も一般的なシナリオは、基底クラスの参照を含む配列やコレクションを使用して、さまざまな派生クラスのオブジェクトを管理し、統一されたインターフェースを呼び出す一方で、各オブジェクトが独自の方法で応答するというものです。

▶ サンプル

CSHARP 📖 参照専用
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal makes a sound");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Cat meows");
    }
}

class Cow : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Cow moos");
    }
}

class Program
{
    static void MakeThemSpeak(Animal[] animals)
    {
        foreach (Animal a in animals)
        {
            a.Speak();
        }
    }

    static void Main()
    {
        Animal[] animals = { new Dog(), new Cat(), new Cow() };
        MakeThemSpeak(animals);
    }
}
論理コード 44 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
Dog barks
Cat meows
Cow moos

MakeThemSpeakメソッドは、Animal型のみに依存しています。どの特定の動物を扱っているかを把握する必要はなく、新しい派生クラスを追加する場合でも、このメソッドの修正は不要です。


4. override と new の違い

派生クラスでキーワードnewを使用すると、基底クラスのメソッドを非公開にできますが、これはポリモーフィズムには関与しません。overrideは動的ディスパッチに関与しますが、newによる非公開化では、宣言された型に基づいてコンパイル時にメソッドが選択されます。

▶ サンプル

CSHARP
using System;

class Base
{
    public virtual void Print()
    {
        Console.WriteLine("Base.Print");
    }
}

class OverrideDerived : Base
{
    public override void Print()
    {
        Console.WriteLine("OverrideDerived.Print");
    }
}

class NewDerived : Base
{
    public new void Print()
    {
        Console.WriteLine("NewDerived.Print");
    }
}

class Program
{
    static void Main()
    {
        Base obj1 = new OverrideDerived();
        obj1.Print();

        Base obj2 = new NewDerived();
        obj2.Print();

        NewDerived obj3 = new NewDerived();
        obj3.Print();
    }
}
▶ 試してみよう
TEXT 📖 参照専用
OverrideDerived.Print
Base.Print
NewDerived.Print
💡 多態的な動作が必要な場合は、常にoverrideを使用してください。newは、特別なケースでのメソッドの隠蔽にのみ使用してください。


5. 抽象メソッドと抽象クラス

abstractメソッドには実装がなく、派生クラスにそのオーバーライドを強制します。抽象メソッドを含むクラスは、それ自体がabstractとして宣言されなければならず、直接インスタンス化することはできません。

▶ サンプル

CSHARP 📖 参照専用
using System;

abstract class Shape
{
    public abstract double Area();
}

class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area()
    {
        return Math.PI * Radius * Radius;
    }
}

class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double Area()
    {
        return Width * Height;
    }
}

class Program
{
    static void PrintArea(Shape shape)
    {
        Console.WriteLine($"Area: {shape.Area():F2}");
    }

    static void Main()
    {
        Shape[] shapes = { new Circle(3), new Rectangle(4, 5) };
        foreach (Shape s in shapes)
        {
            PrintArea(s);
        }
    }
}
論理コード 46 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
Area: 28.27
Area: 20.00

抽象クラスは「それが何であるか」を定義し、派生クラスは「それがどのように機能するか」を決定します。すべての図形は、統一されたArea()インターフェースを通じて面積を計算します。


6. メソッドチェーン:多段階のオーバーライド

virtual/overrideは、継承チェーンに沿ってレイヤーごとに上書きすることができます。派生クラスは、base.を介して親クラスの実装を呼び出し、それをさらに拡張することができます。

▶ サンプル

CSHARP
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal speaks");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        base.Speak();
        Console.WriteLine("Dog barks");
    }
}

class Puppy : Dog
{
    public override void Speak()
    {
        base.Speak();
        Console.WriteLine("Puppy yips");
    }
}

class Program
{
    static void Main()
    {
        Animal a = new Puppy();
        a.Speak();
    }
}
▶ 試してみよう
TEXT 📖 参照専用
Animal speaks
Dog barks
Puppy yips

Puppy.Speak()base.Speak()を呼び出し、それがDog.Speak()をトリガーし、さらにDog.Speak()base.Speak()を呼び出してAnimal.Speak()をトリガーすることで、基底クラスから派生クラスへと続く出力の連鎖が形成される。


7. ToString メソッドのオーバーライド

C# のすべての型は object を継承しており、その ToString() メソッドは virtual です。これをオーバーライドすることで、オブジェクトを出力する際に意味のあるテキストを表示できるようになります。

▶ サンプル

CSHARP
using System;

class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Student(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public override string ToString()
    {
        return $"{Name} (Age: {Age})";
    }
}

class Program
{
    static void Main()
    {
        Student s = new Student("Alice", 20);
        Console.WriteLine(s);
        Console.WriteLine(s.ToString());
    }
}
▶ 試してみよう
TEXT 📖 参照専用
Alice (Age: 20)
Alice (Age: 20)

Console.WriteLineは内部で自動的にToString()を呼び出します。オーバーライドすると、出力はデフォルトの型名からカスタム形式に変更されます。


8. リスコフの置換の原則

リスコフの置換の原則(LSP)は、オブジェクト指向設計の基本原則の一つである。派生クラスのオブジェクトは、プログラムの正しさを損なうことなく、基底クラスのオブジェクトと置き換えられる必要がある。

⚠️ 典型的なLSP違反:派生クラスがメソッドをオーバーライドし、基底クラスで宣言されていない例外をスローしたり、基底クラスの契約に違反する結果を返したりする場合。

▶ サンプル

CSHARP
using System;

class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("Bird flies");
    }
}

class Swallow : Bird
{
    public override void Fly()
    {
        Console.WriteLine("Swallow flies high");
    }
}

class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("Penguin cannot fly");
    }
}

class Program
{
    static void LetFly(Bird bird)
    {
        bird.Fly();
    }

    static void Main()
    {
        LetFly(new Swallow());
        LetFly(new Penguin());
    }
}
▶ 試してみよう
TEXT 📖 参照専用
Swallow flies high

Unhandled Exception: System.NotSupportedException: Penguin cannot fly

PenguinBirdを継承していますが、Flyをオーバーライドして例外をスローしており、これにより基底クラスの「鳥は飛べる」という契約が破られ、LSPに違反しています。 正しい対処法は、継承階層を再設計することです。例えば、「飛ぶ能力」を別のインターフェースとして抽出するなどです。

❓ よくある質問

Q virtualメソッドはオーバーライドせずにそのままにしておいてもよいですか?
A はい。オーバーライドしない場合は、基底クラスのデフォルトの実装が使用されます。
Q overrideメソッドは、さらに上書きすることはできますか?
A はい。overrideメソッドは暗黙的にvirtualのセマンティクスを持ちます。そのため、さらに派生したクラスでも引き続き上書きすることができます。
Q newoverride、どちらが良いですか?
A 多態的な挙動が必要な場合は、overrideを使用してください。 メソッドを隠蔽する必要があり、かつポリモーフィズムが不要であることが確実な場合にのみ、newを使用してください。
Q 抽象クラスとインターフェースの違いは何ですか?
A 抽象クラスにはフィールドや実装コードを含めることができますが、インターフェースは契約のみを定義します。クラスは1つの抽象クラスからのみ継承できますが、複数のインターフェースを実装することは可能です。
Q ToString()を上書きしなかった場合はどうなりますか?
A デフォルトでは、完全修飾型名が返されます。たとえば、Studentと指定すると、Studentが出力されます。

📖 まとめ

📝 練習問題

  1. Vehicleという基底クラスを定義し、そこにvirtual void Move()メソッドを定義する。次に、Carを派生させ、 Move()をオーバーライドするCarおよびBicycleクラスを派生させ、Main内でVehicle[]配列を通じてこれらを一元的に呼び出す
  2. Paymentという抽象クラスを定義し、その中にabstract void Pay(decimal amount)メソッドを定義する。さらに、独自の決済ロジックを実装するCashPaymentおよびCardPaymentクラスを派生させる。
  3. Bookクラスを作成し、ToString()をオーバーライドして書籍のタイトルと価格を出力するようにし、Console.WriteLine(book)の動作を確認する。
  4. overridenewの違いを示すコードを記述してください。基底クラスの参照を使用して、それぞれ2つの派生クラスのオブジェクトを呼び出し、出力の違いを確認してください。
  5. 考えてみてください:もしSquareRectangleを継承しつつ、SetWidthおよびSetHeightをオーバーライドして、辺の長さが常に等しくなるようにした場合、これはリスコフ置換の原則に違反するでしょうか?その理由を説明してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%