本章將帶你建立開發環境,接著探討 .NET 執行環境與 C# 型別系統的核心運作原理。這裡不談簡單的 if/else 和迴圈,而是直接切入關鍵的基礎觀念。這些觀念對於後續理解 C# 的進階特性至關重要。
1.1 C# 的設計哲學
語法細節稍後再談,我們先來建立對 C# 的全局觀。
物件導向為主,函數式為輔
C# 是 物件導向(object-oriented) 語言,強調封裝、繼承和多型。但從 C# 3.0 開始,它大量引入了 函數式程式設計(functional programming) 的精華:
| 函數式特性 | C# 實現 |
|---|---|
| 函式可作為值傳遞 | 委派(delegate)與 lambda 運算式 |
| 宣告式資料處理 | LINQ 查詢運算式 |
| 不可變資料結構 | record、readonly struct、init 存取子 |
| Pattern matching | switch 運算式、is 運算子 |
名詞釋疑
在程式語言中,expression(運算式)通常指的是「可以被計算(evaluate)並回傳一個值的程式碼片段」。另一方面,statement(陳述式)則是執行一個動作,通常不直接傳回值。
這就是 OOP + FP 混合設計:C# 兼具了物件導向的組織力與函數式的表達力。本書後續介紹的 record、pattern matching 和 LINQ,都源自這個哲學。
現在,我們對 C# 有了初步的認識,接著來看它的型別系統。
型別安全:編譯器是你的第一道防線
C# 是一門 強型別(strongly typed) 語言,型別規則非常嚴格:
- 靜態型別(static typing):變數的型別在編譯時期就已確定,編譯器會在程式執行前檢查型別錯誤。
- 強型別(strong typing):不允許隱含的危險型別轉換。例如,你不能把
double直接賦值給int,必須明確轉型。
好處很明顯:不僅能提早發現錯誤,還能避免執行時期爆炸。現代 C# 的許多功能(如 Nullable Reference Types)都在強化這張安全網。
// 編譯器會阻止這種危險操作
double pi = 3.14159;
int rounded = pi; // ✗ 編譯錯誤:無法隱含轉換
int rounded = (int)pi; // ✓ 明確轉型,開發者知道會無條件捨去小數
Note
簡單來說:讓編譯器幫你抓 bug。從型別安全、null 安全、pattern matching 到 record 的值相等性,每一項功能都在減少執行時期的意外。
1.2 快速上手
這個小節會帶你建立一個最簡單的 .NET 專案,並觀察它如何編譯與執行。我們暫時不使用強大的 IDE,而是回歸樸實的命令列介面(CLI)——這是理解 .NET 專案結構與建置流程(build pipeline)的最佳途徑,也是邁向自動化開發的必備技能。
必備工具
在開始之前,請先安裝以下工具:
- .NET 10 SDK(或更新版本):開發 .NET 應用程式必備。
- 開發工具(IDE、編輯器),選擇你覺得合用的:
- Visual Studio 2026 Community:Windows 平台最強大的 IDE,適合大型專案與企業級開發。
- Visual Studio Code:跨平台(Windows、macOS、Linux)、輕量且生態系豐富的編輯器,適合喜歡快速編輯與自訂 workflow 的開發者。
- JetBrains Rider:跨平台(Windows、macOS、Linux)的整合開發工具,非商業用途可免費使用。
你的第一個 .NET 專案
請暫且忘掉 File > New Project 這類滑鼠操作,打開終端機(底下指令以 Windows 環境為例),按照以下步驟操作:
Step 1: 建立 Console 應用程式
dotnet new console --name HelloCSharp
這會以預設的 Console 專案範本建立一個名為「HelloCSharp」的 C# 專案,專案裡面會有一個預設的主程式,檔案名稱是 Program.cs,裡面只有一行程式碼(註解不算):
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
Step 2: 進入專案目錄
cd HelloCSharp
Step 3: 執行程式
dotnet run
當你輸入 dotnet run 時,.NET SDK 會像組裝模型前先檢查零件包一樣,自動把缺少的元件找齊(還原套件,restore)、編譯(build),然後執行應用程式。接著,你會看到終端機輸出:
Hello, World!
上述過程執行了三個命令,分別完成三件事情:
dotnet new:根據範本產生專案檔(.csproj)和程式檔(Program.cs)。dotnet build:將 C# 原始碼編譯成 中間語言(IL) 並打包成.dll或.exe。dotnet run:啟動 .NET Runtime(Common Language Runtime,CLR)執行編譯好的程式。

什麼是 IL(Intermediate Language)?
C# 程式碼不會直接編譯成機器碼,而是先編譯成一種稱為 IL(Intermediate Language,中間語言)的中間格式。IL 是一種與平台無關的指令集,讓同一份編譯後的程式可以在 Windows、macOS、Linux 等不同作業系統上執行。當程式執行時,.NET Runtime(CLR)會將 IL 即時編譯(JIT, Just-In-Time compilation)成該作業系統的機器碼。詳細資訊請參閱微軟官方文件。
如果你想試試用整合開發環境(IDE)來編寫和執行剛才的範例程式,可參考微軟的線上教學文件:使用 Visual Studio 建立 .NET 控制台應用程式。
Note
現代 C# 專案通常啟用「頂層語句(top-level statements)」,這就是為什麼你在
Program.cs中看不到舊版 C# 的寫法,如class Program或static void Main,而只有簡潔的一行Console.WriteLine("Hello, World!");。
1.3 .NET 執行環境架構
當你輸入 dotnet run 指令,看到程式跑起來的那一刻,背後其實是一整套分工精細的系統在協同運作。.NET 平台採用分層設計,兼顧高階框架的便利與底層效能的掌控。具體來說,.NET 執行環境可拆解為三個核心層次:
| 層次 | 名稱 | 功能 |
|---|---|---|
| 最上層 | Application Layer | 應用程式框架(ASP.NET、MAUI、WinUI 等) |
| 中間層 | BCL | 基礎類別庫(集合、I/O、網路、加密等) |
| 最底層 | CLR | 通用語言執行環境(記憶體管理、JIT 編譯、例外處理) |
如下圖所示:

CLR(Common Language Runtime) 是整個 .NET 的心臟,負責:
- 將 IL 編譯成機器碼(JIT 編譯)
- 自動記憶體管理(垃圾回收)
- 型別安全檢查
- 例外處理
BCL(Base Class Library) 提供開發者日常需要的基礎功能:
- 集合(
List<T>、Dictionary<K,V>) - 字串處理、正規運算式(regular expressions)
- 檔案 I/O、網路
- 非同步程式設計(
async/await)
Application Layer 則是針對特定應用場景的框架:
- ASP.NET Core:Web 應用程式與 API
- MAUI:跨平台行動裝置與桌面應用
- WinUI / WPF:Windows 桌面應用
跨平台支援
現代 .NET(從 .NET 5 之後)支援多種平台:
| 執行平台 | 支援的應用類型 |
|---|---|
| Windows | Console、Web、桌面(WPF/WinUI)、服務 |
| macOS | Console、Web、桌面(Mac Catalyst) |
| Linux | Console、Web、服務 |
| iOS / Android | 行動應用(透過 MAUI) |
| 瀏覽器 | WebAssembly(透過 Blazor) |
這意味著你用 C# 寫的程式碼,只要不依賴特定平台的 API,就可以在這些平台上執行。
1.4 堆疊與堆積
要寫出高效能且少 bug 的程式,得先認識記憶體配置。最基礎的兩個概念是 堆疊(stack) 和 堆積(heap)。
為了方便理解,我們可以這樣想像:
-
Stack(堆疊)像是你的辦公桌:
- 空間有限,但存取速度極快(伸手就拿得到)。
- 用來處理當下正在進行的工作(函式呼叫、區域變數)。
- 當工作結束(函式執行完畢),桌上的東西就會立刻被清空,準備處理下一件工作。
-
Heap(堆積)像是公司的倉庫:
- 空間很大,但存取速度較慢(要走去倉庫找)。
- 用來存放長期保存的資料或大型物件。
- 當你不再需要某個物品時,不會立刻消失,而是等待清潔人員(garbage collector)在固定時間來回收清理。

