一、非同步程式設計(async/await)

1.1 基礎概念

什麼是非同步?

同步程式碼像排隊買飲料 — 每個人必須等前一個人買完才能點餐。非同步則像餐廳點餐 — 你點完餐後可以先做別的事,餐點準備好了再通知你。

csharp
// 同步:執行緒被阻塞,等 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)— 承諾未來會完成並給你結果。

csharp
// Task:不回傳值的非同步操作
Task SaveLogAsync()
{
    await db.ExecuteAsync("INSERT INTO LOG ...");
}

// Task<T>:回傳值的非同步操作
Task<DataTable> GetDataAsync()
{
    return await db.FillDataTableAsync(sql, ht);
}

Task 的三種狀態:

Created Running Completed(成功)/ Faulted(例外)/ Canceled(取消)

1.3 async/await 語法

基本規則:

csharp
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 推薦的非同步設計模式。

常見陷阱:

1.5 在 IFA 後端的實際應用

IFA 的 KSI Kernel 框架目前使用同步模式。理解非同步概念對以下場景至關重要:

二、LINQ(Language Integrated Query)

2.1 問題意識:LINQ 出現前,開發者在痛苦什麼?

在 LINQ 出現前(C# 2.0 以前),篩選、排序、聚合都要手動寫迴圈。問題不只是「程式碼長」,而是邏輯分散 — 篩選、排序、投影三個動作分開寫,看不出整體意圖:

csharp — LINQ 之前
// 需求:從員工清單取得 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 的核心思維轉換是從命令式(告訴電腦怎麼做)到宣告式(告訴電腦要什麼):

csharp — 思維對比
// 命令式(舊):告訴電腦「怎麼做」
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。這就是為什麼 DataTable.AsEnumerable() 之後就能用 LINQ — 它把 DataTable 轉成了可迭代的序列。

2.3 Lambda 表達式:可傳遞的邏輯片段

LINQ 方法接受的 x => x.Age > 30 叫做 Lambda 表達式,本質是「一段可以當參數傳遞的程式邏輯」:

csharp
// 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 查詢不是你寫下那一刻執行的,而是在你取用結果時才執行:

csharp
var query = employees.Where(e => e.IsActive);
// 此時:沒有任何迴圈跑過,只是建立了「查詢描述」

foreach (var e in query) { }  // ← 這裡才真正執行
var list = query.ToList();     // ← 或這裡

延遲執行的陷阱:同一個 query 用多次,就跑多次迴圈。

csharp
// ❌ 效能陷阱:每次使用 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

csharp
var activeRows = dt.AsEnumerable()
    .Where(r => r.Field<string>("STATUS") == "A");

「我需要轉換格式」→ Select

csharp
// 只要欄位值,不要整個 DataRow
var fundCodes = dt.AsEnumerable()
    .Select(r => r.Field<string>("FUND_NO"))
    .Distinct()
    .ToList();
// 結果:List<string>,而不是 IEnumerable<DataRow>

「我需要統計」→ Count / Sum / Average

csharp
int activeCount = dt.AsEnumerable()
    .Count(r => r.Field<string>("STATUS") == "A");

decimal total = dt.AsEnumerable()
    .Sum(r => r.Field<decimal>("AMOUNT"));

「我需要分組統計」→ GroupBy

csharp
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

csharp
// ❌ 每次都 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

csharp
// 確認所有必填欄位都有值
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 — 整理下拉選單資料

csharp
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 後處理 — 欄位顯示轉換

csharp
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:存檔前驗證 — 動態必填檢查

csharp
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 條件字串

csharp
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 常見陷阱

陷阱錯誤正確
多次 enumeratequery.Count(); query.First();.ToList() 再使用
DataTable 忘記 AsEnumerabledt.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 負責「資料後處理」,兩者分工清晰:

csharp — 非同步取得資料 + 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 將可同時發出多個下拉查詢:

csharp — Task.WhenAll 並行下拉查詢
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 });
}

四、學習心得總結

關鍵收穫:

1

非同步不等於多執行緒

async/await 是關於「不浪費等待時間」

2

LINQ 是思維方式的轉變

從「怎麼做」變成「要什麼」的宣告式程式設計

3

延遲執行是雙面刃

理解 IEnumerable 的延遲特性可以避免效能陷阱

4

實作比理論重要

DataTable + LINQ 的組合是最常見的應用場景

後續學習方向(Q2)

  • C# ASP.NET Core API 開發規範
  • SQL Server SP 撰寫準則
  • EF Core(選修)