【C#】インターフェースの使い方と落とし穴

に公開
使い方と落とし穴シリーズ一覧

一言

  • 何かを学習するとすぐ試したくなるが、導入が目的になり「そこ、インターフェースいる?」なんてならぬよう注意 (経験者)
  • 学習目的ならともかく。

インターフェースとは

  • 実装を持たないシグネチャだけの型
    • C#8 以降は既定実装で振る舞いを定義可能
    • C#11 以降は静的抽象メンバーで演算子, 静的メソッド, プロパティなども抽象化可能
  • インスタンスフィールドは持てない
    • staticconst フィールドは保持できる (static は C#8 以降)

インターフェースを導入した設計の例

  • ActIdle クラスは IAct インターフェース実装
  • Character クラスは IAct 型フィールド(ActBehaviour)を保持
  • 実装クラス(ActIdle)を差し替えるだけで振る舞いを変更できる

インターフェースの使い方

基本 (定義と実装)

public interface IDamageable
{
    int HP { get; set; }
    void TakeDamage(int value);
}

public class Enemy : IDamageable
{
    public int HP { get; set; } = 100;
    public void TakeDamage(int value) => HP -= value;
}
  • class, struct, record に適用できる
  • 未実装メンバーがあるとエラー (CS0535)
public interface IDamageable {}
public interface IHealable {}

public class Enemy : IDamageable, IHealable {}
  • C# でクラスの多重継承はできないが、インターフェースの複数実装は可能

通常の実装と明示的実装

  • 名前衝突回避やメンバー露出制御に有用
public interface IComparer
{
    //通常の実装 外部からは ~.Compare(x, y) と呼び出す
    int Compare(object x, object y);
}

class Person : IComparer
{
    //明示的実装 外部からは ((IComparer)~).Compare(x, y) と呼び出す
    int IComparer.Compare(object x, object y) => /* … */;
}

ジェネリック制約での利用

void Heal<T>(T obj) where T : IDamageable {}

既定実装 (C#8~)

  • インターフェース側にデフォルトの挙動を持たせられる
  • 既定実装があるメンバーを実装していなくても CS0535 は生じない
public interface IUpdateable
{
    void Update(float dt) => Console.WriteLine($"Δt: {dt}");
}

static abstract 関数 (C#11~)

  • T 経由で静的関数や演算子を呼び出すことが可能になる
  • これにより、静的機能も多態性の対象とできる
  • 数値計算, 物理演算といった、型を問わない処理などで有用
public interface IVector<T> where T : IVector<T>
{
    static abstract T Zero { get; }

    //演算型を抽象化し、共通コードで扱える
    static abstract T operator +(T l, T r);
}

public readonly record struct Vec2(float X, float Y) : IVector<Vec2>
{
    public static Vec2 Zero => new(0, 0);
    public static Vec2 operator +(Vec2 l, Vec2 r) => new(l.X + r.X, l.Y + r.Y);
}

インターフェースを使用するケース

振る舞いの差し替え (Strategy パターン)

public class Character
{
    public IAct ActBehaviour { get; set; }

    public Character(IAct act) => ActBehaviour = act;
    public void Tick() => ActBehaviour.Act();
}

public interface IAct { void Act(); }
public class ActIdle   : IAct { public void Act() {} }
public class ActEnemy  : IAct { public void Act() {} }

var chara_A = new Character(new ActIdle());
var chara_B = new Character(new ActEnemy());
  • ActBehaviour に任意の実装を注入し、実行時に振る舞いを変更できる

コレクション, アルゴリズムの一般化

  • 例:IEnumerable<T> に依存すれば配列でも List でも LINQ でも OK
static int SumHP(IEnumerable<IDamageable> party) => party.Sum(x => x.HP);

抽象クラスとの比較

インターフェース 抽象クラス
多重実装 可能 不可
状態保持 static, constなら可能 可能
デフォルトの挙動実装 可能(既定実装) 可能
用途イメージ 振る舞いの契約の提示 共通実装と状態の共有

オイラはこんな落とし穴に出会った

インターフェースをいじって大量エラー

  • インターフェースに関数を追加し、実装クラスで大量エラー…
  • いわゆる破壊的変更

回避策

  • 大量の修正が予想される場合、代わりに新たなインターフェースを作る
  • 既定実装で後方互換を保つ
  • 実装する前に設計をよく考える

struct 実装によるボクシング

回避策

  • 構造体自体の回避
  • 制約(where T : Iinterface)の導入
    • T が値型として関数を直接呼ぶ限り、ボクシングを回避できる
    • コンパイラがインターフェース実装に対して仮想ではなく直接の呼び出しを生成する
  • 直接呼出し、インターフェースへのキャスト時の比較
  • value.DoWork() は JIT により値型専用呼び出しに最適化され、ボクシングなし
  • インターフェース参照にキャストすると、値型をヒープにして参照を返す(ボクシングあり)

参考

余談

Discussion