🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

引言

C# 是一門快速發展的語言。10 年前的「理所當然」,現在可能因為保守性或性能的原因而不再被推薦。

本次將為涉及保養舊系統或分階段重構的開發者,整理出常見的「舊寫法」到「當前推薦」的替換模式,提供可立即實作的程式碼範本集

在進入 C# 開發現場時,除了新開發外,想到有時會看到意外舊的程式碼。過去傳承下來的系統仍然存在,且有如十年前的系統仍然運行良好。而且開發者也不會輕易改變寫法,可能是因為在專案中的統一性。然而,程式碼確實在不斷演進。希望大家能記住「現在和過去的程式碼」的差異。

處理別清單

這次的內容相當長,因此可以從列表中直接跳轉到感興趣的項目。

No. 類別 模式 連結
1 異步處理 Begin/End/BackgroundWorker → async/await 詳細
2 異步處理 HttpWebRequest/WebClient → HttpClient
3 集合 ArrayList/Hashtable → 泛型集合 詳細
4 資源管理 使用 try/finally 進行 Dispose → 使用/await 使用
5 LINQ 程序性迴圈處理 → LINQ(適材適所) 詳細
6 字串處理 String.Format/連結 → 字串插值
7 樣式 is + 型別轉換 → 樣式匹配
8 控制流程 switch 陳述式 → switch 表達式
9 DTO DTO 類冗長 → record/with 詳細
10 日期時間處理 DateTime.Now 濫用 → DateTimeOffset.UtcNow
11 序列化器 BinaryFormatter/舊序列化器 → System.Text.Json 詳細
12 執行緒管理 直接使用 Thread → Task/ThreadPool 詳細
13 計時器 多用 Timer → PeriodicTimer (.NET 6+)
14 數據層 ADO.NET 同步 API → 非同步/輕量 ORM 詳細
15 隨機數生成 隨機使用 Random → RandomNumberGenerator
16 異常處理 異常後處理 → 異常過濾 詳細
17 Null 檢查 到處是 if (obj != null) → ?. / ??= / 可為空 詳細
18 屬性 手寫屬性 → 自動實作/表達式主體成員 詳細
19 實例化 顯示使用 new Type() → 目標型 new 詳細
20 數據複製 手動複製 → with/解構(ValueTuple) 詳細
21 設定管理 app.config/Web.config → appsettings.json + Options 詳細
22 日誌輸出 Trace.WriteLine → ILogger
W1 WinForms 跨執行緒 UI 更新 → async/await 返回 UI 詳細
W2 WinForms BackgroundWorker → Task + IProgress + CancellationToken
W3 WinForms Application.DoEvents() → 不推薦。分割 + await
W4 WinForms .Result/.Wait() 引起死鎖 → 全部 await
W5 WinForms 計時器選擇:Forms.Timer 優先 / PeriodicTimer 併用
W6 WinForms 確保於 GDI+ 資源(Pen/Brush/Bitmap/Graphics)中正確 Dispose
W7 WinForms Flicker 對策:DoubleBuffered + 集中 OnPaint
W8 WinForms 對話框後的繁重處理:同步 → 非同步
W9 WinForms 按鈕多重啟動:標誌模糊 → Interlocked 或 SemaphoreSlim
W10 WinForms ConfigureAwait(false) 的誤用 → 在 UI 層禁止
W11 WinForms 表單結束時的任務清理:放置 → 取消 & await 完成
W12 WinForms 高 DPI:預設交給 → 明確設定

技術別指導

每個項目的結構如下:

【Before】 舊的寫法
【After】 推薦的寫法
【為什麼】 改進的好處
【遷移備忘錄】 實作時的注意事項或陷阱

異步處理相關

UI 凍結讓人煩躁......舊的回呼地獄複雜且不易除錯。通過移行到 async/await,可以在保持 UI 響應的同時,統一處理異常。

1) 異步:Begin/End/BackgroundWorker → async/await

【Before】回呼地獄

var req = (HttpWebRequest)WebRequest.Create(url);
req.BeginGetResponse(ar =>
{
    var res = req.EndGetResponse(ar);
    // 處理...
}, null);

【After】使用 async/await 直線化

using var client = new HttpClient();
var json = await client.GetStringAsync(url);

