C#のインターフェイス:既定実装・static abstract・明示的実装

C#のインターフェイスを.NET 10で検証し直し。明示的実装、C# 8の既定インターフェイスメソッド、C# 11のstatic abstractメンバーをコンパイル確認した例とエラーコード、抽象クラスとの判断表まで。

2026年11月改訂: この記事は2021年に韓国語で書いたインターフェイスの講座を、日本語の読者向けに書き直したものです。元の2つの例は .NET 10.0.14(C# 14)でコンパイルし直しても出力は同じでしたが、本文の説明には今の基準で誤り・不正確な点が5つありました。特に「同名のメソッドをインターフェイス名なしで実装するとエラーになる」という説明は2021年当時から正しくありませんでした。C# 8 以降のインターフェイスはメソッド本体・static フィールド・private メンバーを持てますし、C# 11 からは static abstract メンバーでジェネリック数値演算もできます。すべてコンパイルして確かめた例とエラーコードを載せています。

C# のインターフェイスを「中身のないメソッドの一覧」と習った人や、最近のコードで static abstract や本体付きのインターフェイスメソッドを見て戸惑った人向けの記事です。先に結論を書くと、インターフェイスは今も「この型はこういうことができる」という契約ですが、2026年のインターフェイスは既定の実装や静的な契約まで持てます。状態(インスタンスフィールド)が要るなら抽象クラス、それ以外はほとんどの場合インターフェイスで足ります。

インターフェイスの基本と2021年版の訂正

インターフェイスは interface キーワードで宣言し、クラスや構造体が : インターフェイス名 で実装します。元記事はこれを「継承」と呼んでいましたが、クラス同士は継承、クラスがインターフェイスを受けるのは実装、と分けたほうが正確です。基底クラスは1つしか持てませんが、インターフェイスはいくつでも実装できます。

元記事の特徴リストを .NET 10 で1つずつ確かめました。

2021年版の説明 .NET 10/C# 14 で確認した結果
メソッド、イベント、インデクサー、プロパティを書ける 正しい。さらに static メンバー、演算子、入れ子の型も書ける
フィールドを含められない 不可なのはインスタンスフィールドだけ(CS0525)。static フィールドは可(C# 8)
すべてのメンバーは既定で public 既定は今も public。ただし private・protected・internal を明示できる(C# 8)
本体のない抽象メンバーだけを持つ C# 8 から既定の実装(本体)を書ける
1つのインターフェイスをクラスで何度も継承できる 同じものを2回書くと CS0528。正しくは「複数のインターフェイスを実装できる」
インターフェイスはクラスの一種 別の種類の型。new できない(CS0144)

実際のエラーメッセージです。

error CS0525: インターフェイスにインスタンス フィールドを含めることはできません
error CS0528: 'IA' は既にインターフェイス リストに存在します

元記事のインターフェイス継承の例(SubInterface : ParentInterface を MyClass が実装)はそのまま動きました。

Interface ParentInterface.myMethod() call!
Inherits SubInterface.myMethod() 0 call!
Inherits SubInterface.myMethod() 1 call!
Inherits SubInterface.myMethod() 2 call!
Inherits SubInterface.myMethod() 3 call!

「すべて漏れなく定義しなければならない」という説明も、本体のないメンバーについては今も正しいです。ただし後で見る既定実装付きのメンバーは実装しなくてもかまいません。

明示的実装が本当に必要な場面

元記事の最初の例は、print() を持つ2つのインターフェイスを void IMyInterfaceA.print() と void IMyInterfaceB.print() で明示的に実装していて、出力も同じでした。

IMyInterfaceA.print() 呼び出し
IMyInterfaceB.print() 呼び出し

一方で元記事は「インターフェイス名なしでメソッドだけ書くと、どちらのものか区別できずエラーになる」と説明していました。実際に試すとエラーにはなりません。

var mc = new MyClass();
IMyInterfaceA a = mc;
IMyInterfaceB b = mc;
a.print(); b.print(); mc.print();

interface IMyInterfaceA { void print(); }
interface IMyInterfaceB { void print(); }
class MyClass : IMyInterfaceA, IMyInterfaceB
{
    public void print() => Console.WriteLine("one public print() serves both");
}
one public print() serves both
one public print() serves both
one public print() serves both

public メソッド1つで2つの契約を同時に満たします。明示的実装を使うのは次のような場面です。

  • インターフェイスごとに違う動作をさせたいとき(元記事の例のように)。
  • 戻り値の型だけが違う同名メンバーを両方提供したいとき。IEnumerable<T> を実装するときに非ジェネリックの IEnumerable.GetEnumerator() を明示的に実装するのが典型です。
  • 公開 API から隠したいメンバーがあるとき。明示的に実装したメンバーはクラス型の変数からは見えません。
var e = new Explicit();
e.print();                    // コンパイルエラー
((IMyInterfaceA)e).print();   // OK
class Explicit : IMyInterfaceA { void IMyInterfaceA.print() => Console.WriteLine("explicit only"); }
error CS1061: 'Explicit' に 'print' の定義が含まれておらず、型 'Explicit' の最初の引数を受け付けるアクセス可能な拡張メソッド 'print' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください

注意点として、構造体の明示的実装メンバーを呼ぶためにインターフェイスへキャストするとボックス化が起きます。割り当て量の実測は構造体の記事にまとめました。

既定インターフェイスメソッド(C# 8)

C# 8 から、インターフェイスのメンバーに本体を書けるようになりました。公開済みのインターフェイスにメソッドを足しても、既存の実装クラスを壊さないための機能です。

ILogger plain = new ConsoleLogger();
ILogger custom = new LoudLogger();
plain.LogError("disk full");
custom.LogError("disk full");
Console.WriteLine($"ILogger.Count = {ILogger.Count}");

interface ILogger
{
    static int Count;                                                  // static フィールドは可
    void Log(string message);
    void LogError(string message) => Log(Stamp("ERROR " + message));   // 既定の実装
    private string Stamp(string s) { Count++; return $"[{Count}] {s}"; } // private メンバー
}
class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
}
class LoudLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
    public void LogError(string message) => Log("!!! " + message.ToUpper());
}
[1] ERROR disk full
!!! DISK FULL
ILogger.Count = 1

ConsoleLogger は LogError を実装していないので既定実装が動き、LoudLogger は同じシグネチャの public メソッドを持っているのでそちらが優先されました。ここで現場でよく引っかかるのが、既定実装はクラスに継承されないという点です。

var log = new ConsoleLogger();
log.LogError("x");   // ConsoleLogger 型の変数からは見えない
error CS1061: 'ConsoleLogger' に 'LogError' の定義が含まれておらず、…

既定実装は ILogger 型として扱うときにしか見えません。クラス型の変数から呼びたいなら、クラスに直接実装するか、インターフェイスに対する拡張メソッドにしたほうが素直です。LangVersion を 7.3 に下げると error CS8370: Feature 'default interface implementation' is not available in C# 7.3. Please use language version 8.0 or greater. となり、C# 8 の機能であることを確認できました。ランタイム側の対応も必要で、Microsoft Learn のドキュメントによると .NET Framework では使えず、.NET Core 3.0 以降が必要です。

static abstract メンバーとジェネリック数値演算(C# 11)

C# 11 から、インターフェイスが静的メンバーや演算子を契約として要求できるようになりました。これでジェネリックメソッドの中で T.Zero や a + b が書けます。

using System.Numerics;

Console.WriteLine(Total<Money>([new(100), new(250), new(5)]));
Console.WriteLine(Total2([1.5, 2.25]));

static T Total<T>(IEnumerable<T> items) where T : IAddable<T>
{
    T sum = T.Zero;
    foreach (var x in items) sum = sum + x;
    return sum;
}
static T Total2<T>(IEnumerable<T> items) where T : INumber<T>
{
    T sum = T.Zero;
    foreach (var x in items) sum += x;
    return sum;
}

interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T a, T b);
}
readonly record struct Money(decimal Amount) : IAddable<Money>
{
    public static Money Zero => new(0);
    public static Money operator +(Money a, Money b) => new(a.Amount + b.Amount);
}
Money { Amount = 355 }
3.75

確かめたことは3つです。

  1. 自作の IAddable<T> に int は渡せません。Total<int>([1, 2, 3]) は error CS0315: 型 'int' はジェネリック型またはメソッド 'Total<T>(IEnumerable<T>)' 内で型パラメーター 'T' として使用できません。'int' から 'IAddable<int>' へのボックス変換がありません。 になりました。既存の型に後からインターフェイスを付けることはできないので、数値型を受けたいなら .NET が実装済みの INumber<T> 系を使います(Total2)。
  2. static abstract メンバーは型パラメーター経由でしか呼べません。IZero.Zero のようにインターフェイス名で呼ぶと error CS8926: 静的な仮想または抽象インターフェイス メンバーには、型パラメーターでのみアクセスできます。
  3. LangVersion=10 でコンパイルすると error CS8703: The modifier 'abstract' is not valid for this item in C# 10.0. Please use language version '11.0' or greater. となり、C# 11 の機能であることを確認しました。

where T : … 制約の基本はジェネリックの記事で扱っています。

インターフェイスか抽象クラスか:判断表

既定実装が書けるようになって「では抽象クラスと何が違うのか」という疑問をよく見かけます。違いは今もはっきりしています。

観点 インターフェイス 抽象クラス
実装/継承できる数 いくつでも 基底は1つだけ
構造体から使えるか 使える 使えない(構造体は継承不可)
インスタンスフィールド(状態) 不可(CS0525) 可
コンストラクター なし あり(: base(…) でつなぐ)
既定の実装 可(C# 8)。ただしインターフェイス型からしか見えない 可。派生クラス型からも見える
静的な契約(演算子、T.Zero) static abstract(C# 11) 不可
後からメンバーを追加 既定実装付きなら互換を保てる 非抽象メンバーの追加なら互換を保てる
アクセス修飾子 既定 public、明示も可 自由
3つの質問からなる判断図。1つ目:共通の型にインスタンスフィールドやコンストラクターの処理が要るか。はいなら抽象クラス。いいえなら2つ目:構造体や、すでに基底クラスを持つクラスが実装するか。はいならインターフェイス。いいえなら3つ目:ジェネリックコードで T の演算子や static なファクトリーが要るか。はいなら static abstract メンバーを持つインターフェイス。いいえならインターフェイス(共通コードは必要に応じて抽象基底クラスへ)。
インターフェイスか抽象クラスかの選び方。インスタンスの状態が要るなら抽象クラス、構造体・既存の基底クラス・ジェネリック数値演算が絡むならインターフェイス。

現場では、契約はインターフェイスとして公開し、実装どうしで共有するコードが多ければ、そのインターフェイスを実装する抽象基底クラスをもう1つ置く組み合わせがよく使われます。使う側はインターフェイスだけに依存するのでテスト用の偽実装を差し込みやすく、実装する側は抽象クラスから共通コードを受け継げます。virtual/override、sealed、コンストラクター内の仮想呼び出しの罠はクラスの継承の記事にまとめています。

確認環境:.NET SDK 10.0.403/ランタイム 10.0.14、C# 14(net10.0 の既定)、macOS(Apple Silicon Mac)。エラーメッセージは日本語 SDK の出力そのままで、LangVersion を下げた確認のメッセージは英語 SDK の出力です。

よくある質問

同じ名前のメソッドを持つインターフェイスを2つ実装するとき、明示的実装は必須?

必須ではありません。public メソッドを1つ書けば両方のインターフェイスを満たし、コンパイルも通ります。2021年版は「インターフェイス名を付けないとエラーになる」と書いていましたが、.NET 10 で確認するとエラーは出ませんでした。インターフェイスごとに動作を変えたいときだけ void IMyInterfaceA.print() のような明示的実装を使います。

既定インターフェイスメソッド(C# 8)はクラス型の変数から呼べる?

呼べません。クラスが自分で実装していない既定メソッドは、インターフェイス型からしか見えません。ConsoleLogger 型の変数で LogError を呼ぶと error CS1061 になり、ILogger 型の変数なら既定実装が動きました。クラスが同じシグネチャの public メソッドを持っていればそちらが優先されます。

static abstract メンバーは何に使う?

ジェネリックコードの中で T.Zero や a + b のように、型そのものの静的メンバーや演算子を使いたいときです(C# 11)。.NET の INumber<T> がこの仕組みで、int や double にそのまま使えます。IZero.Zero のようにインターフェイス名で直接呼ぶと error CS8926 で、型パラメーター T 経由でしかアクセスできません。

  • #C#
  • #インターフェイス
  • #interface
  • #static abstract
  • #.NET

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