堆疊和堆積都是電腦當中的記憶體區塊,只是按照用途和特性給予不同的名稱而已。以下分別說明。
堆疊(stack)
-
特性:
- 極快:配置與釋放只需移動指標。
- 確定性:變數離開作用域(scope)即自動釋放。
- 空間小:通常只有幾 MB,用盡會導致
StackOverflowException。
-
存放內容:
- 區域變數(local variables)的基本資料。
- 方法呼叫的上下文(參數、回傳位址等)。
堆積(heap)
-
特性:
- 較慢:配置需要尋找足夠的連續空間;釋放依賴垃圾回收器(garbage collector, GC)。
- 靈活:空間大,適合存放生命週期較長或大小不定的資料。
-
存放內容:
- 較大或生命週期較長的資料、物件(objects)與集合(collections)等等。
1.5 實值型別與參考型別
C# 程式並非直接在作業系統上執行,而是執行於 .NET 的執行環境,稱為 CLR(Common Language Runtime)。CLR 的任務繁重,例如管理記憶體、處理例外、提供安全機制等等。不過,你不需要立刻了解 .NET CLR 的內部運作和所有功能,只要先知道它肩負以下兩個重要工作:
- 管理型別資訊
- 配置與回收記憶體(垃圾回收)
在記憶體配置的部分,CLR 會根據型別的特性與其實際用法來決定把資料放在堆疊(stack)還是堆積(heap)。所謂型別的特性,指的是 C# 的兩個型別分類:實值型別(value types)和參考型別(reference types)。

實值型別(Value Types)
實值型別有這些:
- 所有數值型別(
int、double、byte、decimal…) bool、charenum(列舉)struct(結構),包含DateTime、Guid、Span<T>
以下是它們的行為特性:
- 直接包含資料。
- 指派給另一個變數時,會複製整個值(copy by value)。
你可以把這想像成影印文件:當你把文件 A 影印一份給同事(變數賦值),同事在影本上塗寫(修改變數),並不會影響你原本的那份文件。
以下範例展示了實值型別的特性:
int a = 10;
int b = a; // 複製一份 10 給 b
b = 99; // 修改 b 不會影響 a
Console.WriteLine(a); // 輸出 10
第 3 行修改 b 的值並不會改變 a 的值,因為 int 是實值型別。
原始碼: DemoValueTypes
參考型別(Reference Types)
以下都是參考型別:
class(包含string,object, 陣列[])interfacedelegate(第 8 章會介紹委派與事件)record(預設為參考型別,詳見本書第 4 章)
行為特性:
- 儲存記憶體位址(reference),指向 heap 上的物件。
- 指派給變數時,只複製位址,兩個變數指向同一個物件。
這好比分享雲端文件連結:你把文件的連結傳給同事(變數賦值),你們兩個人手上拿的都只是連結(reference),但連結到的都是同一份雲端文件(object)。當同事透過連結修改了文件內容,你打開連結時也會看到修改後的結果。

以下範例展示了參考型別的特性:
var user1 = new User { Name = "Alice" };
var user2 = user1; // 複製位址,user1 和 user2 指向同一個物件
user2.Name = "Bob"; // 修改 user2 會影響 user1
Console.WriteLine(user1.Name); // 輸出 Bob
原始碼: DemoReferenceTypes
Ask AI:那些容易搞混的型別
很多 bugs 都源自對型別分類的誤解(面試考題也常問)。你可以把這段提示詞丟給 AI 測試觀念:
Prompt
在 C# 中,
int[](整數陣列)是 Value Type 還是 Reference Type?如果是 Reference Type,但它裡面裝的又是int(Value Type),那這些int存在哪裡?Stack 還是 Heap?請解釋底層記憶體配置。
依你使用的 AI 工具而定,每次得到的答案可能有些差異。以下是 AI 回答的參考範例(經過簡化):
AI 回答
int[]本身是陣列,屬於 Reference Type。因此,整個陣列物件(包含它裡面的所有int元素)都儲存在 Heap 上,即使int本身是 Value Type。
陣列的記憶體配置:元素型別的差異
許多開發者以為陣列就是「連續的記憶體空間」,但這個觀念只對了一半。
事實上,陣列的底層記憶體配置方式,會因為元素是實值型別還是參考型別而有顯著差異。這個差異會影響程式的執行效能(CPU cache 命中率)與垃圾回收(GC)的負擔。接著就從記憶體配置的觀點來看看這兩者有什麼不同。
Value Type 元素陣列
以底下的範例來說:
long[] numbers = new long[3]; // 每個 long 是 8 bytes
numbers[0] = 12345;
numbers[1] = 54321;
numbers[2] = 99999;
在 heap 上,CLR 會配置一塊連續的記憶體空間,三個 long 值直接儲存在陣列中。具體來說:
- 陣列結構:包含陣列長度等標頭資訊。
- 內容:直接存放數值本身(如
12345),緊密排列。 - 結果:一次記憶體配置,資料集中,效率高。
打個比方,就像是健身房的一整排置物櫃:你打開第 0 號櫃子,裡面直接放著你的東西(數值);打開第 1 號櫃子,裡面也直接放著東西。存取非常快速且直接。
Reference Type 元素陣列
範例:
StringBuilder[] builders = new StringBuilder[3];
builders[0] = new StringBuilder("builder1");
builders[1] = new StringBuilder("builder2");
builders[2] = new StringBuilder("builder3");
在這種情況下,陣列中不是直接存放物件本身,而是存放指向物件的參考。具體來說:
- 陣列結構:包含陣列長度等標頭資訊。
- 內容:存放的是指向
StringBuilder物件的參考(記憶體位址)。 - 實際物件:分散放在堆積(heap)裡。
- 結果:多次記憶體配置(陣列一次 + 每個物件各一次),資料非連續,存取成本較高。
如同一排信箱:打開第 0 號信箱,裡面只有一張紙條(參考),上面寫著「你的包裹在倉庫 B 區 3 號架」。你必須再跑去倉庫(heap 的其他位置)才能真正拿到包裹(物件)。如果每個信箱裡的紙條都指向倉庫的不同角落,你就得跑來跑去,這當然比較慢。
這帶來兩個重要影響:
- 記憶體區域性(locality):value type 陣列的元素在記憶體中是連續存放,故 CPU 快取命中率高,存取效能更好。
- GC 壓力:reference type 陣列會產生多個散落物件,增加垃圾回收的負擔。

本節重點整理:
- 無論是實值型別還是參考型別的變數,都會有一個「存放位置」。
- 大多數情況下,區域變數(local variable)是配置在堆疊(stack)中。
- 實值型別變數通常存放在堆疊中,變數本身直接存放資料。
- 參考型別變數可能存放在堆疊或堆積中(依實際情況而定),但該變數的內容是一個位於堆積的記憶體位址,亦即指向實際物件的參考。
1.6 Boxing 與 Unboxing
Boxing 和 unboxing 是為了讓實值型別(value types)也能當作物件處理的機制,但它有額外的效能成本。
Boxing(裝箱)
所謂的 boxing(裝箱)就是把一個 value type 轉換為 object(reference type)的過程。
就好像你買了一顆蘋果(value type),原本可以直接拿在手上(stack),但現在要把它寄存在倉庫(heap),你就必須把它裝進一個箱子(boxing),並且貼上標籤。這個「找箱子」和「打包」的過程,就是額外的成本。
Boxing 過程發生了什麼事:CLR 會在 heap 上配置一塊記憶體,把 value type 的值複製進去。
範例:
int i = 123;
object o = i; // Boxing:將 int 封裝進 Heap 上的 object
Unboxing(拆箱)
跟 boxing(裝箱)相反,unboxing(拆箱)指的是將 object 轉回 value type 的過程。
Unboxing 實際做的事情:檢查 object 裡面是否真的裝了該型別的值,如果是,則將值從 heap 複製回 stack。
範例:
int j = (int)o; // Unboxing

為什麼要在意?
Boxing 有兩個成本:
- CPU 計算:複製資料與記憶體配置。
- GC 壓力:Boxing 會產生額外的 heap 物件,增加垃圾回收的工作量。
隱性 boxing 陷阱常見於舊式集合(如 ArrayList)或格式化字串,例如:
int x = 10;
// 陷阱 1:string.Format 接受 object 參數
string s = string.Format("Score: {0}", x); // x 被 boxing!
// 現代解法:字串插補(通常編譯器會優化)
string s2 = $"Score: {x}";
原始碼: DemoBoxingUnboxing
要提醒的是,「字串插補是否會造成 boxing」取決於編譯器版本與具體寫法;在現代 C#/.NET 中,多數常見情境(例如插補 int)通常不會再退回到 string.Format(object, ...) 那種會 boxing 的路徑,但仍可能產生字串配置(allocation)。
如果你遇到的是「大量格式化輸出」的熱點(hot path),且要求高效能的場景,更可靠的做法通常是:
- 盡量避免走到以
object參數為主的 API(例如某些params object[]形式)。 - 使用
Span<T>來減少字串配置(詳見第 12 章)。 - 如果是集合類型,優先採用 泛型(generics) 集合,例如用
List<int>取代ArrayList。
- 字串插補(string interpolation)會在本書第 2 章介紹(第 2.6 節)。
- 泛型(generics)在本書第 7 章。
1.7 物件與陣列的複製
複製物件或陣列時,得先搞懂 淺層複製(shallow copy) 和 深層複製(deep copy) 是怎麼運作的,因為 C# 沒有內建的深層複製機制。這個主題橫跨實值型別與參考型別,是記憶體管理的重要概念。
物件的淺層複製與深層複製
淺層複製 只複製物件本身,裡面的參考型別欄位還是指向同一個物件:
class Team
{
public string Name { get; set; }
public List<string> Members { get; set; }
// 使用 object.MemberwiseClone() 逐一複製成員(淺層複製)
public Team ShallowCopy() => (Team)MemberwiseClone();
}
var team1 = new Team
{
Name = "Dev",
Members = new List<string> { "Alice", "Bob" }
};
var team2 = team1.ShallowCopy(); // 淺層複製
team2.Name = "QA"; // team2 有自己的 Name 副本
team2.Members.Add("Charlie"); // 但 Members 仍指向同一個 List!
Console.WriteLine(team1.Members.Count); // 輸出 3(受影響!)
此範例展示了淺層複製可能造成的潛在 bug:內層的參考物件只是複製參考,因此對 team2.Members 串列加入新元素時,等同於改動 team1.Members 串列。
原始碼: DemoShallowCopy
.NET 早期版本提供了 ICloneable 介面作為複製物件的標準做法:當類別需要提供複製操作時,只要實作該介面的 Clone() 方法即可。但此設計有個蠻大的缺點:Clone() 方法本身的語意不明確,它沒有明白規定是「淺層複製」還是「深層複製」。於是,當你呼叫一個第三方程式庫的 Clone(),往往需要去查文件來確認它到底是執行淺層複製還是複製了整個物件樹。因此,如果物件要提供淺層複製,現代 .NET 推薦的做法是在類別裡面定義一個名稱明確的方法:ShallowCopy,就如前面範例所展示的。
Note
其他複製物件的方法還有複製建構式(copy constructor),以及使用
record型別搭配with運算式(推薦!詳見本書第 4 章)。
那麼,如果要提供深層複製呢?方法有幾種。以下範例採用的是複製建構式(copy constructor)的寫法:
class Team
{
public string Name { get; set; }
public List<string> Members { get; set; }
public Team(Team original)
{
Name = original.Name; // string 是 immutable,淺層複製即可
// 深層複製,避免共用相同的物件參考
Members = new List<string>(original.Members);
}
}
var team1 = new Team
{
Name = "Dev",
Members = new List<string> { "Alice", "Bob" }
};
var team2 = new Team(team1); // 使用複製建構式來建立物件的副本
team2.Members.Add("Charlie");
Console.WriteLine(team1.Members.Count); // 輸出 2(team1 不受影響)
原始碼: DemoDeepCopy
在這個範例中:
Name屬性只是複製了參考(淺層複製),但不會造成問題。為什麼?因為string雖然是參考型別,但它是不可變(immutable)的——雖然寫法上看起來字串是可修改的,但其實任何對字串的修改操作(如Replace、ToUpper)都會產生一個新的字串物件。相對地,Members是List<string>,屬於可變(mutable)的集合,必須建立新實例才能避免共用同一份資料。- 複製建構式(copy constructor)是一種「最佳實務」,而非「語言強制」。雖然名稱不像
ShallowCopy或DeepCopy那樣明確,但它的好處是強型別(輸入參數和回傳值都是),而且是現代 .NET 實作物件複製的最佳方式之一。例如,C# 9 的record本身就具備自動產生複製建構式的功能。
Note
record型別還能夠搭配with運算式來輕鬆實現淺層複製操作,這也是現代 C# 推薦的做法。詳見〈第 4 章:不可變設計〉。
如果物件內有巢狀的參考型別(例如 Team 內有 Manager 物件,Manager 內又有其他物件),就需要遞迴地為每一層建立新實例。實務上,可使用序列化/反序列化(例如 System.Text.Json)或其他第三方函式庫來自動處理深層複製。
以下整理剛才介紹的幾種物件複製方法:
- 實作
ICloneable.Clone():已過時,不推薦。缺點:語意不明確,不是強型別(回傳object)。 - 實作建構式:普遍接受的最佳實務,推薦使用。缺點是成員數量眾多時,程式碼較繁瑣。
record型別搭配with:語法簡潔,推薦使用。(第 4 章)- 序列化:最簡便,但效能可能較慢、需要注意私有成員是否遺漏。
Ask AI
我想知道在 .NET 中的這幾種複製物件的方法有何優缺點:ICloneable、Record (with)、複製建構式、序列化。請整理一張比較表。
然後,請用 .NET 內建的
System.Text.Json實作物件的深層複製。
陣列的淺層複製與深層複製
陣列複製的原理與物件相同:Array.Clone() 執行的是淺層複製。
如果陣列中的元素型別是 value type 或 string,淺層複製已經足夠。如以下範例:
int[] original = { 1, 2, 3 };
int[] copy = (int[])original.Clone();
copy[0] = 999;
Console.WriteLine(original[0]); // 輸出 1(不受影響,因為 int 是 value type)
對於 reference type 元素,則必須留意:
var original = new StringBuilder[]
{
new StringBuilder("A"),
new StringBuilder("B")
};
var copy = (StringBuilder[])original.Clone();
copy[0].Append("!"); // 修改 copy 會影響 original!
Console.WriteLine(original[0]); // 輸出 "A!"(受影響!)
若要實現深層複製,必須逐一複製每個元素:
var deepCopy = new StringBuilder[original.Length];
for (int i = 0; i < original.Length; i++)
{
deepCopy[i] = new StringBuilder(original[i].ToString());
}
deepCopy[0].Append("!");
Console.WriteLine(original[0]); // 輸出 "A"(不受影響)
原始碼: DemoDeepCopyArray
本章重點回顧
- C# 設計哲學:OOP + FP 混合,強調型別安全,讓編譯器幫你抓 bug。
- 執行機制:原始碼編譯為中介語言代碼(IL code),執行時由 CLR 透過 JIT 編譯為機器碼;CLR 亦負責垃圾回收(GC)。
- Stack 與 Heap:Stack 存取快但空間有限(主要存區域變數),heap 空間大(存物件實體),但需要垃圾回收。
- Value types vs Reference types:前者直接包含資料(傳值),後者儲存記憶體位址(傳址)。
- 陣列記憶體特性:Value type 陣列在記憶體中是連續的(效能佳);Reference type 陣列則是儲存參考(可能增加 GC 壓力)。
- Boxing/Unboxing:將 Value type 轉換為 Object 的過程,隱藏著效能殺手。
- 物件複製:C# 僅內建淺層複製。若需要深層複製,必須手動實作或透過序列化(如
System.Text.Json)達成。
下一章,我們將探討現代 C# 的語法糖與宣告技巧,掌握如何寫出更簡潔、優雅的程式碼。
👉 回到本書主頁
讀者互動