インターフェイスのデフォルト実装

Ver. 8.0

C# 8.0 (.NET Core 3.0)で、インターフェイスの制限が緩和されました。 以下のようになります。

これら指して「インターフェイスのデフォルト実装」(default implementations of interfaces)と呼びます。 (1番目の「インターフェイスが関数メンバーの実装を持てる」というのを主目的に検討されたもので、 言葉の意味だけからすると、狭義にはこの1番目の機能こそが「デフォルト実装」です。 ただ、これのついでに実装されたものなので2番目、3番目には具体的な名前がついていません。)

このようにインターフェイスに対する制限を減らすのであれば、 「クラス(特に抽象クラス)との区別が今でも必要なのかどうか」 というような議論もありました。 今、1から文法を決めれるとしても残したい区別は、 「フィールドを持てない代わりに多重継承できる」という点くらいで、 他の差は「歴史的経緯に由来するもの」という側面が強いです。 (インターフェイスでのフィールド定義は、多重継承、特に、ひし形継承との相性が悪く、複雑度のわりにメリットが少ないです。)

歴史的経緯に由来して、以下のような挙動はクラスと揃えることができませんでした。

  • アクセシビリティ未指定のときなど、既定の挙動が違う
  • 派生インターフェイスでのオーバーライドは明示的実装が必須
  • デフォルト実装を持っているメンバーは、派生クラス・派生インターフェイスからは直接呼べない(親へのキャストが必要)

ここでいう「歴史的経緯」は、 既存機能・既存コードへの影響を最小限にとどめるためや、 .NET ランタイム側の修正が簡単な範囲に収めるために残ってしまった差です。

Java 由来で、「インターフェイスのデフォルト メソッド」(default interface method、略して DIM)と呼ばれたりもします。

ランタイム側の修正

インターフェイスのデフォルト実装は C# コンパイラー上のトリックだけでは実装できず、 .NET ランタイム側の対応が必要な機能です。 C# 8.0 以降を使っていても、ターゲットとなるランタイム(TargetFramework)が古いと使えません。 .NET Core 3.0 (かそれと同世代)以降のランタイムである必要があります。 .NET Framework 側では対応予定はない(.NET Core 3.0 と同世代な .NET Framework 4.8 でも未対応)です。

詳しくは以前書いたブログ「RuntimeFeature クラス」で説明しています。

導入の動機

この制限緩和には、以下のような動機ががあります。

  • 既存のインターフェイスにメンバーを追加しても破壊的変更にならない
  • 同様の機能を持っている Android (Java (8以降))や iOS (Swift)との相互運用
  • トレイト的にも使える

メンバー追加による破壊的変更

最大の動機は1番目の「破壊的変更にならない」という部分です。 抽象メンバーは派生クラスでの実装が必須で、実装しなければコンパイル エラーを起こします。 その結果、「後から追加したら派生クラスがコンパイル エラーを起こす」という状態になります。

interface I
{
    void X();
 
    // 後から追加したものとする
    void Y();
}
 
class C : I
{
    // X は実装してある
    public void X() { }
 
    // C が I を実装するコードを書いたころには Y がなかったので OK。
    // Y を追加したことでコンパイル エラーに。
}

この問題を回避するには、これまでは抽象クラスを使うしかありませんでした。 抽象クラスは抽象クラスで、多重継承ができないという別の制限があるので完全な回避策にはなりません。

(あるいは、語尾にExとか2とか3とかが付いた新しいインターフェイスを作ったり、 ユーザーに破壊的変更を受け入れてもらうという手もありますが、 どちらもかなり最終手段です。)

そこで、C# 8.0 ではインターフェイスも実装を持てるようにしました。 Java 8 の同様の機能も同じ動機に基づいています。 機能名が「デフォルト実装」(default = de(脱) + fault(不備))なのもこのためです。 「本来なくてはならない実装がない」という状態(fault)に対して既定動作を与えることで、エラーを回避します。

「規約だけを定める」というクリーンさを犠牲にしてでも、このメリットは大きいです。

この観点で言うと、インターフェイスのデフォルト実装はライブラリ作者のための機能になります。 特に、広く使われているライブラリほど破壊的変更はできないものなので、 一番恩恵を受けるのはcorefx (.NET Core の標準ライブラリ部分)チームだったりします。

(小さい規模だと、自分たちで作ったインターフェイスを自分たちで使うということが多くなりますし、 その場合は別に破壊的変更が気になること自体あまりありません。)

トレイト用途

トレイト的な用途としては、フィールドを持てないなどの制限があるので、恩恵は限定的です。 C# の場合には拡張メソッドでも似たようなことができるので、特に恩恵は少なめです。

「拡張メソッドでもできなくはないけども、virtual な実装方法を取りたい」みたいな場合に使います。

よく例に上がるのが LINQ to Object の Count メソッドです。 IEnumerable<T>(System.Collections.Generic名前空間) に対する Count(含まれている要素数を数える)は、汎用的に書くなら以下のように書くしかありません。

static int Count<T>(IEnumerable<T> source)
{
    var count = 0;
    foreach (var _ in source) ++count;
    return count;
}

ただ、配列やList<T>など、元々長さを持っている型であれば、この foreach は全くの無駄で、できれば元々フィールドとして持っている長さを直接返したいです。 そのため、実際の Count の実装には is 演算子による分岐が挟まっています。 この分岐をするくらいなら、拡張メソッドではなく、インターフェイスのデフォルト実装としてトレイト的に実装する方が素直(virtual なので ICollection 側でオーバーライドできる)です。

実装を持つ関数メンバー

ということで、インターフェイスが実装を持てるようになりました。

using System;
 
interface I
{
    void X();
 
    // 後から追加しても、デフォルト実装を持っているので平気
    void Y() { }
}
 
class A : I
{
    // X だけ実装していればとりあえず大丈夫
    public void X() { }
}
 
class B : I
{
    public void X() { }
 
    // Y も実装。I 越しでもちゃんとこれが呼ばれる。
    public void Y() => Console.WriteLine("B");
}
 
class Program
{
    static void Main() => M(new B());
    static void M(I i) => i.Y();
}
B

ただし、以下の制限は残っています。

主目的(新規メンバー追加での破壊的変更の回避)のためにはインスタンス メンバーだけ実装を持てればいいわけですが、ついでにいろいろと緩和されています。

静的メンバー

静的メンバーも持てるようになりました。 インスタンス メンバーと違って、静的コンストラクターや静的フィールドは持てます。 定数や、演算子入れ子の型も持てます。



using System;
 
interface I
{
    static I() { }
    static int _field;
    static int Method() => ++_field;
    const int Constant = 1;
    public static I operator +(I x) => x;
    class Inner { }
}
 
class Program
{
    static void Main()
    {
        Console.WriteLine(I.Method());
        I.Inner inner;
    }
}

次節で説明する通り、アクセシビリティは特に指定しなければ public です。 明示すれば protectedprivate にすることもできます。

アクセシビリティ

C# 7.3 までは、インターフェイスのメンバーは常に publicvirtual でした。 C# 8.0 からは、明示的に指定することでクラスと同じく、protected などのアクセシビリティを指定できます。

interface I
{
    // 未指定の挙動は今まで通り、public virtual。
    void Public()
    {
        Private();
    }
 
    // 明示することでそれ以外のアクセシビリティを指定できるように。
    // protected なら派生クラス・派生インターフェイスから、
    // private なら自分自身からのみ呼び出し可能。
    protected void Protected() { }
    private void Private() { }
}
 
interface IDerived : I
{
    void M()
    {
        Public();
        Protected();
        // Private(); はダメ
    }
}

ちなみに、省略時の挙動は今まで通り public virtual です。 クラスの場合の省略時は private なので、クラスとは挙動が異なります。

また、後述しますがprotected なメンバーにアクセスできるのは派生インターフェイスからだけです。 クラスの場合、派生(実装)しているクラスであっても protected メンバーは見えません。

既定で仮想

アクセシビリティを明示して protectedinternal などを付けても、protected virtualinternal virtual の意味になります。 仮想呼び出しになる方が既定動作です。 これも、クラスとは既定動作が違います。 C# のクラスは何も指定しなければ仮想関数にはなりません。

private か、あるいは明示的に sealed を指定した時だけ、非仮想になります。

interface I
{
    // 未指定の挙動は今まで通り、public virtual。
    void Public() { }
 
    // これも実際には protected virtual。
    protected void Protected() { }
 
    // private メンバーは派生側から呼ばれないので virtual である必要がない。
    private void Private() { }
 
    // sealed を明示すれば virtual ではなくせる。
    sealed void Sealed() { }
}

ちなみに、派生インターフェイスで基底インターフェイスの virtual なメンバーに sealed を付けることはできません。 一度 virtual になったものはずっと virtual です。

interface IDerived : I
{
    // 基底側で virtual なものを派生側で sealed に変えることはできない。
    // コンパイル エラーになる。
    sealed void I.Protected() { }
}

(多重継承があり得るインターフェイスでは、ある経路で sealed を付けてオーバーライドを禁止しても、別のある経路では sealed が付いていないなど、不整合があるため認められません。)

多重継承

クラスとの最大の差は多重継承ができる点です。

デフォルト実装があっても、 フィールドさえ持たなければ多重継承の実装はそれほど難しいものではないので、 パフォーマンスなどへの悪影響はありません。 (参考: 「インターフェースのデフォルト実装」の導入(前編))

ただ、「別経路で同じメソッドに別実装が与えられている」という場合があって、 そこでの呼び分けが問題になることがあります。

例えば以下のようなコードでは、どの実装を使いたいのか不明瞭なので、コンパイル エラーを起こします。

using System;
 
interface IA
{
    void M() => Console.WriteLine("A.M");
}
 
interface IB : IA
{
    void IA.M() => Console.WriteLine("B.M");
}
 
interface IC : IA
{
    void IA.M() => Console.WriteLine("C.M");
}
 
// IB にも IC にも M の実装があって、どちらを使いたいのか不明瞭(コンパイル エラー)。
class C : IB, IC
{
}

ちなみに、「コンパイルするときには IB にしか M の実装がなかったからコンパイルできたけど、後から ICM を追加した状態のライブラリに差し替えた」というような状況もあり得て、この場合は実行時エラーになります。 AmbiguousImplementationException(System.Runtime 名前空間)が throw されます。

もちろん、自分自身が実装を持てばそれが優先されるので、この「不明瞭」エラーは起きません。

class C : IB, IC
{
    // これなら IB.M でも IC.M でもなく、この M が呼ばれるので明瞭
    public void M() => Console.WriteLine("new implementation");
}

「どうしても IB.M を呼びたい」というように、特定の実装を明示的に呼び出したい場合もあるかと思います。 そういうときのために、base キーワードに特定の型を指定できる機能も追加される予定です。 base(T) というように書きます。

class C : IB, IC
{
    // これなら IB.M を明示的に呼べる。
    public void M() => base(IB).M();
}

元々 C# 8.0 に入る予定で一時的には実装されていましたが、 最終的には 8.0 から外れて、9.0 で取り組みなおすことになりました。

ちなみに、将来的にはこの書き方も認めたいという計画はあります (参考: 「base(T) アクセス」)。

再抽象化

デフォルト実装を持つメンバーを、派生インターフェイス側で再び抽象メンバーに戻すこともできます。 以下のように、明示的実装っぽい書き方の前に abstract 修飾を付けます。

using System;
 
interface A
{
    void M() => Console.WriteLine("default implementation");
}
 
interface B : A
{
    // 実装を持っているメソッドを abstract に変更。
    abstract void A.M();
}
 
// M の実装が必須になる(コンパイル エラー)。
class C : B
{
}

メソッド M が抽象メンバーになったので、インターフェイスBを実装するクラスには M の実装が必須になります。

この機能を再抽象化(re-abstraction)と言います。

その他の制限

主に既存の(C# 7.3 以前の)コードを壊さないようにするためですが、 その他にもいくつか制限が掛かっています。 派生クラスと派生インターフェイスで挙動が変わったりもするので注意が必要です。

まず、派生インターフェイスでは、オーバーライドは常に明示的実装が必要です。

interface I
{
    void M() { }
}
 
interface IDerived : I
{
    // オーバーライドには明示的実装が必須。
    void I.M() { }
 
    // 単に M と書くと、別物になる。
    // 「別物で基底の M を隠したければ new 修飾を付けろ」と警告が出る。
    void M() { }
}

class C : I
{
    // クラスの場合は別にそんな制限はなくて、public な同名のメソッドを書けば I.M として使える。
    public void M() { }
}

基底インターフェイスのメンバーの呼び出しは、 派生側もインターフェイスの場合にはクラス → クラスの時と同じような感覚です。 普通に呼べるし、proteted なものに触れます。

一方、派生側がクラスの場合、デフォルト実装しかない(自分自身はオーバーライドしていない)時にはそのメンバーを直接呼べません。 また、protected なものには触れません。

interface I
{
    void Abstract();
    void Default() { }
 
    protected void Protected() { }
}
 
interface IDerived : I
{
    void M()
    {
        // クラス → クラスの派生と同じ感覚。
        // public, protected メソッドを呼べるし、デフォルト実装の有無も関係なく呼べる。
        Abstract();
        Default();
        Protected();
    }
}
 
class C : I
{
    // デフォルト実装がないものは実装が必須
    public void Abstract() { }
 
    public void M()
    {
        // これは、自身も実装を持っているので呼べる。
        Abstract();
 
        // これはコンパイル エラーになる。
        Default();
 
        // 呼びたければ1段キャストが必要。
        ((I)this).Default();
 
        // protected なものは呼べない。コンパイル エラーに。
        ((I)this).Protected();
    }
}

更新履歴

ブログ