iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0

前言

昨天我們學習了繼承與多型,透過 virtual 與 override 的方式,讓不同的類別能夠展現不同的行為。今天我們要進一步探討兩個 OOP 的進階概念:介面(Interface) 與抽象類別(Abstract Class)。這兩者常常被初學者搞混,但其實他們解決的問題不同:

  • 抽象類別:適合用來提供一個「部分實作 + 規範」的基礎。
  • 介面:適合用來定義一組「行為契約」,讓不同類別都能遵循而不必有共同祖先。

介面 (Interfaces) — 為多種類型定義共同行為

什麼是介面?

介面 (Interface) 定義了一組相關功能,但不包含實作。類別 (class) 或結構 (struct) 可以實作 (implement) 介面,並提供這些功能的具體實作。介面可包含:實例方法 (methods)、屬性 (properties)、事件 (events)、索引子 (indexers)。但介面不可包含:實例欄位 (fields)、實例建構子 (constructors)、解構子 (finalizers)。

為什麼需要介面?

C# 不支援多重繼承 (multiple inheritance),所以使用介面可以讓類別同時具備多種行為。
結構 (struct) 不能繼承其他 struct 或 class,但可以實作介面,所以這樣也能「模擬繼承」。
達到程式設計上的一致性,例如:任何實作 IEquatable<T> 的類別,都必須實作 Equals 方法。

定義與命名規則

使用 interface 關鍵字定義。
命名慣例:通常以大寫 I 開頭,例如 IEquatable<T>

定義介面

interface IEquatable<T>
{
    bool Equals(T obj);
}

實作介面

public class Car : IEquatable<Car>
{
    public string? Make { get; set; }
    public string? Model { get; set; }
    public string? Year { get; set; }

    // 必須實作 IEquatable<T> 的 Equals 方法
    public bool Equals(Car? car)
    {
        return (this.Make, this.Model, this.Year) ==
               (car?.Make, car?.Model, car?.Year);
    }
}

介面實作方式

介面實作方式主要分為兩種:隱含實作 (Implicit Implementation)以及明確實作 (Explicit Implementation)。其中隱含實作 (Implicit Implementation)介面方法對應到 public、非 static 的類別方法,名稱與簽章相同,最常見的做法如下:

public class Logger : ILoggable
{
    public void Log(string message)   // public + 符合簽章
    {
        Console.WriteLine($"Log: {message}");
    }
}

而在明確實作 (Explicit Implementation),使用介面名稱限定,方法前沒有存取修飾詞,僅能透過介面存取,常用於介面簽章中有 internalprivate 類型時。

internal class InternalConfiguration
{
    public string Setting { get; set; } = "";
}

internal interface IConfigurable
{
    void Configure(InternalConfiguration config);
}

public class ServiceImplementation : IConfigurable
{
    // 明確實作,方法前不加 public
    void IConfigurable.Configure(InternalConfiguration config)
    {
        Console.WriteLine($"Configured with: {config.Setting}");
    }
}

而其呼叫方式如下:

ServiceImplementation service = new ServiceImplementation();
// service.Configure(...) ❌ 不能直接呼叫
((IConfigurable)service).Configure(new InternalConfiguration { Setting = "ABC" }); ✅

介面繼承

介面可以繼承一個或多個介面,類別實作衍生介面時,必須同時實作所有基底介面的成員,範例如下:

interface IA
{
    void A();
}

interface IB : IA
{
    void B();
}

public class MyClass : IB
{
    public void A() => Console.WriteLine("A");
    public void B() => Console.WriteLine("B");
}

abstract 修飾詞

abstract 用於類別或成員(方法、屬性、索引子、事件),表示該成員 沒有完整實作,必須由衍生類別 (derived class) 來實作。
抽象類別 (abstract class):

  • 不能被直接建立物件 (new)。
  • 可包含抽象成員與已實作成員。
  • 常作為「基底類別」(blueprint) 提供共用功能。
  • 可以有欄位、已實作的方法、屬性、建構子
  • 可以有 abstract 方法或屬性,衍生類別必須 override
  • 不能與 sealed 同時使用 (因為 sealed 代表不能被繼承)

Example 1 - Abstract class with mixed members(具有混合成員的抽象類)

下面的範例示範了一個包含已實作方法和抽象成員的抽象類別:

namespace LanguageKeywords;

public abstract class Vehicle
{
    protected string _brand;
    
    // Constructor - implemented method in abstract class
    public Vehicle(string brand) => _brand = brand;
    
    // Implemented method - provides functionality that all vehicles share
    public string GetInfo() => $"This is a {_brand} vehicle.";
    
    // Another implemented method
    public virtual void StartEngine() => Console.WriteLine($"{_brand} engine is starting...");
    
    // Abstract method - must be implemented by derived classes
    public abstract void Move();
    
    // Abstract property - must be implemented by derived classes  
    public abstract int MaxSpeed { get; }
}

public class Car : Vehicle
{
    public Car(string brand) : base(brand) { }
    
    // Implementation of abstract method
    public override void Move() => Console.WriteLine($"{_brand} car is driving on the road.");
    
    // Implementation of abstract property
    public override int MaxSpeed => 200;
}

public class Boat : Vehicle
{
    public Boat(string brand) : base(brand) { }
    
    // Implementation of abstract method
    public override void Move() => Console.WriteLine($"{_brand} boat is sailing on the water.");
    
    // Implementation of abstract property
    public override int MaxSpeed => 50;
}