為什麼

  • 異步處理在外觀上與同步代碼無異,可讀性大幅提升
  • 堆疊追蹤變得明確,易於除錯
  • 異常處理統一(傳統的回呼方式需要在 EndGetResponse 中捕捉異常)

遷移備忘錄

  • 返回值型別更改為 TaskTask<T>
  • UI 層禁止使用同步區塊(.Result.Wait()

2) HttpWebRequest/WebClient → HttpClient (+ IHttpClientFactory)

【Before】舊的異步 API

using var wc = new WebClient();
string s = wc.DownloadString(url); // 同步方法

【After】HttpClient + DI

// 在 Startup.cs / Program.cs 中
// 處理雲端/容器環境的 DNS 變更
services.AddHttpClient("default")
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(5),    // 追隨 DNS 更新
        PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)   // 鬆弛的連接棄用
    });

// 使用範例
public class MyService
{
    private readonly IHttpClientFactory _factory;
    public MyService(IHttpClientFactory factory) => _factory = factory;

    public async Task<string> FetchDataAsync(string url)
    {
        var http = _factory.CreateClient("default");
        return await http.GetStringAsync(url);
    }
}

為什麼

  • 通過重用連接,防止 TIME_WAIT 狀態的套接字堆積
  • 可統一管理超時、重試策略等
  • 測試時易於替換模擬
  • DNS 緩存得到有效利用

遷移備忘錄

  • 建議以靜態或單例的形式共享 HttpClient
  • .NET Core/.NET 5+ 中利用 IHttpClientFactory
  • 雲端/容器環境中若頻繁進行 IP 旋轉,需設置 PooledConnectionLifetime,以防止連接固定於舊的目的地(若不設置則連接會長期存在,無法跟隨 DNS 更新)
  • 舊有的 WebClient 在 .NET 6+ 中事實上已過時,必須移行至 HttpClient

3) ArrayList/Hashtable → 泛型集合

【Before】需要型別轉換,無型別安全

var list = new ArrayList();
list.Add(1);
list.Add("hello"); // 即便混合仍不會有編譯錯誤

int x = (int)list[0];  // 包裝 + 型別轉換

【After】型別安全且性能提升

var list = new List<int> { 1 };
int x = list[0]; // 無需型別轉換,型別安全

// 若需要混合鍵,則需明確指定
var mixed = new List<object> { 1, "hello" };

為什麼

  • 型別安全的優勢,減少運行時錯誤
  • 無需打包/解包,性能提升
  • 與 LINQ 的兼容性高

遷移備忘錄

  • 統一用 Dictionary<TKey, TValue> 進行替換
  • 若舊代碼中使用 Hashtable,因可能隱含著計數值等,故需增加測試的充實性

4) 使用 try/finally 進行 Dispose → 使用/await 使用

【Before】手動調用 Dispose

var fs = new FileStream(path, FileMode.Open);
try
{
    var data = fs.ReadByte();
}
finally 
{ 
    fs?.Dispose();  // 需明確調用
}

【After】使用聲明(同步)

using var fs = new FileStream(path, FileMode.Open);
var data = fs.ReadByte(); 
// 離開範圍時自動 Dispose

當為異步時則使用 await using

await using var fs = new FileStream(
    path, FileMode.Open, FileAccess.Read, 
    FileShare.Read, bufferSize: 4096, 
    options: FileOptions.Asynchronous);

// 推薦 .NET 6+:使用 Memory<byte> + CancellationToken
var buffer = new byte[1024];
int read = await fs.ReadAsync(buffer.AsMemory(), CancellationToken.None);

// 或使用舊的重載(當需兼容 .NET Framework 時)
int read2 = await fs.ReadAsync(buffer, 0, 1024);

為什麼

  • 確保防止資源洩漏
  • 針對異步 I/O 時進行最佳化
  • IAsyncDisposable 被正確實作,await using 將確保有效清理

遷移備忘錄

  • 對於實作 IAsyncDisposable 的型別應用 await using
  • 在 .NET Framework 中並不支持 IAsyncDisposable,因此需 .NET Core 3.0 以上

5) 程序性迴圈處理 → LINQ(適材適所)

【Before】命令式編程

var results = new List<int>();
foreach (var n in nums)
{
    if (n % 2 == 0) 
        results.Add(n * n);
}

