iT邦幫忙

0

【C#學習筆記】17《介面Interface》

  • 分享至 

  • xImage
  •  

在程式設計中,Interface(介面) 是一種用來 定義規範(Contract) 的機制。它描述「物件必須提供哪些功能」,但通常不實作功能本身。可以將Interface視為一種合約的概念,所有繼承這份合約的人,都必須實作裡面的內容。


【C#學習筆記】16《System.Collections.Generic 常用集合》


介面(Interface)

簡單理解

可以把 Interface 想成一份「工作要求」:
Interface:規定要做哪些事情。
Class:真正去完成這些事情。

Interface: IWeapon
-----------------
- Attack()
- Reload()

Class: Gun
----------
- Attack()  // 實際射擊
- Reload()  // 實際換彈

Class: Bow
----------
- Attack()  // 實際射箭
- Reload()  // 實際裝箭

雖然GunBow的實作方式不同,但都遵守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);
}

使用Interface的優點

降低耦合(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的差異

Interface Abstract Class(抽象類別)
定義「必須有哪些功能」 可以定義功能,也可以提供部分共用實作
不保存物件狀態(通常沒有Field) 可以有Field、Property和Logic
一個類別可以實作多個 Interface 一個類別通常只能繼承一個基底類別
適合描述能力(例如 IDamageableIInteractable 適合描述一群具有共同基礎行為的物件(例如 CharacterWeapon

遊戲開發情境

遊戲中有:
👤 Player
👾 Enemy
📦 Crate
它們都可以受到傷害,因此都必須實作IDamageable
架構

                IDamageable
              +--------------+
              | TakeDamage() |
              +--------------+
                 ▲    ▲    ▲
                 │    │    │
          +------+    │    +------+
          │           │           │
      Player      Enemy       Crate
          ▲           ▲           ▲
          └───────────┴───────────┘
                  Bullet
     只知道呼叫 TakeDamage(),不需要知道物件類型

1.定義Interface

public interface IDamageable
{
    void TakeDamage(int damage);
}

這裡只是規定:「任何繼承IDamageable的物件,都必須實作TakeDamage(int damage)方法。」

2.Class實作Interface

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(耐用性),但仍然可以實作同一個介面。

3.Bullet

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。

使用Interface後,新增一個Boss Class

public class Boss : MonoBehaviour, IDamageable
{
    public int health = 500;

    public void TakeDamage(int damage)
    {
        health -= damage;
    }
}

泛型介面(Generic Interface)

泛型介面(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就像是幫你的程式碼買了一份保險,建立一套一視同仁的「合約」,讓你的程式往「低耦合」的優雅架構邁進了一大步!


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言