在程式設計中,Interface(介面) 是一種用來 定義規範(Contract) 的機制。它描述「物件必須提供哪些功能」,但通常不實作功能本身。可以將Interface視為一種合約的概念,所有繼承這份合約的人,都必須實作裡面的內容。
【C#學習筆記】16《System.Collections.Generic 常用集合》
可以把 Interface 想成一份「工作要求」:
Interface:規定要做哪些事情。
Class:真正去完成這些事情。
Interface: IWeapon
-----------------
- Attack()
- Reload()
Class: Gun
----------
- Attack() // 實際射擊
- Reload() // 實際換彈
Class: Bow
----------
- Attack() // 實際射箭
- Reload() // 實際裝箭
雖然Gun和Bow的實作方式不同,但都遵守IWeapon的規範。
public interface IDamageable
{
void TakeDamage(int damage);
}
public class Player : IDamageable
{
public void TakeDamage(int damage)
{
Console.WriteLine($"Player took {damage} damage.");
}
}
public class Enemy : IDamageable
{
public void TakeDamage(int damage)
{
Console.WriteLine($"Enemy took {damage} damage.");
}
}
//統一處理
//無論 target 是 Player 還是 Enemy,都可以直接呼叫 TakeDamage()。
void Attack(IDamageable target)
{
target.TakeDamage(10);
}
降低耦合(Loose Coupling):程式依賴介面,而不是特定類別。
方便擴充:新增符合介面的類別時,不需要修改既有程式。
提高可測試性:可以建立假的(Mock)實作來進行測試。
支援多型(Polymorphism):不同類別可以用相同方式操作。
實際使用Interface,製作一個簡單的武器介面(合約),該介面(合約)具有名字與攻擊力,所有的武器都必須實作這兩個功能。
namespace GameSample;
public interface IWeapon
{
string Name { get; set; }
int Attack();
}
public class Sword : IWeapon
{
public string Name { get; set; } = "Sword";
public int Attack() => 10;
}
public interface IShooter : IWeapon
{
int IWeapon.Attack() => Shoot();
int AmmoLimit { get; set; }
int Ammo { get; set; }
int Reload();
int Shoot() => (Ammo-- > 0) ? 10 : 0;
}
public class Pistol : IShooter
{
public string Name { get; set; } = "Pistol";
public int AmmoLimit { get; set; } = 3;
public int Ammo { get; set; } = 3;
public int Reload() => Ammo = AmmoLimit;
}
using GameSample;
IWeapon sword = new Sword();
IShooter pistol = new Pistol();
IWeapon[] weapons = new IWeapon[] { sword, pistol };
foreach (var weapon in weapons)
{
Console.WriteLine($"Weapon: {weapon.Name}, Attack");
for(int i = 0; i < 5; i++)
{
Console.WriteLine($"Attack {i + 1}: {weapon.Attack()}");
}
Console.WriteLine();
}
| Interface | Abstract Class(抽象類別) |
|---|---|
| 定義「必須有哪些功能」 | 可以定義功能,也可以提供部分共用實作 |
| 不保存物件狀態(通常沒有Field) | 可以有Field、Property和Logic |
| 一個類別可以實作多個 Interface | 一個類別通常只能繼承一個基底類別 |
適合描述能力(例如 IDamageable、IInteractable) |
適合描述一群具有共同基礎行為的物件(例如 Character、Weapon) |
遊戲中有:
👤 Player
👾 Enemy
📦 Crate
它們都可以受到傷害,因此都必須實作IDamageable。
架構
IDamageable
+--------------+
| TakeDamage() |
+--------------+
▲ ▲ ▲
│ │ │
+------+ │ +------+
│ │ │
Player Enemy Crate
▲ ▲ ▲
└───────────┴───────────┘
Bullet
只知道呼叫 TakeDamage(),不需要知道物件類型
public interface IDamageable
{
void TakeDamage(int damage);
}
這裡只是規定:「任何繼承IDamageable的物件,都必須實作TakeDamage(int damage)方法。」
using UnityEngine;
public class Player : MonoBehaviour, IDamageable
{
public int health = 100;
public void TakeDamage(int damage)
{
health -= damage;
Debug.Log($"Player HP: {health}");
if (health <= 0)
{
Debug.Log("Player Died");
}
}
}
using UnityEngine;
public class Player : MonoBehaviour, IDamageable
{
public int health = 100;
public void TakeDamage(int damage)
{
health -= damage;
Debug.Log($"Player HP: {health}");
if (health <= 0)
{
Debug.Log("Player Died");
}
}
}
using UnityEngine;
public class Enemy : MonoBehaviour, IDamageable
{
public int health = 50;
public void TakeDamage(int damage)
{
health -= damage;
Debug.Log($"Enemy HP: {health}");
if (health <= 0)
{
Destroy(gameObject);
}
}
}
using UnityEngine;
public class Crate : MonoBehaviour, IDamageable
{
public int durability = 20;
public void TakeDamage(int damage)
{
durability -= damage;
Debug.Log($"Crate Durability: {durability}");
if (durability <= 0)
{
Destroy(gameObject);
}
}
}
//注意,木箱沒有health,而是durability(耐用性),但仍然可以實作同一個介面。
using UnityEngine;
public class Bullet : MonoBehaviour
{
public int damage = 10;
private void OnTriggerEnter2D(Collider2D other)
{
IDamageable target = other.GetComponent<IDamageable>();
if (target != null)
{
target.TakeDamage(damage);
}
Destroy(gameObject);
}
}
重點在這裡:
IDamageable target = other.GetComponent<IDamageable>();
子彈不在乎碰到的是Player、Enemy還是Crate,只要它有實作IDamageable,就直接呼叫:
target.TakeDamage(damage);
如果不用Interface,可能會寫成:
if (other.GetComponent<Player>() != null)
{
other.GetComponent<Player>().TakeDamage(damage);
}
else if (other.GetComponent<Enemy>() != null)
{
other.GetComponent<Enemy>().TakeDamage(damage);
}
else if (other.GetComponent<Crate>() != null)
{
other.GetComponent<Crate>().TakeDamage(damage);
}
每新增一種可受傷物件(例如 Boss、Turret、Barrel),就要一直修改Bullet。
public class Boss : MonoBehaviour, IDamageable
{
public int health = 500;
public void TakeDamage(int damage)
{
health -= damage;
}
}
泛型介面(Generic Interface)就是可以接受型別參數(Type Parameter)的Interface,讓同一份介面能 適用於不同資料型別,而不用重複撰寫。
假設你想做一個儲存資料的介面:
public interface IRepository
{
object GetById(int id);
}
使用時:
//需要手動轉型(Cast)。
//容易在執行時發生型別錯誤。
//編譯器無法幫你檢查型別是否正確。
IRepository repo = ...;
Player player = (Player)repo.GetById(1); // 需要轉型
把介面改成:
public interface IRepository<T>
{
T GetById(int id);
}
//T代表「任意型別」。
實作時:
public class PlayerRepository : IRepository<Player>
{
public Player GetById(int id)
{
return new Player();
}
}
public class EnemyRepository : IRepository<Enemy>
{
public Enemy GetById(int id)
{
return new Enemy();
}
}
使用時
PlayerRepository repo = new PlayerRepository();
Player player = repo.GetById(1); // 不需要轉型
| 沒有泛型 | 有泛型 |
|---|---|
常需要 object 和型別轉換 |
編譯器直接知道正確型別 |
| 容易在執行時發生轉型錯誤 | 型別錯誤通常在編譯時就能發現 |
| 可能需要為不同型別寫多份介面 | 一份介面就能支援多種型別 |
| 可讀性較差 | API 更清楚、易於維護 |
寫程式最怕的就是「牽一髮而動全身」。如果沒有Interface,每次遊戲要出新怪物、新道具,你可能都要跑回去把子彈或角色的程式碼全部重改一遍。導入Interface就像是幫你的程式碼買了一份保險,建立一套一視同仁的「合約」,讓你的程式往「低耦合」的優雅架構邁進了一大步!