CodeGym /課程 /C# SELF /程式設計中的抽象概念

程式設計中的抽象概念

C# SELF
等級 22 , 課堂 0
開放

1. 前言

你每天都在用電腦或手機,根本不用多想。打開瀏覽器、上網站、拍張照。你根本不用知道處理器怎麼運作、記憶體怎麼跑、或是哪種訊號在晶片裡流動。這就是使用者層級——方便、直覺,把一堆雜事都藏起來了。

但如果哪天出問題了呢?假設某個 app 開不起來。你要重裝它,這時你就得多懂一點——至少要知道去哪下載、怎麼安裝。這就是另一個抽象層級——系統、技術層。如果硬體壞了——像是硬碟或主機板掛了——那你就得懂裝置的物理結構才行。

抽象層可以有很多,每一層都把複雜度藏起來,只給你解決問題時真正需要的東西。

程式設計也是一樣。想像你坐計程車,跟司機說:「去程式設計師街42號。」你根本不在乎司機走哪條路、怎麼換檔、油箱裡加什麼油。你只想抵達目的地。其他的都被藏起來了。這不是懶惰,這就是純粹的抽象:你透過一個簡單的介面跟系統互動,完全不用管底層怎麼實作。

另一個例子——你手機裡的相機。你點圖示、拍照,照片就出現在相簿裡。你不用知道光怎麼穿過鏡頭、感光元件和晶片怎麼運作、資料怎麼進記憶體。這些都被一層好用的介面包起來。這就是抽象——讓你能用強大的工具,卻不用懂它的內部結構。

在程式世界,尤其是 C# 跟 .NET,抽象不只是方便,而是生存必需品。沒有它,很難打造大型、易懂、好維護的專案。它讓工程師能用同一種語言溝通,不會被每個模組的細節淹沒。

2. 為什麼我們需要程式設計裡的抽象?

「好啦,」你可能會說,「聽起來很厲害,但我這個未來的程式大神,為什麼要學這個?」 好問題!抽象不是為了抽象而抽象,而是有很實際、很接地氣的好處:

  • 簡化複雜系統: 我們大腦沒辦法同時記住一個超大程式的所有細節。抽象讓我們能把複雜問題拆成小塊,每塊都「藏」起自己的複雜度,只給我們需要的東西。就像樂高積木:每個零件都很簡單,但你可以拼出任何東西,完全不用管每顆積木怎麼做的。
  • 提升程式可讀性和維護性: 用抽象原則寫出來的程式碼,超級好讀又好懂。看到 device.TurnOn(),你馬上知道它要幹嘛,根本不用去翻那幾百行描述燈泡或風扇怎麼開的程式。這也讓你更容易修 bug 或加新功能。
  • 降低模組之間的耦合: 想像你的程式直接操作一堆只適用某型號手電筒的底層操作。哪天你要換另一種手電筒,整個程式都要重寫!抽象讓你可以用「任何手電筒」的共用介面來操作。你換掉手電筒的「內臟」,開關的程式根本「沒感覺」。因為它只跟抽象的介面互動。
  • 靈活性和無痛修改: 靠抽象,你可以改某個元件的內部實作,完全不影響其他跟它互動的程式。這在大專案裡超重要,因為不同團隊可以各做各的,不會互相卡住。
  • 責任分工: 你程式裡的每個元素(class、method)都有明確的責任。LightBulb class 負責燈泡功能,SmartHomeManager class 負責管理裝置,完全不用知道每個裝置的細節。

抽象不是你「加進」程式裡的某種配料。它更像是一種思考方式,設計程式時的腦袋模式。你要能看到共通點,把差異藏起來。

3. 抽象在 C#(還有 OOP)裡怎麼體現?

你可能沒發現,其實我們早就在用抽象了!它在 C# 程式裡各種層級都出現:

Class 跟 Object

