C#のポリモーフィズム:静的バインディングと動的バインディングを徹底解説
ポリモーフィズム(多態性)とは「多くの形を持つ」という意味です。オブジェクト指向プログラミングパラダイムでは、「1つのインターフェース、複数の機能」として表現されることが多いです。
ポリモーフィズムは静的または動的に分類されます。静的ポリモーフィズムでは、関数への応答がコンパイル時に決定されます。動的ポリモーフィズムでは、実行時に決定されます。
静的ポリモーフィズム(コンパイル時多態性)
コンパイル時に関数とオブジェクトをリンクするメカニズムを早期バインディングまたは静的バインディングと呼びます。C#では静的ポリモーフィズムを実装する2つの技術があります。
- 関数のオーバーロード(Function Overloading)
- 演算子のオーバーロード(Operator Overloading)
演算子オーバーロードについては次章で説明します。
関数のオーバーロード
同じスコープ内で同じ関数名の複数の定義を持てます。定義は引数リストの型と/または数で異なっていなければなりません。戻り値の型のみが異なる関数宣言をオーバーロードすることはできません。
オーバーロードの例
using System;
namespace PolymorphismApplication {
class PrintData {
public void Print(int i) {
Console.WriteLine("Printing int: {0}", i);
}
public void Print(double f) {
Console.WriteLine("Printing float: {0}", f);
}
public void Print(string s) {
Console.WriteLine("Printing string: {0}", s);
}
static void Main(string[] args) {
PrintData p = new PrintData();
p.Print(5); // int版が呼ばれる
p.Print(500.263); // double版が呼ばれる
p.Print("Hello C#"); // string版が呼ばれる
Console.ReadKey();
}
}
}
実行結果:
Printing int: 5
Printing float: 500.263
Printing string: Hello C#
動的ポリモーフィズム(実行時多態性)
C#では、インターフェースの部分的なクラス実装を提供するために使用される抽象クラスを作成できます。実装は派生クラスが継承するときに完了します。抽象クラスには抽象メソッドが含まれ、これは派生クラスで実装されます。派生クラスはより特化した機能を持ちます。
抽象クラスのルール
- 抽象クラスのインスタンスを作成できない
- 抽象メソッドを抽象クラスの外部で宣言できない
- クラスが
sealed宣言されている場合、継承できない。抽象クラスをsealedにすることはできない
抽象クラスの例
using System;
namespace PolymorphismApplication {
abstract class Shape {
public abstract int Area(); // 抽象メソッド(実装なし)
}
class Rectangle : Shape {
private int length;
private int width;
public Rectangle(int a = 0, int b = 0) {
length = a;
width = b;
}
public override int Area() { // 必ずoverrideが必要
Console.WriteLine("Rectangle class area :");
return (width * length);
}
}
class RectangleTester {
static void Main(string[] args) {
Rectangle r = new Rectangle(10, 7);
double a = r.Area();
Console.WriteLine("Area: {0}", a);
Console.ReadKey();
}
}
}
実行結果:
Rectangle class area :
Area: 70
仮想関数(Virtual Functions)
クラスに定義された関数を派生クラスで実装させたい場合、virtual関数を使用します。仮想関数は異なる派生クラスで異なる実装を持て、これらの関数の呼び出しは実行時に決定されます。
動的ポリモーフィズムは抽象クラスと仮想関数によって実装されます。
仮想関数の例
using System;
namespace PolymorphismApplication {
class Shape {
protected int width, height;
public Shape(int a = 0, int b = 0) {
width = a;
height = b;
}
// 仮想メソッド(基底実装あり)
public virtual int Area() {
Console.WriteLine("Parent class area :");
return 0;
}
}
class Rectangle : Shape {
public Rectangle(int a = 0, int b = 0) : base(a, b) { }
public override int Area() {
Console.WriteLine("Rectangle class area :");
return (width * height);
}
}
class Triangle : Shape {
public Triangle(int a = 0, int b = 0) : base(a, b) { }
public override int Area() {
Console.WriteLine("Triangle class area :");
return (width * height / 2);
}
}
class Caller {
public void CallArea(Shape sh) {
int a = sh.Area(); // 実行時に適切なメソッドが呼ばれる
Console.WriteLine("Area: {0}", a);
}
}
class Tester {
static void Main(string[] args) {
Caller c = new Caller();
Rectangle r = new Rectangle(10, 7);
Triangle t = new Triangle(10, 5);
c.CallArea(r); // Rectangle.Area() が呼ばれる
c.CallArea(t); // Triangle.Area() が呼ばれる
Console.ReadKey();
}
}
}
実行結果:
Rectangle class area :
Area: 70
Triangle class area :
Area: 25
静的 vs 動的ポリモーフィズムの比較
| 特徴 | 静的(オーバーロード) | 動的(仮想/抽象) |
|---|---|---|
| 決定タイミング | コンパイル時 | 実行時 |
| 別名 | 早期バインディング | 遅延バインディング |
| 実現方法 | メソッドオーバーロード、演算子オーバーロード | 仮想メソッド、抽象メソッド、インターフェース |
| パフォーマンス | 高速(直接呼び出し) | わずかなオーバーヘッド(vtable検索) |
| 柔軟性 | コンパイル時に既知の型のみ | 実行時に型決定、プラグイン等に対応可能 |
インターフェースによるポリモーフィズム
インターフェースも動的ポリモーフィズムを実現する手段です。
interface IShape {
double Area();
}
class Circle : IShape {
public double Radius { get; set; }
public double Area() => Math.PI * Radius * Radius;
}
class Square : IShape {
public double Side { get; set; }
public double Area() => Side * Side;
}
// 使用
IShape[] shapes = { new Circle { Radius = 5 }, new Square { Side = 4 } };
foreach (var shape in shapes) {
Console.WriteLine($"Area: {shape.Area():F2}");
}
パターンマッチングによる型ベースの分岐(C# 7.0以降)
public static double CalculateArea(object shape) {
return shape switch {
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => t.Base * t.Height / 2,
_ => throw new ArgumentException("Unknown shape")
};
}
ベストプラクティス
- オーバーロードは引数の意味が明確に異なる場合のみ使用し、デフォルト引数やオプション引数で代替可能か検討する
- 継承階層では
virtual/overrideを正しく使用し、newによる隠蔽は避ける - 基底クラスの実装を共有する場合は
virtual、強制実装が必要ならabstractを選択 - インターフェースと抽象クラスの使い分け:「契約」ならインターフェース、「共通実装の共有」なら抽象クラス
sealedでオーバーライドを制御し、意図しない派生を防ぐ- Liskovの置換原則を守る:派生クラスは基底クラスと置換可能でなければならない
C言語
- C# クラスとオブジェクト:堅牢な OOP の基礎
- C# アクセス修飾子を理解する:public、private、protected、internal など
- C# static キーワード:静的変数、メソッド、クラスをマスターする
- C# 抽象クラスと抽象メソッドをマスターする:実践ガイド
- C# 入れ子クラスを理解する:定義、使用法、継承
- C# 部分クラスと部分メソッドを理解する
- C++クラステンプレート完全ガイド:実践的なジェネリックプログラミング
- Java無名インナークラス完全ガイド:定義、構文、実践例
- Java ObjectOutputStream完全ガイド:シリアル化、メソッド、実践例
- Javaジェネリクス完全ガイド:再利用可能で型安全なコードの構築
- Javaファイル操作のマスター:java.ioによる作成・読み取り・書き込み・削除