C#: 抽象クラスとインターフェース
1. 抽象クラスと抽象メソッド
抽象クラスはabstract修飾子を用いて宣言され、インスタンス化することはできません。抽象クラスには、抽象メソッドと具象メソッドの両方を含めることができます。抽象メソッドにはメソッド本体がなく、派生クラスでオーバーライドする必要があります。
▶ サンプル
abstract class Shape
{
public string Name { get; set; }
public abstract double Area();
public void Print()
{
Console.WriteLine($"{Name} Area: {Area():F2}");
}
}
class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Name = "Circle";
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 w, double h)
{
Name = "Rectangle";
Width = w;
Height = h;
}
public override double Area()
{
return Width * Height;
}
}
Circle c = new Circle(3);
Rectangle r = new Rectangle(4, 5);
c.Print();
r.Print();
Circle Area: 28.27
Rectangle Area: 20.00
💡 抽象クラスは「半分完成したテンプレート」のようなものです。サブクラスが実装しなければならない部分(抽象メソッド)を指定すると同時に、直接利用できる部分(具象メソッド)も提供します。
2. 抽象クラスと通常のクラス
| フィーチャー | 通常クラス | 抽象クラス |
|---|---|---|
| インスタンス化 | はい | いいえ |
| 抽象メソッド | 含めることができない | 含めることができる |
| 具体的な方法 | はい | はい |
| コンストラクタ | はい | はい |
| フィールド | はい | はい |
| 継承された場合 | 仮想メソッドをオーバーライド可能 | 抽象メソッドをオーバーライド必須 |
| 密封修飾子 | はい | いいえ(矛盾) |
▶ サンプル
abstract class Logger
{
protected string prefix;
public Logger(string prefix)
{
this.prefix = prefix;
}
public abstract void Log(string message);
public void LogWithTimestamp(string message)
{
string ts = DateTime.Now.ToString("HH:mm:ss");
Log($"[{ts}] {message}");
}
}
class FileLogger : Logger
{
public FileLogger() : base("FILE") { }
public override void Log(string message)
{
Console.WriteLine($"{prefix}: {message}");
}
}
FileLogger fl = new FileLogger();
fl.Log("Service started");
fl.LogWithTimestamp("Database connected");
FILE: Service started
FILE: [14:30:00] Database connected
⚠️ 抽象クラスにはコンストラクタを持つことができますが、コンストラクタはabstractであってはなりません。コンストラクタは、サブクラスがインスタンス化される際にbase()を通じて呼び出されます。
3. インターフェースの定義と実装
インターフェースはinterfaceキーワードを使用して定義され、メンバーの契約を表します。インターフェースを実装するクラスは、そのすべてのメンバーの実装を提供しなければなりません。インターフェースのメンバーは暗黙的にpublicであり、アクセス修飾子を指定することはできません。
▶ サンプル
interface IMovable
{
void Move(double dx, double dy);
double Speed { get; set; }
}
interface IDrawable
{
void Draw();
}
class Player : IMovable, IDrawable
{
public double X { get; set; }
public double Y { get; set; }
public double Speed { get; set; }
public Player(double x, double y, double speed)
{
X = x;
Y = y;
Speed = speed;
}
public void Move(double dx, double dy)
{
X += dx * Speed;
Y += dy * Speed;
Console.WriteLine($"Moved to ({X:F1}, {Y:F1})");
}
public void Draw()
{
Console.WriteLine($"Drawing player @ ({X:F1}, {Y:F1})");
}
}
Player p = new Player(0, 0, 2.0);
p.Draw();
p.Move(3, 4);
p.Draw();
Drawing player @ (0.0, 0.0)
Moved to (6.0, 8.0)
Drawing player @ (6.0, 8.0)
📌 インターフェース名は、大文字のIで始まる必要があります(例:IComparable、IDisposableなど)。これは C# の命名規則です。
4. 複数のインターフェースの実装
クラスは、コンマで区切って複数のインターフェースを実装することができます。これは、C#が単一継承という制限を補うために用いる多態性を実現する方法の一つです。
▶ サンプル
interface IWalkable
{
void Walk();
}
interface ISwimmable
{
void Swim();
}
interface IFlyable
{
void Fly();
}
class Duck : IWalkable, ISwimmable, IFlyable
{
public string Name { get; set; }
public Duck(string name)
{
Name = name;
}
public void Walk()
{
Console.WriteLine($"{Name} waddles along");
}
public void Swim()
{
Console.WriteLine($"{Name} swims in the water");
}
public void Fly()
{
Console.WriteLine($"{Name} flaps its wings and takes off");
}
}
Duck d = new Duck("Donald");
d.Walk();
d.Swim();
d.Fly();
IWalkable walker = d;
walker.Walk();
ISwimmable swimmer = d;
swimmer.Swim();
Donald waddles along
Donald swims in the water
Donald flaps its wings and takes off
Donald waddles along
Donald swims in the water
💡 異なるインターフェース型の変数は、そのインターフェースで定義されたメンバーのみを呼び出すことができます。これは、インターフェース分離の原則を反映したものです。
5. インターフェースのデフォルトメソッド (C# 8)
C# 8 以降、インターフェースにはデフォルトの実装を持つメソッドを含めることができるようになりました。これにより、既存のインターフェースを実装しているクラスの互換性を損なうことなく、インターフェースに新しいメンバーを追加できるようになります。
▶ サンプル
interface ILogger
{
void Log(string message);
void LogWarning(string message)
{
Log($"[WARN] {message}");
}
void LogError(string message)
{
Log($"[ERROR] {message}");
}
}
class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
class SimpleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($">> {message}");
}
}
ConsoleLogger cl = new ConsoleLogger();
cl.Log("System started");
cl.LogWarning("Low memory");
cl.LogError("Connection failed");
SimpleLogger sl = new SimpleLogger();
sl.Log("System started");
sl.LogWarning("Low memory");
System started
[WARN] Low memory
[ERROR] Connection failed
>> System started
>> [WARN] Low memory
⚠️ デフォルトメソッドは、インターフェース型を通じて呼び出された場合にのみ利用可能です。実装クラスの型を持つ変数を通じては呼び出すことはできません(ただし、その実装クラスが当該メソッドを再実装している場合は除きます)。
6. 抽象クラスとインターフェースの選択ガイド
| 比較 | 抽象クラス | インターフェース |
|---|---|---|
| 継承/実装 | 単一継承 | 複数実装 |
| メンバーの種類 | 任意 | メソッド、プロパティ、イベント、インデクサー |
| フィールド | はい | いいえ(C# 8以前) |
| コンストラクタ | はい | いいえ |
| アクセス修飾子 | 任意 | デフォルトは public |
| デフォルトの実装 | はい | C# 8 以降 |
| バージョンの互換性 | メンバーの追加は安全 | メンバーの追加により実装が破綻する(C# 8以前) |
| 設計意図 | 「何であるか」(IS-A) | 「何ができるか」(CAN-DO) |
選考基準:
- 階層的な「is-a」関係を表す → 抽象クラス
- クロスタイプの機能や挙動を表す → インターフェース
- コードと状態を共有する必要がある → 抽象クラス
- 多重継承機能が必要 → インターフェース
- 既存のインターフェースに新しいメソッドを追加しつつ、既存のコードを壊さないようにする必要がある → インターフェースのデフォルトメソッド
🔥 簡単なルール:共通の「アイデンティティ」には抽象クラスを、共通の「機能」にはインターフェースを使用する。
7. 明示的なインターフェースの実装
複数のインターフェースに同じ名前のメンバーが存在する場合、またはインターフェースのメンバーを非表示にする必要がある場合は、明示的なインターフェース実装を使用します。構文はreturn_type InterfaceName.MemberNameです。明示的に実装されたメンバーにはアクセス修飾子を指定できず、インターフェース型を通じてのみ呼び出すことができます。
▶ サンプル
interface IEnglish
{
void Speak();
}
interface IFrench
{
void Speak();
}
class BilingualPerson : IEnglish, IFrench
{
void IEnglish.Speak()
{
Console.WriteLine("Hello!");
}
void IFrench.Speak()
{
Console.WriteLine("Bonjour!");
}
public void Speak()
{
Console.WriteLine("Hi!");
}
}
BilingualPerson p = new BilingualPerson();
p.Speak();
IEnglish eng = p;
eng.Speak();
IFrench fr = p;
fr.Speak();
Hi!
Hello!
Bonjour!
📌 明示的に実装されたインターフェースのメンバーは、クラスのインスタンスからは参照できません。これらを呼び出すには、対応するインターフェース型へのキャストを行う必要があります。これは制限であると同時に、カプセル化の仕組みでもあります。
8. IComparable<T>の実践
IComparable<T>は、.NET で最も一般的に使用されるインターフェースの一つであり、型の自然なソート順序を定義するために使用されます。Array.SortとList<T>.Sort`の両方が、このインターフェースに依存しています。
▶ サンプル
class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public Student(string name, int score)
{
Name = name;
Score = score;
}
public int CompareTo(Student other)
{
if (other == null) return 1;
return Score.CompareTo(other.Score);
}
public override string ToString()
{
return $"{Name}({Score})";
}
}
Student[] students = new Student[]
{
new Student("Alice", 85),
new Student("Bob", 92),
new Student("Charlie", 78),
new Student("Diana", 95),
new Student("Eve", 88)
};
Console.WriteLine("Before sorting:");
foreach (var s in students)
Console.WriteLine(s);
Array.Sort(students);
Console.WriteLine("After sorting (by score ascending):");
foreach (var s in students)
Console.WriteLine(s);
Before sorting:
Alice(85)
Bob(92)
Charlie(78)
Diana(95)
Eve(88)
After sorting (by score ascending):
Charlie(78)
Alice(85)
Eve(88)
Bob(92)
Diana(95)
💻 CompareToの戻り値の規則:負の値は現在のオブジェクトが相手より前であることを、0は等しいことを、正の値は現在のオブジェクトが相手より後であることを意味します。 IComparable<T>を使用すると、カスタム型でもArray.SortやList<T>.Sort`、その他のソート機能を直接利用できます。
❓ よくある質問
📖 まとめ
abstractクラスはインスタンス化できず、抽象メソッドと具体メソッドの両方を含むことができる- 抽象メソッドには本体がありません。派生クラスは、これらを
overrideでオーバーライドする必要があります。 - インターフェースは契約を定義する。実装クラスは、すべてのメンバに対して実装を提供しなければならない。
- クラスは複数のインターフェースを実装することができ、単一継承の制限を補うことができる
- C# 8 のインターフェースは、下位互換性を確保するためにデフォルトメソッドをサポートしています
- 抽象クラスは「それが何であるか」を表し、インターフェースは「それが何ができるか」を表す
- 明示的なインターフェースの実装は、命名上の競合を解決し、そのインターフェース型を通じてのみ呼び出すことができます
IComparable<T>は自然な順序を定義し、Sortの手法と連携して機能します
📝 練習問題
- 抽象メソッド
Start()と具象メソッドStop()を持つ抽象クラスVehicleを定義し、それを実装するサブクラスCarとBikeを作成する - メソッド
string Serialize()を持つインターフェースISerializableと、メソッドvoid Deserialize(string data)を持つインターフェースIDeserializableを定義し、両方のインターフェースを実装するクラスを作成する IPlayableとIRecordableという2つのインターフェースを定義し、両方にvoid Pause()メソッドを持たせ、明示的なインターフェース実装を用いてこれらを区別するIComparable<Product>を実装し、価格順に並べ替えるProductクラスを作成し、Array.Sortを使用してテストを行う- 既存の
IPrintableインターフェースに、デフォルトの実装を持つvoid PrintHeader(string title)メソッドを追加し、既存の実装クラスが影響を受けないことを確認する