【After】函數式編程

var results = nums
    .Where(n => n % 2 == 0)
    .Select(n => n * n)
    .ToList();

為什麼

  • 處理意圖明確(過濾 → 映射)
  • 無需臨時列表

遷移備忘錄

  • 需在確定前保持延遲列舉(在 ToList() 確定前),以降低 GC 壓力
  • 在關鍵路徑檢查瓶頸強烈建議
  • 大型數據集可考慮使用 Span<T>PLINQ/Parallel.For
  • 當使用 LINQ-to-Entities 時,需注意列舉時間(避免 N+1 查詢)

6) String.Format/連結 → 字串插值

【Before】難以閱讀且難以維護

var s = string.Format("{0} - {1}: {2}", id, name, status);
var s2 = id + " - " + name + ": " + status; // GC 壓力高
var f1 = string.Format("{0:D3}", value); // 格式化示例(010)

【After】使用字串插值更為明確

var s = $"{id} - {name}: {status}";
var f2 = $"{value:D3}"; // 格式化示例(010)

為什麼

  • 更具可讀性,減少鍵入錯誤
  • 內部經過有效優化

遷移備忘錄

  • 在迴圈內進行大量連結時持有 StringBuilder
  • 格式化指定(:D3 等)也可在插值內使用:$"{value:D3}"

7) is + 型別轉換 → 樣式匹配

【Before】在轉換前後檢查相同型別

if (obj is Customer)
{
    var c = (Customer)obj;
    Console.WriteLine(c.Name);
}

【After】使用樣式匹配一次到位

if (obj is Customer c)
{
    Console.WriteLine(c.Name);
}

為什麼

  • 減少程式碼行數
  • 不會發生型別轉換失敗的錯誤

遷移備忘錄

  • 結合 switch 表達式,可以合併型別檢查和分支
  • 可在 when 子句中添加條件:if (obj is Customer c when c.Age > 18)

8) switch 陳述式 → switch 表達式

【Before】冗長且易漏掉 break

string msg;
switch (status)
{
    case 0: msg = "OK"; break;
    case 1: msg = "Warn"; break;
    default: msg = "NG"; break;
}

【After】宣告性且簡潔

string msg = status switch 
{ 
    0 => "OK", 
    1 => "Warn", 
    _ => "NG" 
};

為什麼

  • 無 break 漏掉的 Bug
  • 提供網羅性檢查的編譯器支持
  • 複雜條件也能可讀

遷移備忘錄

  • 如需將多個案例彙總為一個結果:0 or 1 or 2 => "Low"
  • 可結合樣式:Customer { Age: > 18 } => "Adult"

DTO・物件相關

使用 DateTime.Now 會造成時區問題。此外,冗餘的 DTO 定義對變更的韌性差,始終存在漏修的風險。使用 record 和 DateTimeOffset,可實現更堅固且簡潔的設計。

9) DTO 類冗長 → record/with

【Before】多次手寫屬性

public class User 
{ 
    public string Name { get; set; } 
    public int Age { get; set; } 
}

// 用途
var user = new User { Name = "Alice", Age = 30 };

【After】使用 record 簡潔化

public record User(string Name, int Age);

// 用途(相同)
var user = new User("Alice", 30);

// 使用 with 進行不變的複製
var user2 = user with { Age = 31 };

為什麼

  • 減少冗長的樣板程式碼
  • 預設使用值相等(相同值則相等)
  • 最適合不變的作業流程

遷移備忘錄

  • 針對基於引用相等的代碼(如列表內的相同引用檢查等)需謹慎對待
  • 可使用 record classrecord struct 來區分引用型 / 值型

10) DateTime.Now 濫用 → DateTimeOffset.UtcNow

【Before】本地時間的陷阱

var now = DateTime.Now;  // 夏令時間或時區變更會導致意外行為

【After】明確使用 UTC

var now = DateTimeOffset.UtcNow;  // 始終使用 UTC
// 在顯示時轉換為本地時間
Console.WriteLine(now.ToLocalTime());

為什麼

  • 時區的處理變得明確
  • 減少夏令時間和DST(夏令時間)的陷阱
  • 在日誌或數據庫中存儲時保持一致性

