C#インターフェース入門:使い方・継承・抽象クラスとの違いを初心者向けに徹底解説

はじめに

C#でオブジェクト指向プログラミングを学び始めると、必ず登場する重要な仕組みが「インターフェース」です。

インターフェースは、クラスに対して「この機能を必ず持ってください」と約束させるための仕組みです。C#ではinterfaceキーワードを使って定義し、クラス側でその内容を実装します。

ただ、初心者のうちは次のような疑問を持ちやすいです。

「クラスと何が違うの?」
「抽象クラスとどう使い分けるの?」
「なぜわざわざインターフェースを使う必要があるの?」
「C# interfacesと検索すると出てくるけれど、実際の開発でどう役立つの?」

この記事では、C#のインターフェースについて、基本構文から使い方、継承、複数実装、抽象クラスとの違い、実践的な活用例まで初心者向けにわかりやすく解説します。

1. C#のインターフェースとは?初心者向けにわかりやすく解説

C#のインターフェースとは、クラスが実装すべきメソッドやプロパティなどの「形」を定義する仕組みです。

インターフェース自体は、基本的に処理の中身ではなく「どのような機能を持つべきか」を表します。

たとえば「印刷できるもの」を表すインターフェースを作るなら、次のように書けます。

C#
public interface IPrintable
{
void Print();
}

このIPrintableは、「Printメソッドを持つべき」という約束を表しています。

このインターフェースを実装するクラスは、必ずPrintメソッドを用意しなければなりません。

C#
public class Report : IPrintable
{
public void Print()
{
Console.WriteLine("レポートを印刷します。");
}
}

このように、インターフェースはクラスの振る舞いを統一するために使われます。

1-1. インターフェースは「クラスが守るべき約束」を定義する仕組み

インターフェースを理解するうえで大切なのは、「約束」という考え方です。

たとえば、次のようなインターフェースがあるとします。

C#
public interface IAnimal
{
void Speak();
}

このIAnimalを実装するクラスは、必ずSpeakメソッドを持つ必要があります。

C#
public class Dog : IAnimal
{
public void Speak()
{
Console.WriteLine("ワンワン");
}
}

public class Cat : IAnimal
{
public void Speak()
{
Console.WriteLine("ニャー");
}
}

DogCatも、具体的な鳴き方は違います。しかし、どちらもIAnimalとして扱う場合、「Speakできる」という共通の約束を持っています。

つまりインターフェースは、クラスの具体的な中身ではなく、「外から見たときに何ができるか」を定義する仕組みです。

1-2. interfaceキーワードで定義できるメンバー

C#のインターフェースでは、主に次のようなメンバーを定義できます。

C#
public interface IUser
{
string Name { get; set; }

void Login();

int GetAge();
}

代表的なメンバーは次のとおりです。

メソッドは、クラスに実行すべき処理を要求します。

C#
void Login();

プロパティは、値の取得や設定を要求します。

C#
string Name { get; set; }

イベントも定義できます。

C#
event EventHandler Completed;

インデクサーも定義できます。

C#
string this[int index] { get; }

また、C# 8.0以降ではデフォルト実装を持つメソッドを定義できるようになりました。

C#
public interface ILogger
{
void Log(string message);

void LogError(string message)
{
Log($"Error: {message}");
}
}

ただし、初心者の段階では、まず「インターフェースにはメソッドやプロパティの形を定義する」と理解すれば十分です。

1-3. インターフェースで実装できること・できないこと

インターフェースでは、クラスが持つべき機能の一覧を定義できます。

たとえば次のようなことができます。

C#
public interface IFileService
{
void Save(string text);
string Load();
}

このインターフェースは、「保存できる」「読み込める」という機能を表しています。

一方で、従来のインターフェースでは、通常のインスタンスフィールドを持つことはできません。

C#
public interface IExample
{
// 通常のインスタンスフィールドは定義できない
// private int count;
}

また、コンストラクターも定義できません。

C#
public interface IExample
{
// コンストラクターは定義できない
// public IExample() { }
}

なぜなら、インターフェースはインスタンスを直接作るためのものではないからです。

次のコードはエラーになります。

C#
// エラー
// IFileService service = new IFileService();

インターフェースは単独で使うのではなく、クラスに実装させて使います。

C#
public class TextFileService : IFileService
{
public void Save(string text)
{
Console.WriteLine($"保存: {text}");
}

public string Load()
{
return "読み込んだテキスト";
}
}

1-4. C#でインターフェースが必要になる理由

C#でインターフェースが必要になる理由は、コードを柔軟にするためです。

たとえば、メールを送信するクラスがあるとします。

C#
public class EmailSender
{
public void Send(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}

このクラスを直接使うと、別の送信方法に変更したいときにコードの修正範囲が広がりやすくなります。

そこで、送信機能をインターフェースとして定義します。

C#
public interface IMessageSender
{
void Send(string message);
}

メール送信クラスは、このインターフェースを実装します。

C#
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}

SMS送信クラスも同じインターフェースを実装できます。

C#
public class SmsSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"SMS送信: {message}");
}
}

これにより、呼び出し側は具体的な送信方法を意識せずに済みます。

C#
public class NotificationService
{
private readonly IMessageSender _sender;

public NotificationService(IMessageSender sender)
{
_sender = sender;
}

public void Notify(string message)
{
_sender.Send(message);
}
}

このように、インターフェースを使うと「何を使うか」ではなく「何ができるか」に注目した設計ができます。

2. C#インターフェースの基本的な使い方

C#インターフェースの基本的な使い方は、次の流れで理解するとわかりやすいです。

まずinterfaceでインターフェースを定義します。次に、クラスでそのインターフェースを実装します。そして、インターフェース型の変数として扱います。

順番に見ていきましょう。

2-1. interfaceの基本構文

インターフェースは、次のような構文で定義します。

C#
public interface Iインターフェース名
{
戻り値の型 メソッド名();
}

