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,然後有 Dog 跟 Cat 繼承它,我們就是在抽象出「動物」這個共通概念,會「發出聲音」。
比如你寫 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 裡加任何新裝置(像 SmartTV、SmartThermostat),只要繼承 SmartDevice,foreach (SmartDevice device in smartHomeDevices) 這個迴圈都不用改。這就是抽象的威力。我們抽象掉裝置的型別,只專注在「能開關」這個共通能力。
這個例子很直觀地展現了繼承和多型(我們之前學過的)就是實現抽象的工具。我們做了一個泛用的抽象(SmartDevice),讓我們能用同一種方式操作不同裝置(LightBulb、Fan)。
不過有個小細節:現在 SmartDevice 的 TurnOn() 和 TurnOff() 有「共用實作」,只是印出「裝置已開/已關(共用實作)」。但如果我們根本沒辦法給所有裝置一個有意義的「共用實作」呢?比如「一般裝置」(SmartDevice 直接 new)只是個溫度感測器,根本沒有開關。或者我們想強制所有子 class 都要自己實作這些 method?這時候就要用抽象 class 和抽象 method,我們下堂課會詳細講。它們是更強大的抽象工具,能保證某些 method 一定要在子 class 裡實作。
這就是我們這次 OOP 抽象原則的導覽。下堂課我們會深入聊 C# 怎麼給我們專用工具——抽象 class 和抽象 method——來強制實現這個概念。準備好,會更有趣喔!
GO TO FULL VERSION