遷移備忘錄

  • 伺服器應用應使用 UTC 統一,僅在顯示時才轉換為本地
  • 推薦使用 DateTimeOffset.UtcNow 而非 DateTime.UtcNow(以保持時區信息)
  • 保存到 SQL Server:若現有結構為 datetime / datetime2,則在保存時傳遞 .UtcDateTime,或新建結構考慮使用 datetimeoffset 型別

序列化器・資源相關

BinaryFormatter 將被淘汰,且伴隨有安全風險。此外,資源洩漏可能導致未檢測的句柄耗盡,進而使應用突然崩潰。養成使用 using/await using 進行管理的習慣是至關重要的。

11) BinaryFormatter/舊序列化器 → System.Text.Json

【Before】高危險且難以維護

// BinaryFormatter 不推薦(有安全風險)
// 使用 Newtonsoft.Json 的情況
var json = JsonConvert.SerializeObject(obj);

【After】使用標準的 System.Text.Json

var json = JsonSerializer.Serialize(obj);
var obj2 = JsonSerializer.Deserialize<MyType>(json);

// 設定選項
var options = new JsonSerializerOptions 
{ 
    PropertyNameCaseInsensitive = true,
    WriteIndented = true
};
var formatted = JsonSerializer.Serialize(obj, options);

為什麼

-安全且無遠端代碼執行的漏洞

  • 依據 .NET 標準性能高效
  • 作為標準庫,減少依賴

遷移備忘錄

  • 命名規則的差異(重要):兩者都預設會直接輸出 CLR 名稱(不是 camelCase)。
    • 為了在 System.Text.Json 中使用 camelCase:JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    • 若使用 Newtonsoft.Json 使用 camelCase:則需顯式設定 CamelCasePropertyNamesContractResolver
    • 若現有結構為 PascalCase,而 JSON API 能接受 camelCase 存取,則可使用 PropertyNameCaseInsensitive = true 進行對應
  • 循環參考:預設將失敗。可在 JsonSerializerOptions 中設定 ReferenceHandler = ReferenceHandler.Preserve
  • 多態(繼承):.NET 7+ 透過 [JsonPolymorphic]/[JsonDerivedType] 支持。舊代碼則可能需重構 DTO 設計
  • 私有 Setter/構造函數:相較於 Newtonsoft.Json 可能有更嚴格的限制,應考慮使用 [JsonConstructor] 或改成 public
  • 使用 [JsonPropertyName] 自訂 JSON 鍵

執行緒・並行處理相關

直接操作 Thread 是死鎖與內存洩漏的溫床,且複雜性會增加。統一使用 Task 及異步 API,將提升擴展性,且除錯也更為簡單。

12) 直接使用 Thread → Task/ThreadPool

【Before】手動管理執行緒

var th = new Thread(Work) { IsBackground = true };
th.Start();
// ...
th.Join(); // 等待

【After】統一使用 Task

await Task.Run(Work);

為什麼

  • 利用 ThreadPool 的優勢,自動擴展
  • 整合取消與異常處理
  • 與 async/await 保持一致性

遷移備忘錄

  • 對於長期背景任務,建議考慮使用 IHostedService (ASP.NET Core)
  • 利用 CancellationToken 實現平滑關閉

13) 多用 Timer → PeriodicTimer (.NET 6+)

【Before】基於事件的錯誤處理困難

var timer = new System.Timers.Timer(1000);
timer.Elapsed += (s, e) => DoWork();
timer.Start();

【After】使用 PeriodicTimer 支持異步

var cts = new CancellationTokenSource();
var pt = new PeriodicTimer(TimeSpan.FromSeconds(1));

_ = RunPeriodicAsync(pt, cts.Token);

async Task RunPeriodicAsync(PeriodicTimer timer, CancellationToken ct)
{
    try
    {
        while (await timer.WaitForNextTickAsync(ct))
        {
            await DoWorkAsync();
        }
    }
    finally
    {
        await timer.DisposeAsync();
    }
}

為什麼

  • 支持異步工作流程
  • 明確處理取消
  • 異常更易於引發且更易處理

遷移備忘錄

  • 只在 .NET 6.0 及以上版本可用
  • 在 UI 框架(WinForms)中仍需使用 System.Windows.Forms.Timer

14) ADO.NET 同步 API → 非同步/輕量 ORM

【Before】阻塞執行緒

