一、非同步程式設計(async/await)
1.1 基礎概念
什麼是非同步?
同步程式碼像排隊買飲料 — 每個人必須等前一個人買完才能點餐。非同步則像餐廳點餐 — 你點完餐後可以先做別的事,餐點準備好了再通知你。
// 同步:執行緒被阻塞,等 DB 回應
DataTable result = dbc.FillDataTable(sql, ht); // 等待中...什麼都不能做
// 非同步:執行緒可以去服務其他 Request
DataTable result = await dbc.FillDataTableAsync(sql, ht); // 先去忙別的
為什麼 Web API 需要非同步?
ASP.NET Core 的執行緒池是有限的。如果每個 Request 都同步等待 DB 回應,執行緒就被卡住了:
| 情境 | 100 個同時 Request | 執行緒使用 |
|---|---|---|
| 同步 | 100 個執行緒全部等 DB | 執行緒池耗盡 → 503 |
| 非同步 | 100 個 Request 發出 DB 請求後釋放執行緒 | 只需少量執行緒輪流處理 |
1.2 Task 與 Task<T>
Task 是「一個尚未完成的工作」的物件表示。它不是執行緒,而是一個承諾(Promise)— 承諾未來會完成並給你結果。
// Task:不回傳值的非同步操作
Task SaveLogAsync()
{
await db.ExecuteAsync("INSERT INTO LOG ...");
}
// Task<T>:回傳值的非同步操作
Task<DataTable> GetDataAsync()
{
return await db.FillDataTableAsync(sql, ht);
}
Task 的三種狀態:
1.3 async/await 語法
基本規則:
async標記方法:告訴編譯器這個方法內有awaitawait等待 Task:暫停方法執行,釋放執行緒,Task 完成後恢復- 回傳
Task或Task<T>:async 方法的回傳型態 - 命名慣例
XxxAsync:非同步方法名稱加 Async 後綴
public async Task<IActionResult> Init()
{
var data = await GetMasterDataAsync();
var task1 = GetDropdownAsync("QL_YEAR");
var task2 = GetDropdownAsync("QL_FUND");
await Task.WhenAll(task1, task2);
return Ok(data);
}
1.4 TAP 模式(Task-based Asynchronous Pattern)
TAP 是 .NET 推薦的非同步設計模式。
常見陷阱:
- ❌ 不要用
.Result或.Wait()— 會造成死鎖 - ✔ 用
await - ❌ 不要
async void(除了事件處理器) — 例外無法被捕獲 - ✔ 用
async Task
1.5 在 IFA 後端的實際應用
IFA 的 KSI Kernel 框架目前使用同步模式。理解非同步概念對以下場景至關重要:
- 場景 1:Controller Action 從同步升級為非同步
- 場景 2:手動 Transaction 的非同步版本
二、LINQ(Language Integrated Query)
2.1 問題意識:LINQ 出現前,開發者在痛苦什麼?
在 LINQ 出現前(C# 2.0 以前),篩選、排序、聚合都要手動寫迴圈。問題不只是「程式碼長」,而是邏輯分散 — 篩選、排序、投影三個動作分開寫,看不出整體意圖:
// 需求:從員工清單取得 IT 部門、按薪資降冪、只取姓名
List<Employee> filtered = new List<Employee>();
foreach (var emp in employees)
{
if (emp.Department == "IT")
filtered.Add(emp);
}
filtered.Sort((a, b) => b.Salary.CompareTo(a.Salary));
List<string> names = new List<string>();
foreach (var emp in filtered)
names.Add(emp.Name);
// 12 行,邏輯分散、難以一眼看出意圖
2.2 LINQ 的本質:宣告式程式設計
📖 官方定義:「LINQ 提供一致的模型,讓您使用各種資料來源和格式的資料。」
— Microsoft Docs: LINQ 概觀
LINQ 的核心思維轉換是從命令式(告訴電腦怎麼做)到宣告式(告訴電腦要什麼):
// 命令式(舊):告訴電腦「怎麼做」
var result = new List<string>();
foreach (var emp in employees)
if (emp.Department == "IT")
result.Add(emp.Name);
// 宣告式(LINQ):告訴電腦「要什麼」
var result = employees
.Where(e => e.Department == "IT")
.Select(e => e.Name)
.ToList();
LINQ 能查 List、DataTable、XML 甚至資料庫,原因是它建立在兩個介面上:
IEnumerable<T>— LINQ to Objects(記憶體集合)IQueryable<T>— LINQ to SQL(EF Core 等,可翻譯成 SQL 查詢)
任何實作 IEnumerable<T> 的型別都可以使用 LINQ。這就是為什麼 DataTable.AsEnumerable() 之後就能用 LINQ — 它把 DataTable 轉成了可迭代的序列。
2.3 Lambda 表達式:可傳遞的邏輯片段
LINQ 方法接受的 x => x.Age > 30 叫做 Lambda 表達式,本質是「一段可以當參數傳遞的程式邏輯」:
// Lambda 的完整形式
(Employee e) => { return e.Department == "IT"; }
// 簡化形式(型別可推斷、單行可省略 return)
e => e.Department == "IT"
// 傳入 LINQ — 這裡就是在傳遞「篩選邏輯」給 Where 方法
employees.Where(e => e.Department == "IT")
2.4 核心機制:延遲執行(Deferred Execution)
LINQ 查詢不是你寫下那一刻執行的,而是在你取用結果時才執行:
var query = employees.Where(e => e.IsActive);
// 此時:沒有任何迴圈跑過,只是建立了「查詢描述」
foreach (var e in query) { } // ← 這裡才真正執行
var list = query.ToList(); // ← 或這裡
延遲執行的陷阱:同一個 query 用多次,就跑多次迴圈。
// ❌ 效能陷阱:每次使用 query 都會重跑一次迴圈
var query = dt.AsEnumerable().Where(r => r.Field<string>("STATUS") == "A");
var count = query.Count(); // 跑一次
var first = query.First(); // 再跑一次
// ✅ 先具體化(materialize),再使用
var list = dt.AsEnumerable()
.Where(r => r.Field<string>("STATUS") == "A")
.ToList(); // 只跑一次
var count = list.Count; // O(1)
var first = list[0]; // O(1)
2.5 常用方法(以「解決什麼問題」為主軸)
「我需要篩選」→ Where
var activeRows = dt.AsEnumerable()
.Where(r => r.Field<string>("STATUS") == "A");
「我需要轉換格式」→ Select
// 只要欄位值,不要整個 DataRow
var fundCodes = dt.AsEnumerable()
.Select(r => r.Field<string>("FUND_NO"))
.Distinct()
.ToList();
// 結果:List<string>,而不是 IEnumerable<DataRow>
「我需要統計」→ Count / Sum / Average
int activeCount = dt.AsEnumerable()
.Count(r => r.Field<string>("STATUS") == "A");
decimal total = dt.AsEnumerable()
.Sum(r => r.Field<decimal>("AMOUNT"));
「我需要分組統計」→ GroupBy
var byFund = dt.AsEnumerable()
.GroupBy(r => r.Field<string>("FUND_NO"))
.Select(g => new {
FundNo = g.Key,
TotalAmount = g.Sum(r => r.Field<decimal>("AMOUNT")),
Count = g.Count()
});
「我需要快速查找」→ ToDictionary
// ❌ 每次都 LINQ 查,O(n)
var name = configs.First(c => c.Code == targetCode).Name;
// ✅ 先轉成 Dictionary,之後查詢 O(1)
var configDict = dt.AsEnumerable()
.ToDictionary(
r => r.Field<string>("CODE"),
r => r.Field<string>("NAME")
);
string name = configDict["TARGET_CODE"];
「我需要驗證」→ Any / All
// 確認所有必填欄位都有值
string[] required = { "FUND_NO", "YEAR", "NAME" };
bool allFilled = required.All(f => !string.IsNullOrEmpty(row[f]?.ToString()));
「我需要取特定一筆」→ First / Single / FirstOrDefault
| 方法 | 找不到時 | 找到多筆時 | 使用時機 |
|---|---|---|---|
First | 拋例外 | 回傳第一筆 | 確定有資料,要第一筆 |
FirstOrDefault | 回傳 null | 回傳第一筆 | 不確定有沒有資料 |
Single | 拋例外 | 拋例外 | 業務上保證唯一一筆 |
SingleOrDefault | 回傳 null | 拋例外 | 可能不存在,但不允許多筆 |
IFA 建議:查主鍵資料用 SingleOrDefault(多筆代表資料異常,應該報錯);查條件資料用 FirstOrDefault。
2.6 在 IFA 後端的實際應用
IFA 後端使用 DataTable + 原生 SQL,LINQ 主要用於記憶體內的資料後處理。
情境 A:Init — 整理下拉選單資料
DataTable dt = l_dbc.FillDataTable(sql, ht);
var dropdownItems = dt.AsEnumerable()
.Where(r => r.Field<string>("STATUS") != "D")
.OrderByDescending(r => r.Field<string>("YEAR"))
.Select(r => new {
Code = r.Field<string>("FUND_NO"),
Name = r.Field<string>("FUND_NAME")
})
.ToList();
情境 B:Searched 後處理 — 欄位顯示轉換
void l_Master_Searched(object sender, SearchedEventArgs e)
{
// 取出所有不重複的年度,供篩選條件使用
var years = e.masterData.AsEnumerable()
.Select(r => r.Field<string>("YEAR"))
.Distinct()
.OrderByDescending(y => y)
.ToList();
}
情境 C:存檔前驗證 — 動態必填檢查
string[] required = new[] { "FUND_NO", "YEAR", "CODE", "NAME" };
var emptyFields = required
.Where(f => string.IsNullOrEmpty(newRow[f]?.ToString()))
.ToList();
if (emptyFields.Any())
{
e.cancel = true;
e.cancelMsg = $"以下欄位不可為空:{string.Join("、", emptyFields)}";
}
情境 D:動態組裝 WHERE 條件字串
string[] keyFields = new[] { "FUND_NO", "YEAR", "SEQ_NO" };
string whereClause = string.Join(" AND ",
keyFields.Select(k => $"M.{k} = @{k}"));
// 結果:"M.FUND_NO = @FUND_NO AND M.YEAR = @YEAR AND M.SEQ_NO = @SEQ_NO"
2.7 常見陷阱
| 陷阱 | 錯誤 | 正確 |
|---|---|---|
| 多次 enumerate | query.Count(); query.First(); | 先 .ToList() 再使用 |
| DataTable 忘記 AsEnumerable | dt.Where(...) | dt.AsEnumerable().Where(...) |
| null 欄位沒處理 | r.Field<string>("COL") 可能是 null | 搭配 ?.ToString() 或 null check |
| GroupBy 後忘記投影 | 直接用 g,拿到的是 IGrouping | .Select(g => new { g.Key, ... }) |
三、整合應用:非同步 + LINQ
3.1 概念整合
在 IFA 後端,非同步負責「不阻塞執行緒」,LINQ 負責「資料後處理」,兩者分工清晰:
public async Task<IActionResult> Search([FromBody] SearchClass searchData)
{
// 非同步查詢 → 不阻塞執行緒
var result = await doSearchResultAsync(searchData);
// LINQ 後處理 → 記憶體內篩選與轉換
var processed = result.AsEnumerable()
.Where(r => r.Field<string>("STATUS") != "D")
.Select(r => new {
FundNo = r.Field<string>("FUND_NO"),
Year = ShareModel.dispYear(r.Field<string>("YEAR"))
})
.ToList();
return Ok(processed);
}
3.2 IFA 未來升級方向
KSI Kernel 目前使用同步模式。當框架升級支援非同步 API 時,Init Action 將可同時發出多個下拉查詢:
public async Task<IActionResult> Init()
{
// 同時發出三個查詢,不依序等待
var t1 = GetDropdownAsync("QL_YEAR");
var t2 = GetDropdownAsync("QL_FUND");
var t3 = GetMasterDataAsync();
await Task.WhenAll(t1, t2, t3);
// LINQ 整理下拉清單
var years = t1.Result.AsEnumerable()
.Where(r => r.Field<string>("STATUS") == "A")
.Select(r => r.Field<string>("YEAR"))
.Distinct()
.ToList();
return Ok(new { years, funds = t2.Result, master = t3.Result });
}
四、學習心得總結
關鍵收穫:
非同步不等於多執行緒
async/await 是關於「不浪費等待時間」
LINQ 是思維方式的轉變
從「怎麼做」變成「要什麼」的宣告式程式設計
延遲執行是雙面刃
理解 IEnumerable 的延遲特性可以避免效能陷阱
實作比理論重要
DataTable + LINQ 的組合是最常見的應用場景
後續學習方向(Q2)
- C# ASP.NET Core API 開發規範
- SQL Server SP 撰寫準則
- EF Core(選修)