C#: 多型
1. 多態性の概要
多態性は、オブジェクト指向プログラミングの中核となる機能の一つであり、単一のインターフェースが、異なるオブジェクトに対して異なる振る舞いを実現する能力を指します。C# では、多態性には次の 2 つの形態があります:
- 実行時ポリモーフィズム:動的ディスパッチのための
virtual/overrideによって実現される。どのメソッドが呼び出されるかは実行時に決定される。 - コンパイル時のポリモーフィズム:メソッドのオーバーロードによって実現され、コンパイラがパラメータのシグネチャに基づいて呼び出し先を選択する(第14課で解説)。
このレッスンでは、オブジェクト指向設計において最も重要な機能である「実行時ポリモーフィズム」に焦点を当てます。
2. 仮想メソッドとオーバーライド
基底クラスはvirtualを使用してメソッドをオーバーライド可能としてマークし、派生クラスはoverrideを使用して新しい実装を提供します。実行時には、どのメソッドが呼び出されるかは、オブジェクトの実際の型(変数の宣言型ではなく)によって決まります。これが動的ディスパッチです。
▶ サンプル
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();
}
}
Dog barks
Cat meows
変数はAnimalとして宣言されていますが、実際のオブジェクトはDogとCatです。実行時には、オーバーライドされた各バージョンが呼び出されます。これがポリモーフィズムの核心的な効果です。
3. 実用例:統一されたインターフェース、異なる実装
ポリモーフィズムの最も一般的なシナリオは、基底クラスの参照を含む配列やコレクションを使用して、さまざまな派生クラスのオブジェクトを管理し、統一されたインターフェースを呼び出す一方で、各オブジェクトが独自の方法で応答するというものです。
▶ サンプル
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);
}
}
Dog barks
Cat meows
Cow moos
MakeThemSpeakメソッドは、Animal型のみに依存しています。どの特定の動物を扱っているかを把握する必要はなく、新しい派生クラスを追加する場合でも、このメソッドの修正は不要です。
4. override と new の違い
派生クラスでキーワードnewを使用すると、基底クラスのメソッドを非公開にできますが、これはポリモーフィズムには関与しません。overrideは動的ディスパッチに関与しますが、newによる非公開化では、宣言された型に基づいてコンパイル時にメソッドが選択されます。
▶ サンプル
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();
}
}
OverrideDerived.Print
Base.Print
NewDerived.Print
obj1:overrideのオーバーライド — 基底クラスの参照を経由した場合でも、派生クラスのバージョンが呼び出されるobj2:newの隠蔽 — 基底クラスの参照を通じて、基底クラスのバージョンが呼び出される。ポリモーフィズムが破綻するobj3:newの非公開バージョンには、派生クラスの参照を通じてのみアクセスできます
overrideを使用してください。newは、特別なケースでのメソッドの隠蔽にのみ使用してください。
5. 抽象メソッドと抽象クラス
abstractメソッドには実装がなく、派生クラスにそのオーバーライドを強制します。抽象メソッドを含むクラスは、それ自体がabstractとして宣言されなければならず、直接インスタンス化することはできません。
▶ サンプル
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);
}
}
}
Area: 28.27
Area: 20.00
抽象クラスは「それが何であるか」を定義し、派生クラスは「それがどのように機能するか」を決定します。すべての図形は、統一されたArea()インターフェースを通じて面積を計算します。
6. メソッドチェーン:多段階のオーバーライド
virtual/overrideは、継承チェーンに沿ってレイヤーごとに上書きすることができます。派生クラスは、base.を介して親クラスの実装を呼び出し、それをさらに拡張することができます。
▶ サンプル
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();
}
}
Animal speaks
Dog barks
Puppy yips
Puppy.Speak()がbase.Speak()を呼び出し、それがDog.Speak()をトリガーし、さらにDog.Speak()がbase.Speak()を呼び出してAnimal.Speak()をトリガーすることで、基底クラスから派生クラスへと続く出力の連鎖が形成される。
7. ToString メソッドのオーバーライド
C# のすべての型は object を継承しており、その ToString() メソッドは virtual です。これをオーバーライドすることで、オブジェクトを出力する際に意味のあるテキストを表示できるようになります。
▶ サンプル
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());
}
}
Alice (Age: 20)
Alice (Age: 20)
Console.WriteLineは内部で自動的にToString()を呼び出します。オーバーライドすると、出力はデフォルトの型名からカスタム形式に変更されます。
8. リスコフの置換の原則
リスコフの置換の原則(LSP)は、オブジェクト指向設計の基本原則の一つである。派生クラスのオブジェクトは、プログラムの正しさを損なうことなく、基底クラスのオブジェクトと置き換えられる必要がある。
▶ サンプル
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());
}
}
Swallow flies high
Unhandled Exception: System.NotSupportedException: Penguin cannot fly
PenguinはBirdを継承していますが、Flyをオーバーライドして例外をスローしており、これにより基底クラスの「鳥は飛べる」という契約が破られ、LSPに違反しています。 正しい対処法は、継承階層を再設計することです。例えば、「飛ぶ能力」を別のインターフェースとして抽出するなどです。
❓ よくある質問
virtualメソッドはオーバーライドせずにそのままにしておいてもよいですか?overrideメソッドは、さらに上書きすることはできますか?overrideメソッドは暗黙的にvirtualのセマンティクスを持ちます。そのため、さらに派生したクラスでも引き続き上書きすることができます。newとoverride、どちらが良いですか?overrideを使用してください。 メソッドを隠蔽する必要があり、かつポリモーフィズムが不要であることが確実な場合にのみ、newを使用してください。ToString()を上書きしなかった場合はどうなりますか?Studentと指定すると、Studentが出力されます。📖 まとめ
- 多態性には、実行時(
virtual/override)とコンパイル時(オーバーロード)の2つの形態がある。 virtualはオーバーライド可能なメソッドを示し、overrideは派生クラスでの新しい実装を示しますabstractこの手法では、派生クラスにオーバーライドを強制する。抽象クラスはインスタンス化できない。overrideは動的ディスパッチに参加する;newは基底クラスのメソッドを隠蔽し、ポリモーフィズムを破る- 基底クラスの参照を通じて、派生クラスのさまざまなメソッドを呼び出すことは、ポリモーフィズムの典型的な応用例である
- リスコフの置換の原則では、派生クラスが基底クラスと完全に置き換え可能であることが求められる
ToString()のオーバーライドは、日常的なコーディングにおいてポリモーフィズムが最もよく使われる例の一つです
📝 練習問題
Vehicleという基底クラスを定義し、そこにvirtual void Move()メソッドを定義する。次に、Carを派生させ、Move()をオーバーライドするCarおよびBicycleクラスを派生させ、Main内でVehicle[]配列を通じてこれらを一元的に呼び出すPaymentという抽象クラスを定義し、その中にabstract void Pay(decimal amount)メソッドを定義する。さらに、独自の決済ロジックを実装するCashPaymentおよびCardPaymentクラスを派生させる。Bookクラスを作成し、ToString()をオーバーライドして書籍のタイトルと価格を出力するようにし、Console.WriteLine(book)の動作を確認する。overrideとnewの違いを示すコードを記述してください。基底クラスの参照を使用して、それぞれ2つの派生クラスのオブジェクトを呼び出し、出力の違いを確認してください。- 考えてみてください:もし
SquareがRectangleを継承しつつ、SetWidthおよびSetHeightをオーバーライドして、辺の長さが常に等しくなるようにした場合、これはリスコフ置換の原則に違反するでしょうか?その理由を説明してください。