using var cmd = new SqlCommand(sql, conn);
using var r = cmd.ExecuteReader();  // 同步,阻塞執行緒
while (r.Read()) { /* 行處理 */ }

【After】非同步等待

using var cmd = new SqlCommand(sql, conn);
using var r = await cmd.ExecuteReaderAsync();  // 非同步獲取結果
while (await r.ReadAsync())  // 行迴圈
{
    // 逐行處理
}

// 只有當有多個結果集(多個 SQL 陳述或 MARS)時:
while (await r.NextResultAsync())
{
    while (await r.ReadAsync()) { /* ... */ }
}

更進一步使用 Dapper 等輕量 ORM

using var connection = new SqlConnection(connStr);
var users = await connection.QueryAsync<User>("SELECT * FROM Users");

為什麼

  • 更有效利用執行緒
  • 防止大量連接時執行緒耗盡
  • 使用 Dapper 則 SQL 對映具備型別安全且簡潔

遷移備忘錄

  • 也可考慮使用 Entity Framework Core,但若已有的 ADO.NET 代碼則 Dapper 是最小改動的選擇
  • 移行至非同步整體需同步進行

15) 隨機使用 Random → RandomNumberGenerator

【Before】可預測

var rnd = new Random();
var val = rnd.Next();  // 隨機種子基於當前時間

【After】不可預測且安全

// 建議使用 .NET 6+:GetInt32() 簡潔且安全
int val = RandomNumberGenerator.GetInt32(int.MaxValue);        // [0, int.MaxValue)
int dice = RandomNumberGenerator.GetInt32(1, 7);               // [1, 6]

// 傳統的 GetBytes() 方法(兼容 .NET Framework 等舊版本)
var bytes = RandomNumberGenerator.GetBytes(4);
int val_legacy = BitConverter.ToInt32(bytes, 0) & int.MaxValue;

// 若為非安全用處則使用 Random.Shared (.NET 6+)
int val2 = Random.Shared.Next();

為什麼

  • 加密安全
  • 防止多執行緒環境下的碰撞
  • 應對關鍵生成等安全需求的場景

遷移備忘錄

  • 對於簡單抽樣可使用 Random.Shared 即可
  • 在生成密鑰/令牌時選擇 RandomNumberGenerator

16) 異常後處理 → 異常過濾

【Before】條件分支冗長

try { /*...*/ }
catch (IOException ex)
{
    if (!IsTransient(ex)) throw;
    Log(ex);
}

【After】過濾器清楚表達意圖

try { /*...*/ }
catch (IOException ex) when (IsTransient(ex))
{
    Log(ex);
}

為什麼

  • 利用過濾器判定是否處理異常
  • 未處理的異常會自動往上傳播
  • 堆疊保持不變,便於除錯

遷移備忘錄

  • 過濾器理想中應無副作用(狀態變更)
  • 若已有副作用如日誌輸出等則應在 catch 區塊中處理

17) 到處是 if (obj != null) → ?. / ??= / Nullable Reference Types

【Before】冗長,Null 檢查漏掉的風險

if (user != null) 
    name = user.Name;

if (config == null)
    config = DefaultConfig();

【After】使用運算子簡潔化

name = user?.Name;  // 若 user 為 null 則返回 null

config ??= DefaultConfig();  // 僅在為 null 時賦值

接著導入 Nullable Reference Types

#nullable enable
public string? GetUserName(User? user)
{
    return user?.Name;  // 確保型別安全
}

為什麼

  • 減少 Null 參考異常
  • 在編譯階段進行 null 安全檢查
  • 增強可讀性

遷移備忘錄

  • #nullable enable 可逐步導入(按檔案單位)
  • 注意與現有代碼的兼容性

18) 手寫屬性 → 自動實作/表達式本體成員

【Before】樣板代碼冗長

private int _x;
public int X 
{ 
    get { return _x; } 
    set { _x = value; } 
}

【After】使用自動實作縮短

public int X { get; set; }

// 若需只讀則使用 init
public int Y { get; init; }

// 使用表達式本體成員
public override string ToString() => $"X={X}, Y={Y}";

為什麼

  • 減少需要維護的代碼
  • 若無需驗證邏輯則自動實作已足夠