Class 這個概念本身就是抽象。LightBulb class 抽象出「燈泡」這個概念,可以開、關、調亮度。當我們寫 LightBulb myLamp = new LightBulb();,我們就是在用這個抽象,而不是直接操作電子或原子。

舉例: 看我們的 LightBulb class。它有 TurnOn() method。你呼叫 myLamp.TurnOn(),燈就亮了。但你不會寫直接控制電流、開微型閥門、或啟動燈絲核融合(開玩笑啦!)的程式。這些細節都藏在 TurnOn() 的實作裡。

存取修飾詞:private 欄位和方法,就是封裝(encapsulation)的直接體現,而封裝又是實現抽象最重要的方式之一。我們把某些資料或操作藏起來,讓 class 的使用者不用管內部複雜度。像銀行 app 裡的 _updateBalance()(私有、底線開頭表示內部細節)可以做很複雜的餘額更新邏輯,但外部只看到 Deposit()Withdraw()。這就是抽象。

Method 跟 Function

每次呼叫 method,其實就是在用抽象。你相信 method 會幫你做某件事,根本不用管它怎麼做。

像我們最熟的 Console.WriteLine("哈囉,世界!");,你只要呼叫它,文字就會出現在螢幕上。你不用知道底層怎麼分配記憶體、字型怎麼變成像素、顯示卡怎麼把它們畫出來。

如果每次都要想這些,寫個小程式都要花幾小時。

Console.WriteLine 就是一個超強的抽象,把一大堆工作都藏起來了。

繼承和多型

這裡抽象的威力最明顯!當我們寫一個基底 class Animal,然後有 DogCat 繼承它,我們就是在抽象出「動物」這個共通概念,會「發出聲音」。

比如你寫 Animal myPet = new Dog();,然後 myPet.MakeSound();。你其實是在用 Animal 這個抽象。你「抽象掉」了 myPet 其實是 Dog。多型讓這個抽象的 MakeSound(),在不同型別下有不同表現(狗會叫,貓會喵)。你只管「要做什麼」(發聲),「怎麼做」就交給各自的 class。這就是抽象的勝利!

介面(先提一下,細節之後再說)

我們還沒學到,但先記住:C# 裡的 interface,是最純粹的「只說要做什麼、不管怎麼做」的抽象。interface 就是一份合約,描述有哪些 method、property 或 event,但完全不實作。它說:「誰實作這個 interface,一定要會這些功能。」我們會在第111講細講,但先知道:這是 C# 抽象的巔峰。

抽象 class(也是先提,細節下堂課)

抽象 class 介於一般 class 和 interface 之間。它可以有已實作的 method,也可以有抽象 method(像我們在第105講簡單看過),這些 method 沒有內容,一定要在子 class 裡實作。抽象 class 用來建立共用骨架、共通功能,但留下一些「洞」(抽象 method),讓子 class 去補齊。下堂課我們會超詳細講!

總之,抽象不是 C# 的某個語法元素而已。它是一個貫穿所有程式層級的強大概念,從簡單的 method 到複雜的 class 階層都用得到。

4. 程式範例:智慧家庭管理系統

回到我們的 app,來看看抽象怎麼讓它更靈活。假設我們要做一個「智慧家庭」系統。一開始只有燈泡和風扇:


public class LightBulb
{
    public string Name;
    public LightBulb(string name) => Name = name;
    public void TurnOn() => Console.WriteLine($"{Name}: 燈已開");
    public void ChangeBrightness(int level) => Console.WriteLine($"{Name}: 亮度 {level}%");
}

public class Fan
{
    public string Name;
    public Fan(string name) => Name = name;
    public void TurnOn() => Console.WriteLine($"{Name}: 風扇已開");
    public void AdjustSpeed(int speed) => Console.WriteLine($"{Name}: 速度 {speed}");
}

class Program
{
    static void Main()
    {
        var lamp = new LightBulb("廚房");
        var fan = new Fan("臥室");

        lamp.TurnOn();
        lamp.ChangeBrightness(75);

        fan.TurnOn();
        fan.AdjustSpeed(3);
    }
}