public class AbstractExample
{
    public static void Examples()
    {
        // Cannot instantiate abstract class: Vehicle v = new Vehicle("Generic"); // Error!
        
        Car car = new Car("Toyota");
        Boat boat = new Boat("Yamaha");
        
        // Using implemented methods from abstract class
        Console.WriteLine(car.GetInfo());
        car.StartEngine();
        
        // Using abstract methods implemented in derived class
        car.Move();
        Console.WriteLine($"Max speed: {car.MaxSpeed} km/h");
        
        Console.WriteLine();
        
        Console.WriteLine(boat.GetInfo());
        boat.StartEngine();
        boat.Move();
        Console.WriteLine($"Max speed: {boat.MaxSpeed} km/h");
    }
}

class Program
{
    static void Main()
    {
        AbstractExample.Examples();
    }
}
/* Output:
This is a Toyota vehicle.
Toyota engine is starting...
Toyota car is driving on the road.
Max speed: 200 km/h

This is a Yamaha vehicle.
Yamaha engine is starting...
Yamaha boat is sailing on the water.
Max speed: 50 km/h
*/

從上面程式碼可以得到幾個重點:

  1. 抽象類別:不能直接實例化,只能被繼承
  2. 抽象方法:只有方法簽名,沒有實現,必須在子類中實現
  3. 虛擬方法:有默認實現,但可以在子類中選擇性覆寫
  4. 多型:不同子類可以對相同的方法有不同的實現
  5. 建構函式鏈:使用 : base() 調用父類建構函式
    這個範例展示了物件導向程式設計的幾個重要原則:封裝、繼承和多型。

Example 2

在此範例中,Square類別必須提供 GetArea 的實現,因為它衍生自Shape

abstract class Shape
{
    public abstract int GetArea();
}

class Square : Shape
{
    private int _side;

    public Square(int n) => _side = n;

    // GetArea method is required to avoid a compile-time error.
    public override int GetArea() => _side * _side;

    static void Main()
    {
        var sq = new Square(12);
        Console.WriteLine($"Area of the square = {sq.GetArea()}");
    }
}
// Output: Area of the square = 144

其中

abstract class Shape
{
    public abstract int GetArea();
}

這是一個抽象類別,不能直接創建實例,且定義了一個抽象方法GetArea(),沒有實現(沒有大括號 {} 的內容,任何繼承Shape的類別都必須實現這個方法。

從以上程式碼可以得到幾個重點:

  1. 抽象類別:使用 abstract 關鍵字聲明、不能直接實例化、可以包含抽象方法和具體方法
  2. 抽象方法:只有方法簽名,沒有實現、必須在子類中實現、使用 abstract 關鍵字聲明
  3. 繼承:使用 : 符號表示繼承、子類必須實現所有抽象方法
  4. 方法覆寫:使用 override 關鍵字來實現抽
  5. 象方法、方法簽名必須與抽象方法完全匹配

Example 3

在這個例子中,類別 DerivedClass 衍生自抽象類別 BaseClass。此抽象類別包含一個抽象方法 AbstractMethod 和兩個抽象屬性 XY

// Abstract class
abstract class BaseClass
{
    protected int _x = 100;
    protected int _y = 150;

    // Abstract method
    public abstract void AbstractMethod();

    // Abstract properties
    public abstract int X { get; }
    public abstract int Y { get; }
}

class DerivedClass : BaseClass
{
    public override void AbstractMethod()
    {
        _x++;
        _y++;
    }

    public override int X   // overriding property
    {
        get
        {
            return _x + 10;
        }
    }

    public override int Y   // overriding property
    {
        get
        {
            return _y + 10;
        }
    }

    static void Main()
    {
        var o = new DerivedClass();
        o.AbstractMethod();
        Console.WriteLine($"x = {o.X}, y = {o.Y}");
    }
}
// Output: x = 111, y = 161

由於X的初始值是100、Y的初始值是150,在_x++;_y++;這裡分別變成X:101 Y:151,到後面return _x + 10;return _y + 10;,所以得到Output: x = 111, y = 161。

從上方程式碼內容得到的重點:

  1. 抽象類別:使用 abstract 關鍵字聲明、可以包含抽象成員和具體實現、不能直接實例化
  2. 抽象方法:沒有方法體(沒有 {} 中的代碼)、必須在子類中實現、使用 abstract 關鍵字聲明
  3. 抽象屬性:類似抽象方法,只有屬性聲明沒有實現、可以在子類中實現 get 或 set 存取子
  4. 存取修飾符:
    • protected:只有該類別和其子類可以訪問
    • public:任何程式碼都可以訪問
    • override:表示覆寫基類的抽象或虛擬成員
      這個範例展示了如何使用抽象類別來定義一個基礎結構,並讓子類來實現具體的行為。

Example 4

public abstract class Shape
{
    public string Color { get; set; }

    // Constructor of the abstract class
    protected Shape(string color)
    {
        Color = color;
        Console.WriteLine($"Created a shape with color {color}.");
    }

    // Abstract method that must be implemented by derived classes
    public abstract double CalculateArea();
}

public class Square : Shape
{
    public double Side { get; set; }

    // Constructor of the derived class calling the base class constructor
    public Square(string color, double side) : base(color)
    {
        Side = side;
    }

    public override double CalculateArea()
    {
        return Side * Side;
    }
}

public class Program
{
    public static void Main(string[] args)
     {
            Square square = new Square("red", 5);
            Console.WriteLine($"Area of the square: {square.CalculateArea()}");            
     }
}

結果如下圖:
https://ithelp.ithome.com.tw/upload/images/20250924/20178767LHOwGjUAUV.png

從上面的程式碼內容可以得到以下重點:
建構函式鏈:使用 : base(參數) 調用父類的建構函式,父類的建構函式會先執行
封裝:使用屬性(Property)來封裝欄位(Field),提供更好的控制和驗證


上一篇
Day11-建構子與物件初始化
系列文
30 天從 Python 轉職場 C# 新手入門12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言