遷移備忘錄

  • init 訪問器僅在初始化時可設定,表現不變性
  • 方法也可用表達式本體縮短: public void Log() => Console.WriteLine(X);

19) 明示使用 new Type() → 目標型 new

【Before】型別需重複書寫兩次

List<string> list = new List<string>();
Dictionary<int, string> dict = new Dictionary<int, string>();

【After】型別推斷以降低重複

List<string> list = new();
Dictionary<int, string> dict = new();

為什麼

  • 減少無意義噪聲,使閱讀更易
  • IDE 支持已逐步增強

遷移備忘錄

  • 若上下文不明確,則建議不要省略以提高可讀性
  • 判斷可依開發團隊的編碼規範

20) 手動複製 → with/解構(ValueTuple)

【Before】逐個手動賦值屬性

var u2 = new User 
{ 
    Name = u1.Name, 
    Age = u1.Age, 
    Email = u1.Email 
};

【After】使用 with 變更指定的欄位

var u2 = u1 with { Age = u1.Age + 1 };

使用 ValueTuple 進行解構

var (min, max) = GetRange();

為什麼

  • 減少樣板代碼
  • 保持不變性

遷移備忘錄

  • 僅限於 record 型別的功能

21) 設定:app.config/Web.config → appsettings.json + Options

【Before】XML 階層結構難以構建

<configuration>
  <appSettings>
    <add key="ApiKey" value="secret"/>
    <add key="ApiUrl" value="https://api.example.com"/>
  </appSettings>
</configuration>

【After】使用 JSON 以便階層及環境分開

{
  "Api": { 
    "Key": "secret", 
    "Url": "https://api.example.com" 
  },
  "Logging": { "Level": "Information" }
}
// 在 Startup.cs / Program.cs
services.Configure<ApiOptions>(config.GetSection("Api"));

// 使用側
public class ApiService
{
    private readonly ApiOptions _opts;
    public ApiService(IOptions<ApiOptions> opts) => _opts = opts.Value;
}

為什麼

  • 階層結構自然
  • 可獨立的環境有 appsettings.Development.json
  • 具型別安全

遷移備忘錄

  • 用戶敏感信息(API金鑰等)應使用 Secret Manager 或 Azure Key Vault 進行管理

22) 日誌:Trace.WriteLine → ILogger

【Before】基於文本搜尋困難

Trace.WriteLine($"Start Processing {id}");

【After】結構化日誌可機械讀取

_logger.LogInformation("Start Processing {Id}", id);

為什麼

  • 以 JSON 格式進行結構化日誌輸出
  • 可與 ElasticSearch/Splunk 等收集基礎設施集成
  • 使用占位符 {Id} 進行屬性化設計

遷移備忘錄

  • ASP.NET Core 通過 DI 自動注入 ILogger<T>
  • 對於現有的 .NET Framework,建議引入 Serilog

WinForms 特化區段

GUI 特有的陷阱(跨執行緒例外、GDI+ 資源洩漏、閃爍)若不加以處理,應用將不穩定,並失去用戶的信賴。這些對策是使應用長久受愛戴的基礎。

W1) 跨執行緒 UI 更新:InvokeRequired 地獄 → async/await 返回 UI

【Before】重複檢查 Invoke

if (InvokeRequired)
    Invoke(new Action(() => label1.Text = text));
else
    label1.Text = text;

【After】使用 async/await 自動返回 UI

private async void btnRun_Click(object sender, EventArgs e)
{
    btnRun.Enabled = false;
    try
    {
        var data = await Task.Run(LoadBigDataAsync);
        label1.Text = data.Summary;  // await 後在 UI 執行緒中執行
    }
    finally
    {
        btnRun.Enabled = true;
    }
}

為什麼

  • await 預設會捕獲 UI 的 SynchronizationContext
  • 自動返回至 UI 執行緒,所以不再需要 Invoke
  • 代碼更簡潔,避免死鎖

遷移備忘錄

  • 在 WinForms UI 層中勿使用 ConfigureAwait(false)
  • 在庫層中使用 ConfigureAwait(false) 以放棄 UI 上下文

原文出處:https://qiita.com/Sakai_path/items/3edc7a0a8db4e43753c8


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝28   💬4   ❤️7
840
🥈
我愛JS
📝2   💬8   ❤️2
113
🥉
御魂
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付