⛳
【C#】インターフェースの使い方と落とし穴
使い方と落とし穴シリーズ一覧
一言
- 何かを学習するとすぐ試したくなるが、導入が目的になり「そこ、インターフェースいる?」なんてならぬよう注意 (経験者)
- 学習目的ならともかく。
インターフェースとは
-
実装を持たないシグネチャだけの型
- C#8 以降は既定実装で振る舞いを定義可能
- C#11 以降は静的抽象メンバーで演算子, 静的メソッド, プロパティなども抽象化可能
-
インスタンスフィールドは持てない
-
static
やconst
フィールドは保持できる (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) => /* … */;
}
ジェネリック制約での利用
- 型パラメータ(
T
)に受け付けるものを
特定インターフェース自身、または、特定インターフェースを実装したものに限定する - ジェネリック制約自体の解説:ジェネリック制約(where)の使い方と落とし穴
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
実装によるボクシング
- ボクシング自体の解説:int → objectの裏側、ボクシング,アンボクシングとは
- 値型がインターフェースを通して扱われるとボクシングが発生
✅ 回避策
- 構造体自体の回避
- 制約(
where T : Iinterface
)の導入-
T
が値型として関数を直接呼ぶ限り、ボクシングを回避できる - コンパイラがインターフェース実装に対して仮想ではなく直接の呼び出しを生成する
-
- 直接呼出し、インターフェースへのキャスト時の比較
-
value.DoWork()
は JIT により値型専用呼び出しに最適化され、ボクシングなし - インターフェース参照にキャストすると、値型をヒープにして参照を返す(ボクシングあり)
参考
- Microsoft - Interfaces - define behavior for multiple types
- Microsoft - interface(C# Reference)
- Microsoft - 18 Interfaces 18.6 Interface implementations
- Microsoft - Boxing and Unboxing (C# Programming Guide)
- ++C++ - C# 8.0 の新機能
余談
- よかったらこっちも見てくれよなっ
インターフェース既定実装による菱形継承ライクな状況
Discussion