C#: 抽象クラスとインターフェース

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

抽象クラスはabstract修飾子を用いて宣言され、インスタンス化することはできません。抽象クラスには、抽象メソッドと具象メソッドの両方を含めることができます。抽象メソッドにはメソッド本体がなく、派生クラスでオーバーライドする必要があります。

▶ サンプル

CSHARP 📖 参照専用
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();
論理コード 41 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
Circle Area: 28.27
Rectangle Area: 20.00

💡 抽象クラスは「半分完成したテンプレート」のようなものです。サブクラスが実装しなければならない部分(抽象メソッド)を指定すると同時に、直接利用できる部分(具象メソッド)も提供します。


2. 抽象クラスと通常のクラス

フィーチャー 通常クラス 抽象クラス
インスタンス化 はい いいえ
抽象メソッド 含めることができない 含めることができる
具体的な方法 はい はい
コンストラクタ はい はい
フィールド はい はい
継承された場合 仮想メソッドをオーバーライド可能 抽象メソッドをオーバーライド必須
密封修飾子 はい いいえ(矛盾)

▶ サンプル

CSHARP
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");
▶ 試してみよう
TEXT 📖 参照専用
FILE: Service started
FILE: [14:30:00] Database connected

⚠️ 抽象クラスにはコンストラクタを持つことができますが、コンストラクタはabstractであってはなりません。コンストラクタは、サブクラスがインスタンス化される際にbase()を通じて呼び出されます。


3. インターフェースの定義と実装

インターフェースはinterfaceキーワードを使用して定義され、メンバーの契約を表します。インターフェースを実装するクラスは、そのすべてのメンバーの実装を提供しなければなりません。インターフェースのメンバーは暗黙的にpublicであり、アクセス修飾子を指定することはできません。

▶ サンプル

CSHARP
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();
▶ 試してみよう
TEXT 📖 参照専用
Drawing player @ (0.0, 0.0)
Moved to (6.0, 8.0)
Drawing player @ (6.0, 8.0)

📌 インターフェース名は、大文字のIで始まる必要があります(例:IComparableIDisposableなど)。これは C# の命名規則です。


4. 複数のインターフェースの実装

クラスは、コンマで区切って複数のインターフェースを実装することができます。これは、C#が単一継承という制限を補うために用いる多態性を実現する方法の一つです。

▶ サンプル

CSHARP
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();
▶ 試してみよう
TEXT 📖 参照専用
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 以降、インターフェースにはデフォルトの実装を持つメソッドを含めることができるようになりました。これにより、既存のインターフェースを実装しているクラスの互換性を損なうことなく、インターフェースに新しいメンバーを追加できるようになります。

▶ サンプル

CSHARP
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");
▶ 試してみよう
TEXT 📖 参照専用
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)

選考基準:

🔥 簡単なルール:共通の「アイデンティティ」には抽象クラスを、共通の「機能」にはインターフェースを使用する。


7. 明示的なインターフェースの実装

複数のインターフェースに同じ名前のメンバーが存在する場合、またはインターフェースのメンバーを非表示にする必要がある場合は、明示的なインターフェース実装を使用します。構文はreturn_type InterfaceName.MemberNameです。明示的に実装されたメンバーにはアクセス修飾子を指定できず、インターフェース型を通じてのみ呼び出すことができます。

▶ サンプル

CSHARP
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();
▶ 試してみよう
TEXT 📖 参照専用
Hi!
Hello!
Bonjour!

📌 明示的に実装されたインターフェースのメンバーは、クラスのインスタンスからは参照できません。これらを呼び出すには、対応するインターフェース型へのキャストを行う必要があります。これは制限であると同時に、カプセル化の仕組みでもあります。


8. IComparable<T>の実践

IComparable<T>は、.NET で最も一般的に使用されるインターフェースの一つであり、型の自然なソート順序を定義するために使用されます。Array.SortList<T>.Sort`の両方が、このインターフェースに依存しています。

▶ サンプル

CSHARP
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);
▶ 試してみよう
TEXT 📖 参照専用
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.SortList<T>.Sort`、その他のソート機能を直接利用できます。

❓ よくある質問

Q 抽象クラスはインターフェースを実装できますか?
A はい。抽象クラスはインターフェースを実装でき、そのメンバーを具体的に実装するか、抽象のままにするかを選択できます。
Q インターフェースは別のインターフェースから継承できますか?
A はい。インターフェースは複数のインターフェースから継承することができ、実装クラスは継承チェーン上のすべてのメンバーを実装する必要があります。
Q 明示的なインターフェースの実装に public 修飾子を付けることはできますか?
A いいえ。明示的なインターフェースの実装には、いかなるアクセス修飾子も付けることはできません。これらはインターフェース型を通じてのみアクセス可能です。
Q インターフェースのデフォルトメソッドと抽象クラスの仮想メソッドの違いは何ですか?
A インターフェースのデフォルトメソッドはインスタンスフィールドにアクセスできませんが、抽象クラスの仮想メソッドはアクセスできます。また、デフォルトメソッドはインターフェースを通じて呼び出されますが、仮想メソッドはクラスのインスタンスを通じて呼び出されます。

📖 まとめ

📝 練習問題

  1. 抽象メソッドStart()と具象メソッドStop()を持つ抽象クラスVehicleを定義し、それを実装するサブクラスCarBikeを作成する
  2. メソッドstring Serialize()を持つインターフェースISerializableと、メソッドvoid Deserialize(string data)を持つインターフェースIDeserializableを定義し、両方のインターフェースを実装するクラスを作成する
  3. IPlayableIRecordableという2つのインターフェースを定義し、両方にvoid Pause()メソッドを持たせ、明示的なインターフェース実装を用いてこれらを区別する
  4. IComparable<Product>を実装し、価格順に並べ替えるProductクラスを作成し、Array.Sortを使用してテストを行う
  5. 既存のIPrintableインターフェースに、デフォルトの実装を持つvoid PrintHeader(string title)メソッドを追加し、既存の実装クラスが影響を受けないことを確認する
Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%