具体例は次のとおりです。

C#
public interface IWorker
{
void Work();
}

C#では、インターフェース名の先頭にIを付ける命名規則がよく使われます。

たとえば次のような名前です。

C#
IWorker
ILogger
IRepository
IUserService
IDisposable
IEnumerable

これは必須ではありませんが、C#の標準ライブラリでも広く使われている慣習です。初心者のうちは、このルールに従っておくと読みやすいコードになります。

2-2. クラスでインターフェースを実装する方法

クラスでインターフェースを実装するには、クラス名の後ろにコロン:を書き、インターフェース名を指定します。

C#
public class Employee : IWorker
{
public void Work()
{
Console.WriteLine("社員が働きます。");
}
}

重要なのは、インターフェースで定義されたメンバーをすべて実装する必要がある点です。

次のようにWorkメソッドを書かないとコンパイルエラーになります。

C#
public class Employee : IWorker
{
// Workメソッドがないためエラー
}

インターフェースは「このメソッドを必ず実装してください」という契約なので、実装し忘れることはできません。

2-3. メソッド・プロパティを定義するサンプルコード

メソッドとプロパティを持つインターフェースの例を見てみましょう。

C#
public interface IUser
{
int Id { get; }
string Name { get; set; }

void ShowProfile();
}

このIUserを実装するクラスは、IdNameShowProfileを用意する必要があります。

C#
public class Customer : IUser
{
public int Id { get; }
public string Name { get; set; }

public Customer(int id, string name)
{
Id = id;
Name = name;
}

public void ShowProfile()
{
Console.WriteLine($"ID: {Id}, 名前: {Name}");
}
}

使用例は次のとおりです。

C#
Customer customer = new Customer(1, "田中");
customer.ShowProfile();

実行結果は次のようになります。

C#
ID: 1, 名前: 田中

プロパティについては、インターフェース側でgetだけを定義した場合、実装側では読み取り専用として実装できます。

C#
public interface IProduct
{
string Name { get; }
}
C#
public class Product : IProduct
{
public string Name { get; }

public Product(string name)
{
Name = name;
}
}

2-4. インターフェース型の変数として扱う方法

インターフェースの大きな特徴は、実装クラスのオブジェクトをインターフェース型の変数に代入できることです。

C#
IWorker worker = new Employee();
worker.Work();

これは、「EmployeeはIWorkerの約束を守っているので、IWorkerとして扱える」という意味です。

複数のクラスを同じインターフェース型で扱うこともできます。

C#
public class Robot : IWorker
{
public void Work()
{
Console.WriteLine("ロボットが作業します。");
}
}
C#
List<IWorker> workers = new List<IWorker>
{
new Employee(),
new Robot()
};

foreach (IWorker worker in workers)
{
worker.Work();
}

実行結果は次のようになります。

C#
社員が働きます。
ロボットが作業します。

このように、異なるクラスを同じ型として扱える点が、インターフェースの大きなメリットです。

2-5. よくあるコンパイルエラーと解決方法

インターフェースを使い始めた初心者がよく出会うエラーを確認しておきましょう。

まず多いのが、メソッドの実装漏れです。

C#
public interface IPrinter
{
void Print();
}

public class DocumentPrinter : IPrinter
{
// Printメソッドがないためエラー
}

解決方法は、インターフェースで定義されたメンバーをすべて実装することです。

C#
public class DocumentPrinter : IPrinter
{
public void Print()
{
Console.WriteLine("印刷します。");
}
}

次に多いのが、アクセス修飾子の付け忘れです。

C#
public class DocumentPrinter : IPrinter
{
void Print()
{
Console.WriteLine("印刷します。");
}
}

このコードはエラーになります。インターフェースのメンバーを暗黙的に実装する場合、実装メソッドはpublicである必要があります。

C#
public class DocumentPrinter : IPrinter
{
public void Print()
{
Console.WriteLine("印刷します。");
}
}

また、戻り値や引数が一致していない場合もエラーになります。

C#
public interface ICalculator
{
int Add(int x, int y);
}

public class Calculator : ICalculator
{
// 戻り値が違うためエラー
public void Add(int x, int y)
{
}
}

解決するには、インターフェースと完全に同じシグネチャにします。

C#
public class Calculator : ICalculator
{
public int Add(int x, int y)
{
return x + y;
}
}

3. インターフェースを使うメリット

インターフェースを使うメリットは、単に「メソッドをまとめられる」だけではありません。

実際の開発では、保守性、拡張性、テストのしやすさに大きく関わります。

3-1. クラス同士の依存関係を減らせる

インターフェースを使うと、クラス同士の依存関係を弱められます。

たとえば、注文処理クラスが具体的なメール送信クラスに依存している場合を考えます。

C#
public class OrderService
{
private readonly EmailSender _emailSender = new EmailSender();

public void Order()
{
_emailSender.Send("注文が完了しました。");
}
}

このコードでは、OrderServiceEmailSenderに強く依存しています。将来、SMS送信やチャット通知に変更したい場合、OrderServiceの修正が必要になります。

そこで、インターフェースを使います。

C#
public interface INotificationSender
{
void Send(string message);
}
C#
public class EmailSender : INotificationSender
{
public void Send(string message)
{
Console.WriteLine($"メール: {message}");
}
}
C#
public class OrderService
{
private readonly INotificationSender _sender;

public OrderService(INotificationSender sender)
{
_sender = sender;
}

public void Order()
{
_sender.Send("注文が完了しました。");
}
}

これでOrderServiceは、具体的なEmailSenderではなく、INotificationSenderに依存するようになります。

つまり、通知方法を差し替えやすくなります。

3-2. 複数のクラスに共通の機能を持たせられる

インターフェースを使うと、複数のクラスに共通の機能を持たせられます。

たとえば、ファイル、画像、レポートなど、さまざまなものを保存対象にしたい場合を考えます。