這段程式只要一個一個操作裝置都沒問題。但如果我們想讓智慧家庭真的很聰明,集中管理所有裝置?比如回家前一次開全部裝置?

如果我們用 object[] 存這些裝置,就會遇到問題:object 根本不知道有 TurnOn() 這個 method。你要呼叫它,就得檢查每個物件型別再轉型,超麻煩又醜:


// 沒有抽象和多型:
foreach (object device in allDevices)
{
    if (device is LightBulb bulb)
    {
       bulb.TurnOn();
    }
    else if (device is Fan fan)
    {
        fan.TurnOn();
    }
    // 每加一種裝置就要多寫一段...超煩!
}

這時候繼承和多型就派上用場了,它們加上封裝,就是實現抽象的工具。來寫個基底 class SmartDevice抽象出「智慧裝置」這個共通概念,然後讓燈泡和風扇繼承它。


class SmartDevice
{
    public string Name;
    public SmartDevice(string name) => Name = name;
    public virtual void TurnOn()  => Console.WriteLine($"{Name}: 裝置已開");
    public virtual void TurnOff() => Console.WriteLine($"{Name}: 裝置已關");
}

class LightBulb : SmartDevice
{
    public LightBulb(string name) : base(name) { }
    public override void TurnOn()  => Console.WriteLine($"{Name}: 燈已開");
    public override void TurnOff() => Console.WriteLine($"{Name}: 燈已關");
    public void ChangeBrightness(int x) => Console.WriteLine($"{Name}: 亮度 {x}%");
}

class Fan : SmartDevice
{
    public Fan(string name) : base(name) { }
    public override void TurnOn()  => Console.WriteLine($"{Name}: 風扇已開");
    public override void TurnOff() => Console.WriteLine($"{Name}: 風扇已關");
    public void AdjustSpeed(int s) => Console.WriteLine($"{Name}: 速度 {s}");
}

class Program
{
    static void Main()
    {
        SmartDevice[] devices =
        {
            new LightBulb("廚房"),
            new Fan("臥室"),
            new SmartDevice("感測器")
        };

        foreach (var d in devices) d.TurnOn();
        foreach (var d in devices) d.TurnOff();

        // 示範呼叫專屬方法
        foreach (var d in devices)
        {
            if (d is LightBulb b)
                b.ChangeBrightness(50);
            if (d is Fan f)
                f.AdjustSpeed(2);
        }
    }
}

你看,程式碼變得多乾淨又靈活!現在我們可以在 smartHomeDevices 裡加任何新裝置(像 SmartTVSmartThermostat),只要繼承 SmartDeviceforeach (SmartDevice device in smartHomeDevices) 這個迴圈都不用改。這就是抽象的威力。我們抽象掉裝置的型別,只專注在「能開關」這個共通能力。

這個例子很直觀地展現了繼承和多型(我們之前學過的)就是實現抽象的工具。我們做了一個泛用的抽象(SmartDevice),讓我們能用同一種方式操作不同裝置(LightBulbFan)。

不過有個小細節:現在 SmartDeviceTurnOn()TurnOff() 有「共用實作」,只是印出「裝置已開/已關(共用實作)」。但如果我們根本沒辦法給所有裝置一個有意義的「共用實作」呢?比如「一般裝置」(SmartDevice 直接 new)只是個溫度感測器,根本沒有開關。或者我們想強制所有子 class 都要自己實作這些 method?這時候就要用抽象 class抽象 method,我們下堂課會詳細講。它們是更強大的抽象工具,能保證某些 method 一定要在子 class 裡實作。

這就是我們這次 OOP 抽象原則的導覽。下堂課我們會深入聊 C# 怎麼給我們專用工具——抽象 class抽象 method——來強制實現這個概念。準備好,會更有趣喔!

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION