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 響應的同時,統一處理異常。
var req = (HttpWebRequest)WebRequest.Create(url);
req.BeginGetResponse(ar =>
{
var res = req.EndGetResponse(ar);
// 處理...
}, null);
using var client = new HttpClient();
var json = await client.GetStringAsync(url);
為什麼
EndGetResponse 中捕捉異常)遷移備忘錄
Task 或 Task<T>.Result、.Wait())using var wc = new WebClient();
string s = wc.DownloadString(url); // 同步方法
// 在 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 狀態的套接字堆積遷移備忘錄
HttpClient.NET Core/.NET 5+ 中利用 IHttpClientFactoryPooledConnectionLifetime,以防止連接固定於舊的目的地(若不設置則連接會長期存在,無法跟隨 DNS 更新)WebClient 在 .NET 6+ 中事實上已過時,必須移行至 HttpClientvar list = new ArrayList();
list.Add(1);
list.Add("hello"); // 即便混合仍不會有編譯錯誤
int x = (int)list[0]; // 包裝 + 型別轉換
var list = new List<int> { 1 };
int x = list[0]; // 無需型別轉換,型別安全
// 若需要混合鍵,則需明確指定
var mixed = new List<object> { 1, "hello" };
為什麼
遷移備忘錄
Dictionary<TKey, TValue> 進行替換Hashtable,因可能隱含著計數值等,故需增加測試的充實性var fs = new FileStream(path, FileMode.Open);
try
{
var data = fs.ReadByte();
}
finally
{
fs?.Dispose(); // 需明確調用
}
using var fs = new FileStream(path, FileMode.Open);
var data = fs.ReadByte();
// 離開範圍時自動 Dispose
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);
為什麼
IAsyncDisposable 被正確實作,await using 將確保有效清理遷移備忘錄
IAsyncDisposable 的型別應用 await usingIAsyncDisposable,因此需 .NET Core 3.0 以上var results = new List<int>();
foreach (var n in nums)
{
if (n % 2 == 0)
results.Add(n * n);
}
var results = nums
.Where(n => n % 2 == 0)
.Select(n => n * n)
.ToList();
為什麼
遷移備忘錄
ToList() 確定前),以降低 GC 壓力Span<T> 或 PLINQ/Parallel.Forvar s = string.Format("{0} - {1}: {2}", id, name, status);
var s2 = id + " - " + name + ": " + status; // GC 壓力高
var f1 = string.Format("{0:D3}", value); // 格式化示例(010)
var s = $"{id} - {name}: {status}";
var f2 = $"{value:D3}"; // 格式化示例(010)
為什麼
遷移備忘錄
StringBuilder:D3 等)也可在插值內使用:$"{value:D3}"if (obj is Customer)
{
var c = (Customer)obj;
Console.WriteLine(c.Name);
}
if (obj is Customer c)
{
Console.WriteLine(c.Name);
}
為什麼
遷移備忘錄
switch 表達式,可以合併型別檢查和分支when 子句中添加條件:if (obj is Customer c when c.Age > 18)string msg;
switch (status)
{
case 0: msg = "OK"; break;
case 1: msg = "Warn"; break;
default: msg = "NG"; break;
}
string msg = status switch
{
0 => "OK",
1 => "Warn",
_ => "NG"
};
為什麼
遷移備忘錄
0 or 1 or 2 => "Low"Customer { Age: > 18 } => "Adult"使用 DateTime.Now 會造成時區問題。此外,冗餘的 DTO 定義對變更的韌性差,始終存在漏修的風險。使用 record 和 DateTimeOffset,可實現更堅固且簡潔的設計。
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
// 用途
var user = new User { Name = "Alice", Age = 30 };
public record User(string Name, int Age);
// 用途(相同)
var user = new User("Alice", 30);
// 使用 with 進行不變的複製
var user2 = user with { Age = 31 };
為什麼
遷移備忘錄
record class 和 record struct 來區分引用型 / 值型var now = DateTime.Now; // 夏令時間或時區變更會導致意外行為
var now = DateTimeOffset.UtcNow; // 始終使用 UTC
// 在顯示時轉換為本地時間
Console.WriteLine(now.ToLocalTime());
為什麼
遷移備忘錄
DateTimeOffset.UtcNow 而非 DateTime.UtcNow(以保持時區信息)datetime / datetime2,則在保存時傳遞 .UtcDateTime,或新建結構考慮使用 datetimeoffset 型別BinaryFormatter 將被淘汰,且伴隨有安全風險。此外,資源洩漏可能導致未檢測的句柄耗盡,進而使應用突然崩潰。養成使用 using/await using 進行管理的習慣是至關重要的。
// BinaryFormatter 不推薦(有安全風險)
// 使用 Newtonsoft.Json 的情況
var json = JsonConvert.SerializeObject(obj);
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);
為什麼
-安全且無遠端代碼執行的漏洞
遷移備忘錄
System.Text.Json 中使用 camelCase:JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCaseNewtonsoft.Json 使用 camelCase:則需顯式設定 CamelCasePropertyNamesContractResolverPropertyNameCaseInsensitive = true 進行對應JsonSerializerOptions 中設定 ReferenceHandler = ReferenceHandler.Preserve[JsonPolymorphic]/[JsonDerivedType] 支持。舊代碼則可能需重構 DTO 設計Newtonsoft.Json 可能有更嚴格的限制,應考慮使用 [JsonConstructor] 或改成 public[JsonPropertyName] 自訂 JSON 鍵直接操作 Thread 是死鎖與內存洩漏的溫床,且複雜性會增加。統一使用 Task 及異步 API,將提升擴展性,且除錯也更為簡單。
var th = new Thread(Work) { IsBackground = true };
th.Start();
// ...
th.Join(); // 等待
await Task.Run(Work);
為什麼
遷移備忘錄
IHostedService (ASP.NET Core)CancellationToken 實現平滑關閉var timer = new System.Timers.Timer(1000);
timer.Elapsed += (s, e) => DoWork();
timer.Start();
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 及以上版本可用System.Windows.Forms.Timerusing var cmd = new SqlCommand(sql, conn);
using var r = cmd.ExecuteReader(); // 同步,阻塞執行緒
while (r.Read()) { /* 行處理 */ }
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()) { /* ... */ }
}
using var connection = new SqlConnection(connStr);
var users = await connection.QueryAsync<User>("SELECT * FROM Users");
為什麼
遷移備忘錄
var rnd = new Random();
var val = rnd.Next(); // 隨機種子基於當前時間
// 建議使用 .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 即可RandomNumberGeneratortry { /*...*/ }
catch (IOException ex)
{
if (!IsTransient(ex)) throw;
Log(ex);
}
try { /*...*/ }
catch (IOException ex) when (IsTransient(ex))
{
Log(ex);
}
為什麼
遷移備忘錄
catch 區塊中處理if (user != null)
name = user.Name;
if (config == null)
config = DefaultConfig();
name = user?.Name; // 若 user 為 null 則返回 null
config ??= DefaultConfig(); // 僅在為 null 時賦值
#nullable enable
public string? GetUserName(User? user)
{
return user?.Name; // 確保型別安全
}
為什麼
遷移備忘錄
#nullable enable 可逐步導入(按檔案單位)private int _x;
public int X
{
get { return _x; }
set { _x = value; }
}
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);List<string> list = new List<string>();
Dictionary<int, string> dict = new Dictionary<int, string>();
List<string> list = new();
Dictionary<int, string> dict = new();
為什麼
遷移備忘錄
var u2 = new User
{
Name = u1.Name,
Age = u1.Age,
Email = u1.Email
};
var u2 = u1 with { Age = u1.Age + 1 };
var (min, max) = GetRange();
為什麼
遷移備忘錄
record 型別的功能<configuration>
<appSettings>
<add key="ApiKey" value="secret"/>
<add key="ApiUrl" value="https://api.example.com"/>
</appSettings>
</configuration>
{
"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 等遷移備忘錄
Trace.WriteLine($"Start Processing {id}");
_logger.LogInformation("Start Processing {Id}", id);
為什麼
{Id} 進行屬性化設計遷移備忘錄
ILogger<T>GUI 特有的陷阱(跨執行緒例外、GDI+ 資源洩漏、閃爍)若不加以處理,應用將不穩定,並失去用戶的信賴。這些對策是使應用長久受愛戴的基礎。
if (InvokeRequired)
Invoke(new Action(() => label1.Text = text));
else
label1.Text = text;
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 的 SynchronizationContextInvoke遷移備忘錄
ConfigureAwait(false)ConfigureAwait(false) 以放棄 UI 上下文原文出處:https://qiita.com/Sakai_path/items/3edc7a0a8db4e43753c8