C#
public interface ISavable
{
void Save();
}
C#
public class TextDocument : ISavable
{
public void Save()
{
Console.WriteLine("テキストを保存しました。");
}
}

public class ImageFile : ISavable
{
public void Save()
{
Console.WriteLine("画像を保存しました。");
}
}

public class ReportFile : ISavable
{
public void Save()
{
Console.WriteLine("レポートを保存しました。");
}
}

これらのクラスは中身は違いますが、すべてSaveできるという共通点を持っています。

C#
List<ISavable> files = new List<ISavable>
{
new TextDocument(),
new ImageFile(),
new ReportFile()
};

foreach (ISavable file in files)
{
file.Save();
}

インターフェースを使えば、「保存できるもの」という共通の見方で複数のクラスを扱えます。

3-3. ポリモーフィズムを実現できる

ポリモーフィズムとは、同じ呼び出し方でも、実際のオブジェクトによって異なる動作をする仕組みです。

インターフェースは、ポリモーフィズムを実現する代表的な方法です。

C#
public interface IPayment
{
void Pay(int amount);
}
C#
public class CreditCardPayment : IPayment
{
public void Pay(int amount)
{
Console.WriteLine($"{amount}円をクレジットカードで支払いました。");
}
}

public class CashPayment : IPayment
{
public void Pay(int amount)
{
Console.WriteLine($"{amount}円を現金で支払いました。");
}
}

public class QRPayment : IPayment
{
public void Pay(int amount)
{
Console.WriteLine($"{amount}円をQR決済で支払いました。");
}
}

呼び出し側は、支払い方法の詳細を知らなくてもPayを呼び出せます。

C#
public class PaymentService
{
public void ExecutePayment(IPayment payment, int amount)
{
payment.Pay(amount);
}
}
C#
var service = new PaymentService();

service.ExecutePayment(new CreditCardPayment(), 1000);
service.ExecutePayment(new CashPayment(), 2000);
service.ExecutePayment(new QRPayment(), 3000);

同じPayメソッドを呼んでいても、実際の処理はクラスごとに異なります。これがポリモーフィズムです。

3-4. テストしやすいコードを書ける

インターフェースを使うと、単体テストがしやすくなります。

たとえば、外部APIを呼び出すクラスに直接依存していると、テスト時にも本物のAPIを呼んでしまう可能性があります。

そこで、API呼び出しをインターフェース化します。

C#
public interface IUserApiClient
{
string GetUserName(int id);
}

本番用の実装は次のようにします。

C#
public class UserApiClient : IUserApiClient
{
public string GetUserName(int id)
{
// 実際には外部APIを呼び出す
return "本番ユーザー";
}
}

テスト用には、偽物の実装を用意できます。

C#
public class FakeUserApiClient : IUserApiClient
{
public string GetUserName(int id)
{
return "テストユーザー";
}
}

呼び出し側はインターフェースに依存します。

C#
public class UserService
{
private readonly IUserApiClient _apiClient;

public UserService(IUserApiClient apiClient)
{
_apiClient = apiClient;
}

public string GetDisplayName(int id)
{
return _apiClient.GetUserName(id);
}
}

テスト時には、FakeUserApiClientを渡せば外部APIに依存せずテストできます。

C#
var service = new UserService(new FakeUserApiClient());
string name = service.GetDisplayName(1);

Console.WriteLine(name);

3-5. 変更に強い設計にできる

インターフェースを使うと、将来の変更に強い設計ができます。

たとえば、ログ出力をコンソールからファイルに変更したい場合を考えます。

C#
public interface ILogger
{
void Log(string message);
}

コンソール用の実装です。

C#
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}

ファイル用の実装です。

C#
public class FileLogger : ILogger
{
public void Log(string message)
{
// 実際にはファイルに書き込む
Console.WriteLine($"ファイルに出力: {message}");
}
}

呼び出し側は、ILoggerだけに依存します。

C#
public class ApplicationService
{
private readonly ILogger _logger;

public ApplicationService(ILogger logger)
{
_logger = logger;
}

public void Run()
{
_logger.Log("処理を開始しました。");
}
}

これにより、ログの出力先を変更しても、ApplicationServiceのコードを変更せずに済みます。

変更に強いコードとは、影響範囲が小さいコードです。インターフェースは、そのような設計を支える重要な仕組みです。

4. C#インターフェースの継承と複数実装

C#のインターフェースは、クラスと同じように継承できます。また、クラスは複数のインターフェースを実装できます。

これはC#のインターフェースを理解するうえで非常に重要なポイントです。

4-1. インターフェースを継承する基本構文

インターフェースが別のインターフェースを継承する場合も、コロン:を使います。

C#
public interface IBaseService
{
void Start();
}
C#
public interface IAdvancedService : IBaseService
{
void Stop();
}

この場合、IAdvancedServiceStartStopの両方を持つインターフェースになります。

つまり、IAdvancedServiceを実装するクラスは、両方のメソッドを実装する必要があります。

C#
public class AdvancedService : IAdvancedService
{
public void Start()
{
Console.WriteLine("開始します。");
}

public void Stop()
{
Console.WriteLine("停止します。");
}
}

4-2. 継承元インターフェースのメンバーも実装が必要

インターフェースを継承した場合、継承元のメンバーも実装対象になります。

C#
public interface IReadService
{
string Read();
}

public interface IWriteService : IReadService
{
void Write(string text);
}

このIWriteServiceを実装するクラスは、ReadWriteの両方が必要です。

C#
public class FileService : IWriteService
{
public string Read()
{
return "ファイル内容";
}

public void Write(string text)
{
Console.WriteLine($"書き込み: {text}");
}
}

次のようにWriteだけ実装してもエラーになります。

C#
public class FileService : IWriteService
{
public void Write(string text)
{
Console.WriteLine(text);
}

// Readがないためエラー
}

継承したインターフェースを実装するときは、元のインターフェースに何が定義されているかも確認しましょう。

4-3. クラスで複数のインターフェースを実装する方法

C#では、クラスの多重継承はできません。しかし、インターフェースは複数実装できます。

C#
public interface IReadable
{
string Read();
}

public interface IWritable
{
void Write(string text);
}

1つのクラスで両方を実装できます。

C#
public class TextFile : IReadable, IWritable
{
public string Read()
{
return "テキストを読み込みました。";
}

public void Write(string text)
{
Console.WriteLine($"テキストを書き込みました: {text}");
}
}

使う側は、必要な役割に応じて型を変えられます。

C#
TextFile file = new TextFile();

IReadable reader = file;
Console.WriteLine(reader.Read());

IWritable writer = file;
writer.Write("こんにちは");

このように、インターフェースを複数実装することで、1つのクラスに複数の役割を持たせられます。

4-4. 同じ名前のメンバーがある場合の注意点

複数のインターフェースを実装するとき、同じ名前のメンバーが含まれる場合があります。

C#
public interface IPrinter
{
void Execute();
}

public interface IScanner
{
void Execute();
}

この2つのインターフェースは、どちらもExecuteメソッドを持っています。

同じ処理でよければ、1つのメソッドで両方を実装できます。

C#
public class MultiFunctionDevice : IPrinter, IScanner
{
public void Execute()
{
Console.WriteLine("実行します。");
}
}

この場合、IPrinterとして呼んでもIScannerとして呼んでも同じ処理が実行されます。

C#
MultiFunctionDevice device = new MultiFunctionDevice();

IPrinter printer = device;
IScanner scanner = device;

printer.Execute();
scanner.Execute();

しかし、印刷とスキャンで別々の処理をさせたい場合は、明示的インターフェース実装を使います。

4-5. 明示的インターフェース実装の使いどころ

明示的インターフェース実装とは、インターフェース名を指定してメンバーを実装する方法です。

C#
public class MultiFunctionDevice : IPrinter, IScanner
{
void IPrinter.Execute()
{
Console.WriteLine("印刷を実行します。");
}

void IScanner.Execute()
{
Console.WriteLine("スキャンを実行します。");
}
}

この場合、クラスの変数から直接Executeを呼ぶことはできません。

C#
MultiFunctionDevice device = new MultiFunctionDevice();

// device.Execute(); // エラー

インターフェース型に代入して呼び出します。

C#
IPrinter printer = device;
printer.Execute();

IScanner scanner = device;
scanner.Execute();

実行結果は次のようになります。

C#
印刷を実行します。
スキャンを実行します。

明示的インターフェース実装は、次のような場合に便利です。

同じ名前のメンバーを別々の処理にしたい場合。

クラスの通常のAPIとしては公開したくないが、インターフェース経由では使えるようにしたい場合。

複数の標準インターフェースを実装していて名前の衝突を避けたい場合。

初心者のうちは頻繁に使うものではありませんが、複数インターフェース実装で名前が衝突したときに役立つ機能として覚えておきましょう。

5. インターフェースと抽象クラスの違い

C#を学ぶと、インターフェースとよく比較されるものに抽象クラスがあります。

どちらも「直接インスタンス化できない」「派生クラスや実装クラスに処理を任せる」という点では似ています。

しかし、役割は異なります。

5-1. インターフェースと抽象クラスの役割の違い

インターフェースは、「何ができるか」を定義するものです。

C#
public interface IFlyable
{
void Fly();
}

これは「飛べる」という機能を表しています。

一方、抽象クラスは、「共通の性質や処理を持つ基底クラス」として使います。

C#
public abstract class Animal
{
public string Name { get; set; }

public void Sleep()
{
Console.WriteLine("眠ります。");
}

public abstract void Speak();
}

抽象クラスは、共通のフィールド、プロパティ、メソッド、処理を持たせることができます。

たとえば、犬と猫はどちらも動物なので、Animalを継承するのは自然です。

C#
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワンワン");
}
}

一方、鳥や飛行機やドローンはすべて「飛べる」かもしれませんが、同じ親クラスにするのは不自然です。

この場合はインターフェースが向いています。

C#
public class Bird : IFlyable
{
public void Fly()
{
Console.WriteLine("鳥が飛びます。");
}
}

public class Airplane : IFlyable
{
public void Fly()
{
Console.WriteLine("飛行機が飛びます。");
}
}

5-2. フィールド・コンストラクター・アクセス修飾子の違い

抽象クラスは、通常のクラスに近い特徴を持っています。

フィールドを持てます。

C#
public abstract class BaseService
{
protected int count;
}

コンストラクターも持てます。

C#
public abstract class BaseService
{
protected string Name { get; }

protected BaseService(string name)
{
Name = name;
}
}

共通処理も持てます。

C#
public abstract class BaseService
{
public void Start()
{
Console.WriteLine("共通の開始処理");
}

public abstract void Execute();
}

一方、インターフェースは、基本的には契約を定義するものです。通常のインスタンスフィールドやコンストラクターは持てません。

C#
public interface IService
{
void Execute();
}

また、インターフェースのメンバーは、基本的に実装クラスから公開される契約として扱われます。

初心者のうちは、次のように覚えるとわかりやすいです。

抽象クラスは「共通処理を持てる親クラス」。

インターフェースは「実装すべき機能の約束」。

5-3. 多重実装できるかどうかの違い

C#では、クラスは1つのクラスしか継承できません。

C#
public class MyClass : BaseClass
{
}

複数のクラスを同時に継承することはできません。

C#
// C#ではクラスの多重継承はできない
// public class MyClass : BaseClass1, BaseClass2
// {
// }

一方、インターフェースは複数実装できます。

C#
public class MyClass : IReadable, IWritable, IDisposable
{
public string Read()
{
return "読み込み";
}

public void Write(string text)
{
Console.WriteLine(text);
}

public void Dispose()
{
Console.WriteLine("破棄処理");
}
}

この違いは非常に重要です。

あるクラスに複数の役割を持たせたい場合は、インターフェースが向いています。

5-4. 共通処理を持たせたい場合は抽象クラス

複数のクラスで共通する処理がある場合は、抽象クラスが便利です。

たとえば、すべてのレポートに共通する出力処理があるとします。

C#
public abstract class ReportBase
{
public void PrintHeader()
{
Console.WriteLine("=== レポート ===");
}

public abstract void PrintBody();
}

派生クラスでは、本文部分だけを実装します。

C#
public class SalesReport : ReportBase
{
public override void PrintBody()
{
Console.WriteLine("売上レポートの本文");
}
}

使う側は次のようになります。

C#
SalesReport report = new SalesReport();
report.PrintHeader();
report.PrintBody();

共通処理を親クラスにまとめたい場合、抽象クラスは有効です。

ただし、抽象クラスを継承すると、他のクラスを継承できなくなります。設計上、本当に「親子関係」として自然かどうかを考えることが大切です。

5-5. 契約だけを定義したい場合はインターフェース

共通処理ではなく、「この機能を持っていることだけを保証したい」場合はインターフェースが向いています。

C#
public interface IExportable
{
void Export();
}

PDF、CSV、Excelなど、出力形式は異なっても「エクスポートできる」という共通点があります。

C#
public class PdfExporter : IExportable
{
public void Export()
{
Console.WriteLine("PDFに出力します。");
}
}

public class CsvExporter : IExportable
{
public void Export()
{
Console.WriteLine("CSVに出力します。");
}
}

public class ExcelExporter : IExportable
{
public void Export()
{
Console.WriteLine("Excelに出力します。");
}
}

このような場合は、抽象クラスよりもインターフェースのほうが自然です。

C#
public class ExportService
{
public void Export(IExportable exporter)
{
exporter.Export();
}
}

インターフェースは、実装クラスの内部構造に関係なく、共通の操作を提供したいときに役立ちます。

5-6. どちらを使うべきか判断する基準

インターフェースと抽象クラスの使い分けは、次の基準で考えるとわかりやすいです。

「できること」を表したいならインターフェースを使います。

C#
IPrintable
ISavable
ILogger
IRepository

「同じ種類のもの」を表し、共通処理を持たせたいなら抽象クラスを使います。

C#
Animal
ReportBase
ControllerBase
ServiceBase

複数の役割を持たせたい場合は、インターフェースが向いています。

C#
public class CsvFile : IReadable, IWritable, IExportable
{
public string Read()
{
return "CSV読み込み";
}

public void Write(string text)
{
Console.WriteLine("CSV書き込み");
}

public void Export()
{
Console.WriteLine("CSV出力");
}
}

共通の状態や共通処理を持たせたい場合は、抽象クラスが向いています。

C#
public abstract class FileBase
{
public string FilePath { get; }

protected FileBase(string filePath)
{
FilePath = filePath;
}

public void ShowPath()
{
Console.WriteLine(FilePath);
}
}

迷った場合は、まずインターフェースで「外から見た役割」を定義できないか考えるとよいでしょう。そのうえで、共通処理が多い場合に抽象クラスを検討します。

6. 実践でよく使うインターフェースの活用例

インターフェースは文法として理解するだけでなく、実際の開発でどのように使われるかを知ることが大切です。

ここでは、C#の実践でよく登場するインターフェースの活用例を紹介します。

6-1. IEnumerable・IDisposableなど標準インターフェースの例

C#には、標準で多くのインターフェースが用意されています。

代表的なものにIEnumerableがあります。

IEnumerableは、foreachで列挙できることを表すインターフェースです。

C#
List<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};

IEnumerable<string> enumerableNames = names;

foreach (string name in enumerableNames)
{
Console.WriteLine(name);
}

List<T>IEnumerable<T>を実装しているため、foreachで順番に取り出せます。

もう1つよく使うのがIDisposableです。

IDisposableは、使い終わったリソースを解放するためのインターフェースです。

C#
public class FileResource : IDisposable
{
public void Dispose()
{
Console.WriteLine("リソースを解放しました。");
}
}

IDisposableを実装したクラスは、using文と一緒に使われることが多いです。

C#
using (var resource = new FileResource())
{
Console.WriteLine("リソースを使用中");
}

usingブロックを抜けると、自動的にDisposeが呼ばれます。

このように、C#の標準ライブラリでもインターフェースは多く使われています。

6-2. Repositoryパターンでデータ処理を抽象化する例

Repositoryパターンは、データベースへのアクセス処理を抽象化するためによく使われます。

まず、ユーザー情報を表すクラスを作ります。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}

次に、ユーザーを取得するためのインターフェースを定義します。

C#
public interface IUserRepository
{
User? FindById(int id);
void Add(User user);
}

実装クラスを作ります。

C#
public class UserRepository : IUserRepository
{
private readonly List<User> _users = new List<User>();

public User? FindById(int id)
{
return _users.FirstOrDefault(user => user.Id == id);
}

public void Add(User user)
{
_users.Add(user);
}
}

サービスクラスは、具体的なUserRepositoryではなくIUserRepositoryに依存します。

C#
public class UserService
{
private readonly IUserRepository _repository;

public UserService(IUserRepository repository)
{
_repository = repository;
}

public void Register(User user)
{
_repository.Add(user);
}

public User? GetUser(int id)
{
return _repository.FindById(id);
}
}

この設計にすると、データ保存先がメモリ、データベース、外部APIに変わっても、UserServiceの変更を少なくできます。

6-3. Dependency Injectionでインターフェースを使う例

Dependency Injection、略してDIは、依存するオブジェクトを外部から渡す設計方法です。

インターフェースとDIは非常に相性がよいです。

たとえば、ログ出力用インターフェースを定義します。

C#
public interface ILoggerService
{
void Log(string message);
}

実装クラスを作ります。

C#
public class ConsoleLoggerService : ILoggerService
{
public void Log(string message)
{
Console.WriteLine($"ログ: {message}");
}
}

サービスクラスでは、コンストラクターでインターフェースを受け取ります。

C#
public class ProductService
{
private readonly ILoggerService _logger;

public ProductService(ILoggerService logger)
{
_logger = logger;
}

public void CreateProduct()
{
_logger.Log("商品を作成しました。");
}
}

使う側で実装を渡します。

C#
ILoggerService logger = new ConsoleLoggerService();
ProductService service = new ProductService(logger);

service.CreateProduct();

ASP.NET Coreなどのフレームワークでは、DIコンテナを使ってインターフェースと実装クラスを登録することがよくあります。

C#
services.AddScoped<ILoggerService, ConsoleLoggerService>();

このように登録しておくと、必要な場所でILoggerServiceを受け取るだけで、実装クラスが自動的に渡されます。

6-4. 単体テストでモックに差し替える例

インターフェースを使うと、単体テストで本物の処理をモックに差し替えられます。

たとえば、メール送信処理を考えます。

C#
public interface IMailSender
{
void Send(string to, string subject);
}

本番用の実装です。

C#
public class MailSender : IMailSender
{
public void Send(string to, string subject)
{
Console.WriteLine($"{to}にメールを送信しました。件名: {subject}");
}
}

ユーザー登録サービスで使います。

C#
public class AccountService
{
private readonly IMailSender _mailSender;

public AccountService(IMailSender mailSender)
{
_mailSender = mailSender;
}

public void Register(string email)
{
_mailSender.Send(email, "登録完了");
}
}

テスト用の実装を作ります。

C#
public class MockMailSender : IMailSender
{
public bool IsSent { get; private set; }

public void Send(string to, string subject)
{
IsSent = true;
}
}

テスト時には、MockMailSenderを渡します。

C#
var mockMailSender = new MockMailSender();
var accountService = new AccountService(mockMailSender);

accountService.Register("test@example.com");

Console.WriteLine(mockMailSender.IsSent);

実際にメールを送らずに、「メール送信処理が呼ばれたか」を確認できます。

このように、インターフェースはテストしやすい設計に欠かせません。

6-5. 拡張しやすいアプリケーション設計の例

インターフェースを使うと、新しい機能を追加しやすい設計になります。

たとえば、レポート出力機能を考えます。

C#
public interface IReportExporter
{
void Export(string content);
}

PDF出力を実装します。

C#
public class PdfReportExporter : IReportExporter
{
public void Export(string content)
{
Console.WriteLine($"PDF出力: {content}");
}
}

CSV出力を実装します。

C#
public class CsvReportExporter : IReportExporter
{
public void Export(string content)
{
Console.WriteLine($"CSV出力: {content}");
}
}

レポートサービスは、インターフェースに依存します。

C#
public class ReportService
{
private readonly IReportExporter _exporter;

public ReportService(IReportExporter exporter)
{
_exporter = exporter;
}

public void ExportReport()
{
_exporter.Export("売上データ");
}
}

将来、Excel出力を追加したくなった場合も、新しいクラスを追加するだけで対応できます。

C#
public class ExcelReportExporter : IReportExporter
{
public void Export(string content)
{
Console.WriteLine($"Excel出力: {content}");
}
}

既存のReportServiceを変更せずに機能を拡張できるため、保守性が高くなります。

7. C# 8.0以降のインターフェースで知っておきたい機能

C# 8.0以降、インターフェースには新しい機能が追加されました。

従来のインターフェースは「実装を持たない契約」というイメージが強いものでしたが、現在のC#では一部の実装をインターフェース側に持たせることもできます。

ただし、初心者は最初からすべてを理解する必要はありません。まずは基本を押さえたうえで、新しい機能を補足として知っておきましょう。

7-1. デフォルトインターフェースメソッドとは

デフォルトインターフェースメソッドとは、インターフェース内にメソッドの実装を書ける機能です。

C#
public interface ILogger
{
void Log(string message);

void LogWarning(string message)
{
Log($"Warning: {message}");
}
}

この場合、Logは実装クラスで必ず実装する必要があります。

C#
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}

LogWarningはインターフェース側にデフォルト実装があるため、実装クラスで必ずしも書く必要はありません。

C#
ILogger logger = new ConsoleLogger();
logger.LogWarning("注意が必要です。");

デフォルトインターフェースメソッドは、既存のインターフェースに新しいメソッドを追加するときに役立ちます。

従来は、インターフェースにメソッドを追加すると、すべての実装クラスに修正が必要でした。しかし、デフォルト実装があれば、既存クラスを壊さずに機能追加しやすくなります。

7-2. static abstractメンバーとは

C# 11以降では、インターフェースにstatic abstractメンバーを定義できます。

これは、主にジェネリックな数値計算などで使われる高度な機能です。

簡単な例を見てみます。

C#
public interface IAddable<T>
{
static abstract T Add(T left, T right);
}

実装クラスでは、静的メソッドを実装します。

C#
public class MyNumber : IAddable<MyNumber>
{
public int Value { get; }

public MyNumber(int value)
{
Value = value;
}

public static MyNumber Add(MyNumber left, MyNumber right)
{
return new MyNumber(left.Value + right.Value);
}
}

この機能は、通常のアプリケーション開発で初心者がすぐに使うものではありません。

ただし、C#のインターフェースは進化しており、静的なメンバーにも契約を定義できるようになっている、という点は知っておくとよいでしょう。

7-3. 従来のインターフェースとの違い

従来のインターフェースは、基本的に「実装を持たないもの」として理解されていました。

C#
public interface IService
{
void Execute();
}

実装クラスは、必ずExecuteを書く必要があります。

C#
public class Service : IService
{
public void Execute()
{
Console.WriteLine("実行します。");
}
}

一方、C# 8.0以降では、インターフェースにデフォルト実装を書けます。

C#
public interface IService
{
void Execute();

void Start()
{
Console.WriteLine("開始します。");
}
}

ただし、だからといってインターフェースに何でも処理を書いてよいわけではありません。

インターフェースの本来の役割は、あくまで契約を定義することです。デフォルト実装は、互換性維持や共通の補助処理など、目的を絞って使うべきです。

7-4. 初心者がまず理解すべき範囲

初心者がまず理解すべきなのは、次の4つです。

インターフェースはinterfaceキーワードで定義する。

インターフェースは、クラスが実装すべきメソッドやプロパティを定義する。

クラスはインターフェースを実装すると、定義されたメンバーを必ず用意する。

インターフェース型の変数を使うと、実装クラスを柔軟に差し替えられる。

たとえば、次のコードが理解できれば、インターフェースの基本は押さえられています。

C#
public interface IMessageService
{
void Send(string message);
}
C#
public class EmailMessageService : IMessageService
{
public void Send(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}
C#
IMessageService service = new EmailMessageService();
service.Send("こんにちは");

デフォルトインターフェースメソッドやstatic abstractメンバーは、基本がわかってから学べば問題ありません。

8. インターフェース設計でよくある失敗と注意点

インターフェースは便利ですが、使い方を間違えると逆にコードが複雑になります。

ここでは、C#のインターフェース設計でよくある失敗を紹介します。

8-1. 何でもインターフェース化してしまう

初心者がやりがちな失敗の1つが、すべてのクラスに対してインターフェースを作ってしまうことです。

C#
public interface IUserNameFormatter
{
string Format(string firstName, string lastName);
}

もちろん、このようなインターフェースが有効な場合もあります。

しかし、実装クラスが1つしかなく、将来差し替える予定もなく、テストでもモック化する必要がない場合、インターフェースを作る意味が薄いこともあります。

インターフェースを作るたびに、ファイル数や設計上の概念が増えます。

そのため、次のような目的がある場合にインターフェース化を検討するとよいでしょう。

複数の実装を切り替えたい。

外部サービスやデータベースへの依存を抽象化したい。

単体テストでモックに差し替えたい。

上位の処理を具体的な実装から分離したい。

何でもインターフェース化するのではなく、「なぜ必要なのか」を考えることが大切です。

8-2. 役割が大きすぎるインターフェースを作る

インターフェースに多くの役割を詰め込みすぎるのもよくある失敗です。

C#
public interface IUserService
{
void CreateUser();
void UpdateUser();
void DeleteUser();
void SendMail();
void ExportCsv();
void PrintReport();
void ValidatePassword();
}

このようなインターフェースは、役割が広すぎます。

ユーザー管理、メール送信、CSV出力、帳票印刷、パスワード検証が1つにまとまってしまっています。

この場合、実装クラスは不要なメソッドまで実装しなければならない可能性があります。

役割ごとに分けると、使いやすくなります。

C#
public interface IUserRepository
{
void CreateUser();
void UpdateUser();
void DeleteUser();
}
C#
public interface IMailSender
{
void SendMail();
}
C#
public interface ICsvExporter
{
void ExportCsv();
}
C#
public interface IPasswordValidator
{
bool ValidatePassword(string password);
}

小さく分けられたインターフェースは、実装しやすく、テストしやすく、変更にも強くなります。

8-3. 実装クラスに依存した名前を付ける

インターフェース名が特定の実装クラスに依存していると、抽象化の意味が弱くなります。

たとえば、次のような名前です。

C#
public interface ISqlUserRepository
{
User? FindById(int id);
}

この名前だと、SQLを使うことが前提になっています。

将来、APIやファイルから取得する実装に差し替えたい場合、名前が不自然になります。

より抽象的にするなら、次のようにします。

C#
public interface IUserRepository
{
User? FindById(int id);
}

実装クラス側で具体的な技術を表します。

C#
public class SqlUserRepository : IUserRepository
{
public User? FindById(int id)
{
return new User { Id = id, Name = "SQLユーザー" };
}
}
C#
public class ApiUserRepository : IUserRepository
{
public User? FindById(int id)
{
return new User { Id = id, Name = "APIユーザー" };
}
}

インターフェース名は、実装方法ではなく役割を表す名前にしましょう。

8-4. 変更頻度の高いメンバーを詰め込みすぎる

インターフェースに変更頻度の高いメンバーをたくさん入れると、実装クラスへの影響が大きくなります。

たとえば、次のようなインターフェースを考えます。

C#
public interface IReportService
{
void ExportPdf();
void ExportCsv();
void ExportExcel();
void ExportJson();
void ExportXml();
}

将来、出力形式が増えるたびにインターフェースを変更すると、すべての実装クラスに影響します。

このような場合、出力形式ごとにインターフェースを分けることも検討できます。

C#
public interface IPdfExporter
{
void ExportPdf();
}
C#
public interface ICsvExporter
{
void ExportCsv();
}

または、形式を引数で受け取る設計も考えられます。

C#
public interface IReportExporter
{
void Export(string format);
}

インターフェースは変更に強い設計を作るためのものですが、インターフェース自体が頻繁に変わると逆効果になることがあります。

8-5. 命名規則のIプレフィックスを忘れる

C#では、インターフェース名の先頭にIを付けるのが一般的です。

C#
public interface ILogger
{
void Log(string message);
}
C#
public interface IUserRepository
{
User? FindById(int id);
}

この命名規則により、コードを読んだときにインターフェースであることがすぐにわかります。

次のようにIを付けない名前も文法上は可能です。

C#
public interface Logger
{
void Log(string message);
}

しかし、C#の一般的な慣習から外れるため、チーム開発では読みにくくなる可能性があります。

初心者のうちは、次のように覚えておきましょう。

インターフェース名はIから始める。

役割を表す名前にする。

実装クラス名には具体的な技術や方法を含めてもよい。

例は次のとおりです。

C#
public interface INotificationSender
{
void Send(string message);
}

public class EmailNotificationSender : INotificationSender
{
public void Send(string message)
{
Console.WriteLine($"メール通知: {message}");
}
}

public class SmsNotificationSender : INotificationSender
{
public void Send(string message)
{
Console.WriteLine($"SMS通知: {message}");
}
}

9. C#インターフェースの理解を深める練習問題

ここまで学んだ内容を定着させるために、簡単な練習問題を解いてみましょう。

コードを書きながら確認すると、C#インターフェースの理解が深まります。

9-1. 簡単なインターフェースを作成する問題

次の条件を満たすインターフェースとクラスを作成してください。

IGreeterインターフェースを作る。

Greetメソッドを定義する。

JapaneseGreeterクラスで実装する。

解答例は次のとおりです。

C#
public interface IGreeter
{
void Greet();
}
C#
public class JapaneseGreeter : IGreeter
{
public void Greet()
{
Console.WriteLine("こんにちは");
}
}

使用例です。

C#
IGreeter greeter = new JapaneseGreeter();
greeter.Greet();

実行結果は次のようになります。

C#
こんにちは

この問題では、インターフェースの基本構文と実装方法を確認できます。

9-2. 複数クラスで同じインターフェースを実装する問題

次の条件を満たすコードを作成してください。

IShapeインターフェースを作る。

GetAreaメソッドを定義する。

CircleクラスとRectangleクラスで実装する。

解答例です。

C#
public interface IShape
{
double GetArea();
}

円を表すクラスです。

C#
public class Circle : IShape
{
public double Radius { get; }

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

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

長方形を表すクラスです。

C#
public class Rectangle : IShape
{
public double Width { get; }
public double Height { get; }

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

public double GetArea()
{
return Width * Height;
}
}

使用例です。

C#
List<IShape> shapes = new List<IShape>
{
new Circle(3),
new Rectangle(4, 5)
};

foreach (IShape shape in shapes)
{
Console.WriteLine(shape.GetArea());
}

この問題では、複数のクラスを同じインターフェース型で扱う練習ができます。

9-3. インターフェース継承を使う問題

次の条件を満たすコードを作成してください。

IReadableインターフェースを作る。

Readメソッドを定義する。

IWritableインターフェースを作り、IReadableを継承する。

Writeメソッドを定義する。

TextStorageクラスでIWritableを実装する。

解答例です。

C#
public interface IReadable
{
string Read();
}
C#
public interface IWritable : IReadable
{
void Write(string text);
}
C#
public class TextStorage : IWritable
{
private string _text = "";

public string Read()
{
return _text;
}

public void Write(string text)
{
_text = text;
}
}

使用例です。

C#
IWritable storage = new TextStorage();

storage.Write("保存したテキスト");
Console.WriteLine(storage.Read());

実行結果は次のようになります。

C#
保存したテキスト

この問題では、インターフェースの継承と、継承元メンバーの実装が必要であることを確認できます。

9-4. 抽象クラスとの使い分けを考える問題

最後に、インターフェースと抽象クラスの使い分けを考える問題です。

次のケースでは、インターフェースと抽象クラスのどちらが向いているでしょうか。

ケース1は、「PDF、CSV、Excelなど、形式は違うがすべてエクスポートできるものを扱いたい」場合です。

この場合は、インターフェースが向いています。

C#
public interface IExporter
{
void Export();
}

理由は、「エクスポートできる」という機能を表したいからです。

ケース2は、「犬、猫、鳥など、すべて動物として共通の名前や睡眠処理を持たせたい」場合です。

この場合は、抽象クラスが向いています。

C#
public abstract class Animal
{
public string Name { get; set; } = "";

public void Sleep()
{
Console.WriteLine($"{Name}が眠ります。");
}

public abstract void Speak();
}

理由は、「動物」という共通の種類を表し、共通の状態や処理を持たせたいからです。

ケース3は、「メール通知、SMS通知、チャット通知を同じ通知処理として扱いたい」場合です。

この場合は、インターフェースが向いています。

C#
public interface INotifier
{
void Notify(string message);
}

理由は、通知方法は違っても「通知できる」という共通の役割を定義したいからです。

このように、インターフェースと抽象クラスは目的に応じて使い分けます。

まとめ

C#のインターフェースは、クラスが実装すべきメソッドやプロパティを定義する仕組みです。

インターフェースを使うことで、複数のクラスを同じ型として扱えるようになり、依存関係を減らし、変更に強く、テストしやすいコードを書けます。

基本構文は次のとおりです。

C#
public interface IService
{
void Execute();
}

実装クラスは、インターフェースで定義されたメンバーを実装します。

C#
public class Service : IService
{
public void Execute()
{
Console.WriteLine("実行します。");
}
}

インターフェース型として扱うことで、具体的な実装に依存しないコードを書けます。

C#
IService service = new Service();
service.Execute();

また、インターフェースは継承でき、クラスは複数のインターフェースを実装できます。

C#
public class MyClass : IReadable, IWritable
{
public string Read()
{
return "読み込み";
}

public void Write(string text)
{
Console.WriteLine(text);
}
}

抽象クラスとの違いも重要です。共通処理や状態を持たせたい場合は抽象クラス、契約や役割だけを定義したい場合はインターフェースが向いています。

C# interfacesを正しく理解すると、オブジェクト指向設計、Dependency Injection、単体テスト、Repositoryパターンなど、実践的な開発で役立つ考え方が身につきます。

最初は難しく感じるかもしれませんが、まずは「インターフェースはクラスが守るべき約束」と考えると理解しやすくなります。コードを書きながら、少しずつ使いどころを身につけていきましょう。