C# 速覽
這一章不是完整的 C# 教學,只挑出後面章節一定會用到的語法,讓你讀程式碼時不卡殼。如果你寫過 Java、TypeScript、Kotlin 之類的靜態型別語言,大部分內容一眼就能看懂,可以快速瀏覽。
與其他章節不同,本章是一次"速覽",會一次介紹幾個小語法點。它們有一個共同的主題:C# 是靜態型別語言,編譯器在執行前就知道每個值的型別,並據此幫你檢查程式碼。後面所有章節都建立在這個前提上。
本章的完整程式碼是一個控制檯程式:
// 1. Static typing and type inference
int count = 2;
var owner = "Alex";
// 2. String interpolation
Console.WriteLine($"{owner} has {count} todo items");
// 3. Nullable reference types
string? nickname = null;
Console.WriteLine($"Nickname length: {nickname?.Length ?? 0}");
// 4. Lambda expressions
Func<int, int> square = x => x * x;
Console.WriteLine($"Square of 5 is {square(5)}");
// 5. Records
var milk = new Todo(1, "Buy milk", false);
var milkDone = milk with { Done = true };
Console.WriteLine(milk);
Console.WriteLine(milkDone);
Console.WriteLine($"Value equality: {milk == new Todo(1, "Buy milk", false)}");
// 6. Collections and LINQ
List<Todo> todos = [milk, new Todo(2, "Write code", true)];
var pending = todos.Where(t => !t.Done).Select(t => t.Title);
Console.WriteLine($"Pending: {string.Join(", ", pending)}");
// 7. async / await
var message = await LoadMessageAsync();
Console.WriteLine(message);
static async Task<string> LoadMessageAsync()
{
await Task.Delay(100);
return "Async operation complete";
}
record Todo(int Id, string Title, bool Done);執行它(在 samples/01-csharp-tour 目錄下,或者按「環境準備」的方式用 dotnet new console 新建專案並替換 Program.cs):
dotnet run預期輸出:
Alex has 2 todo items
Nickname length: 0
Square of 5 is 25
Todo { Id = 1, Title = Buy milk, Done = False }
Todo { Id = 1, Title = Buy milk, Done = True }
Value equality: True
Pending: Buy milk
Async operation complete下面分三組來看。
型別、可空與 Lambda
// 1. Static typing and type inference
int count = 2;
var owner = "Alex";
// 2. String interpolation
Console.WriteLine($"{owner} has {count} todo items");
// 3. Nullable reference types
string? nickname = null;
Console.WriteLine($"Nickname length: {nickname?.Length ?? 0}");
// 4. Lambda expressions
Func<int, int> square = x => x * x;
Console.WriteLine($"Square of 5 is {square(5)}");
// 5. Records
var milk = new Todo(1, "Buy milk", false);
var milkDone = milk with { Done = true };
Console.WriteLine(milk);
Console.WriteLine(milkDone);
Console.WriteLine($"Value equality: {milk == new Todo(1, "Buy milk", false)}");
// 6. Collections and LINQ
List<Todo> todos = [milk, new Todo(2, "Write code", true)];
var pending = todos.Where(t => !t.Done).Select(t => t.Title);
Console.WriteLine($"Pending: {string.Join(", ", pending)}");
// 7. async / await
var message = await LoadMessageAsync();
Console.WriteLine(message);
static async Task<string> LoadMessageAsync()
{
await Task.Delay(100);
return "Async operation complete";
}
record Todo(int Id, string Title, bool Done);靜態型別與 var
第 2 行顯式寫出了型別 int。第 3 行用 var 讓編譯器根據右邊的值推斷型別——owner 的型別是 string,而且之後永遠是 string。
var 不是"動態型別"。如果之後寫 owner = 42;,編譯會直接失敗。它只是省去了重複書寫型別名,型別本身在編譯期就確定了。在 VS Code 裡把滑鼠懸停在變數上,就能看到推斷出的型別。
FastAPI 對照
Python 的型別標註(type hints)是可選的,執行階段也不檢查;FastAPI 讀取它們來做資料轉換。C# 的型別是強制的:編譯器檢查每一處使用,型別不對就無法執行。後面你會看到 ASP.NET Core 同樣讀取這些型別來做參數繫結和產生文件。
字串插值
第 6 行的字串前面有個 $,這叫字串插值(string interpolation):花括號裡可以直接寫變數或表示式,執行階段替換成它的值。這和 Python 的 f-string、JavaScript 的模板字串是一回事。
可空引用型別
第 9 行的 string? 表示"這個變數可能是 null";不帶問號的 string 表示"不應該是 null"。這個特性叫可空引用型別(nullable reference types),在專案檔案中由 <Nullable>enable</Nullable> 開啟,.NET 模板預設就是開啟的。
為什麼需要它?空引用錯誤(存取 null 的成員)是最常見的執行階段崩潰之一。有了可空標註,編譯器能提前發現隱患。比如把第 10 行改成直接存取 nickname.Length,編譯時就會警告:
Program.cs(10,19): warning CS8602: 解引用可能出现空引用。第 10 行的正確寫法用到了兩個運算子:
?.(空條件運算子):nickname為null時不存取.Length,整個表示式的結果直接是null;??(空合併運算子):左邊為null時使用右邊的值,這裡是0。
注意
可空標註是編譯期檢查,不會在執行階段阻止 null 出現。比如從 JSON 反序列化出來的資料,即使型別寫的是 string,缺少欄位時仍然可能是 null。「請求本文」一章會遇到這種情況,「參數驗證」一章會解決它。
Lambda 表示式
第 13 行的 x => x * x 是一個 Lambda 表示式(匿名函式):=> 左邊是參數,右邊是回傳值。Func<int, int> 是它的型別,意思是"接收一個 int,回傳一個 int 的函式"。
下一章「第一步」會用到它:app.MapGet("/", () => ...) 的第二個參數就是 Lambda。Lambda 的函式體也可以是一個用花括號包起來的程式碼塊,裡面用 return 回傳結果,後面的章節會經常這樣寫。
FastAPI 對照
相當於 Python 的 lambda x: x * x,但 C# 的 Lambda 可以包含多行陳述式,所以端點的處理邏輯可以直接寫在 Lambda 裡。
record 與 LINQ
// 1. Static typing and type inference
int count = 2;
var owner = "Alex";
// 2. String interpolation
Console.WriteLine($"{owner} has {count} todo items");
// 3. Nullable reference types
string? nickname = null;
Console.WriteLine($"Nickname length: {nickname?.Length ?? 0}");
// 4. Lambda expressions
Func<int, int> square = x => x * x;
Console.WriteLine($"Square of 5 is {square(5)}");
// 5. Records
var milk = new Todo(1, "Buy milk", false);
var milkDone = milk with { Done = true };
Console.WriteLine(milk);
Console.WriteLine(milkDone);
Console.WriteLine($"Value equality: {milk == new Todo(1, "Buy milk", false)}");
// 6. Collections and LINQ
List<Todo> todos = [milk, new Todo(2, "Write code", true)];
var pending = todos.Where(t => !t.Done).Select(t => t.Title);
Console.WriteLine($"Pending: {string.Join(", ", pending)}");
// 7. async / await
var message = await LoadMessageAsync();
Console.WriteLine(message);
static async Task<string> LoadMessageAsync()
{
await Task.Delay(100);
return "Async operation complete";
}
record Todo(int Id, string Title, bool Done);record
第 38 行一行程式碼定義了一個型別 Todo:
- 圓括號中的
int Id, string Title, bool Done既是建構函式的參數,也自動產生同名的init屬性,只能在初始化時賦值; - 編譯器還自動產生了
ToString()(所以第 19 行能列印出Todo { Id = 1, ... })和基於值的相等比較。
這種型別叫 record(記錄型別)。本例使用的是位置式 record class(class 關鍵字可以省略),適合表示"一組資料",有兩個方便的特性:
- 值相等:兩個 record 的所有屬性都相等,它們就相等。第 21 行
milk == new Todo(1, "Buy milk", false)的結果是True,即使它們是兩個不同的物件。 - 初始化後不能重新賦值:本例自動產生的
init屬性只能在初始化時賦值。想要"修改",就用 第 18 行 的with表示式複製一份並替換部分屬性,原來的milk保持不變。
record 本身並不強制不可變:你仍然可以宣告帶 set 的屬性。如果屬性指向一個可變清單,init 也只限制替換這個清單,不會阻止修改清單中的元素。這叫淺層不可變性(shallow immutability)。
為什麼 Web API 偏愛 record?請求和回應中的資料本質上就是"一組值":它們從 JSON 來、到 JSON 去,中途不應該被意外修改。record 用最少的程式碼表達了這種意圖。從「請求本文」一章開始,所有的資料模型都會用 record 定義。
技術細節
在使用最上層陳述式的 Program.cs 中,型別定義(如 record Todo)必須寫在所有陳述式的後面,所以你會在每個範例檔案的末尾看到它們。第 32 行的 static async Task<string> LoadMessageAsync() 是一個區域性函式,可以寫在陳述式之間,也可以寫在後面。
FastAPI 對照
record 的角色類似 Pydantic 模型或 @dataclass(frozen=True):用欄位宣告描述資料的形狀。區別在於 record 本身不做驗證,驗證由 ASP.NET Core 在「參數驗證」一章中另行完成。
集合與 LINQ
第 24 行的方括號 [milk, new Todo(2, "Write code", true)] 是集合表示式(collection expression),用來建立一個 List<Todo>。List<Todo> 中的 <Todo> 叫泛型參數,表示"這是一個只能裝 Todo 的清單",往裡放其他型別的東西會編譯失敗。
第 25 行是 LINQ(Language Integrated Query,語言整合查詢):
Where(t => !t.Done):篩選出未完成的項;Select(t => t.Title):只取出標題。
兩個方法都接收 Lambda 作為參數,可以像管道一樣串聯。LINQ 在後面會頻繁出現:「查詢參數」一章用它做篩選和分頁,「EF Core 入門」一章會看到同樣的寫法被翻譯成 SQL 查詢資料庫。
FastAPI 對照
第 25 行相當於 Python 的 [t.title for t in todos if not t.done]。
async / await
// 1. Static typing and type inference
int count = 2;
var owner = "Alex";
// 2. String interpolation
Console.WriteLine($"{owner} has {count} todo items");
// 3. Nullable reference types
string? nickname = null;
Console.WriteLine($"Nickname length: {nickname?.Length ?? 0}");
// 4. Lambda expressions
Func<int, int> square = x => x * x;
Console.WriteLine($"Square of 5 is {square(5)}");
// 5. Records
var milk = new Todo(1, "Buy milk", false);
var milkDone = milk with { Done = true };
Console.WriteLine(milk);
Console.WriteLine(milkDone);
Console.WriteLine($"Value equality: {milk == new Todo(1, "Buy milk", false)}");
// 6. Collections and LINQ
List<Todo> todos = [milk, new Todo(2, "Write code", true)];
var pending = todos.Where(t => !t.Done).Select(t => t.Title);
Console.WriteLine($"Pending: {string.Join(", ", pending)}");
// 7. async / await
var message = await LoadMessageAsync();
Console.WriteLine(message);
static async Task<string> LoadMessageAsync()
{
await Task.Delay(100);
return "Async operation complete";
}
record Todo(int Id, string Title, bool Done);第 32~36 行定義了一個非同步方法:
async表示方法內部可以使用await;- 回傳型別
Task<string>表示"一個將來會產生string的操作"; - 第 34 行的
await Task.Delay(100)等待 100 毫秒,但不會阻塞執行緒。
第 29 行用 await 等待這個方法完成,取出結果。按照慣例,非同步方法的名字以 Async 結尾。
為什麼 Web 服務要用非同步?伺服器處理請求時,大部分時間在等待:等資料庫回傳、等檔案讀完、等另一個服務回應。同步程式碼在等待期間會一直佔著執行緒;非同步程式碼在等待時把執行緒交還給伺服器,讓它去處理別的請求。結果是同樣的機器能同時處理多得多的請求。從「EF Core 入門」開始,所有資料庫操作都會是非同步的。
FastAPI 對照
和 Python 的 async def / await 概念一致。區別是 ASP.NET Core 中同步和非同步的處理常式可以混用,框架都能正確處理,不存在"在 async 函式裡呼叫同步阻塞程式碼會卡住事件迴圈"這類問題——但阻塞呼叫仍會浪費執行緒,所以涉及 I/O 時仍應使用非同步 API。
總結
- C# 是靜態型別語言,
var只是讓編譯器推斷型別,型別本身在編譯期確定。 $"..."是字串插值;string?表示可為空,配合?.和??安全地處理null。可空檢查只發生在編譯期。- Lambda 表示式
x => ...是匿名函式,MapGet等方法用它作為處理常式。 - 位置式 record 能簡潔地定義資料型別,自帶值相等和
with複製;本例的屬性使用init,但record並不強制所有成員不可變。 - LINQ 用
Where、Select等方法鏈式處理集合;async/await 讓等待 I/O 時不佔用執行緒。
