【C#】インターフェイスにメソッドの既定動作(デフォルト実装)を定義する

C# 8.0以前では、インターフェイスはあくまで「契約」を定義するものであり、具体的な処理(実装)を持つことはできませんでした。しかし、C# 8.0での言語仕様の拡張により、インターフェイスのメソッドに「既定の実装(Default Interface Methods)」を持たせることが可能になりました。

この機能を利用することで、既存の実装クラスに変更を加えることなくインターフェイスに新しいメソッドを追加したり、共通のロジックをインターフェイス側に集約したりすることが可能になります。ここでは、会員ランクに応じたポイント還元率の計算ロジックを例に、その実装方法と注意点を解説します。


目次

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

通常、インターフェイスにメソッドを追加すると、そのインターフェイスを実装しているすべてのクラスでコンパイルエラー(「メソッドが実装されていません」)が発生します。

しかし、インターフェイス側でメソッドの本体(ボディ)を定義しておけば、実装クラス側でそのメソッドをオーバーライドしない限り、インターフェイス側の処理が自動的に使用されます。これにより、APIのバージョンアップ時の互換性維持(既存コードを壊さずに機能追加)が容易になります。

実践的なコード例:会員ランク別ポイント計算

以下のコードは、会員インターフェイス(IMember)にポイント計算のロジックを定義しています。RegularMemberクラスはこのメソッドを実装していませんが、インターフェイスの既定の実装を利用して計算を行います。

using System;

namespace MembershipSystem
{
    // 会員インターフェイス
    public interface IMember
    {
        string Name { get; }
        int Rank { get; }

        // C# 8.0以降: インターフェイスにメソッドの実装を記述可能
        // 実装クラス側でオーバーライドされない場合、このロジックが使用されます。
        decimal CalculatePointRate()
        {
            return Rank switch
            {
                1 => 0.01m, // ランク1: 1%
                2 => 0.05m, // ランク2: 5%
                3 => 0.10m, // ランク3: 10%
                _ => 0.0m   // その他 : 0%
            };
        }
    }

    // 通常会員クラス
    // CalculatePointRateを実装していませんが、コンパイルエラーにはなりません。
    public class RegularMember : IMember
    {
        public string Name { get; set; }
        public int Rank { get; set; }
    }

    // 特別会員クラス
    // インターフェイスの既定実装では不足がある場合、独自に実装(再定義)も可能です。
    public class VipMember : IMember
    {
        public string Name { get; set; }
        public int Rank { get; set; }

        // 既定の実装ではなく、独自のロジックを適用
        public decimal CalculatePointRate()
        {
            return 0.20m; // 一律20%
        }
    }

    class Program
    {
        static void Main()
        {
            // 重要: 変数の型をインターフェイス型 (IMember) にする必要があります。
            IMember member1 = new RegularMember { Name = "Tanaka", Rank = 2 };
            IMember member2 = new VipMember { Name = "Suzuki", Rank = 3 };

            // member1はRegularMemberに実装がないため、IMemberの既定実装が呼ばれます。
            Console.WriteLine($"{member1.Name} (Rank {member1.Rank}): {member1.CalculatePointRate():P0}");

            // member2はVipMemberの実装が優先されます。
            Console.WriteLine($"{member2.Name} (Rank {member2.Rank}): {member2.CalculatePointRate():P0}");
        }
    }
}

実行結果

Tanaka (Rank 2): 5%
Suzuki (Rank 3): 20%

技術的なポイントと重要な制約

1. インターフェイス型へのキャストが必要

この機能を利用する上で最も注意すべき点は、**「既定の実装は、変数の型がインターフェイス型の時のみ呼び出せる」**ということです。

もし以下のようにクラス型として変数を宣言した場合、コンパイルエラーになります。

// RegularMemberクラス自体には CalculatePointRate メソッドが存在しないためエラー
RegularMember member = new RegularMember();
// member.CalculatePointRate(); // コンパイルエラー!

// 明示的にインターフェイスへキャストすれば呼び出し可能
((IMember)member).CalculatePointRate(); // OK

これは、既定の実装がクラスのパブリックインターフェイス(クラスのメンバ)として継承されるわけではないためです。

2. 抽象クラスとの違い

インターフェイスに実装を持てるようになると抽象クラス(abstract class)との違いが曖昧に感じられますが、明確な違いがあります。

  • インターフェイス: 状態(フィールドデータ)を持てません。プロパティは定義できますが、値を保持するフィールドは実装クラス側で持つ必要があります。多重継承(複数実装)が可能です。
  • 抽象クラス: 状態(フィールド)を持てます。多重継承はできません。

3. トレイト(Traits)パターンへの応用

この機能により、C#でも他の言語に見られる「トレイト」に近いパターンが可能になりました。複数のインターフェイスに小さな機能(メソッド実装)を持たせ、それらを組み合わせることでクラスの振る舞いを構成するという設計手法が選択肢に入ります。

まとめ

インターフェイスのデフォルト実装は、ライブラリの設計者にとって非常に強力な機能です。リリース済みのインターフェイスに後からメソッドを追加しても、利用側のコードを壊すことなく機能拡張が可能になります。ただし、呼び出しにはインターフェイス型としての扱いが必要になる点に留意して実装してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次