はじめに
C#は急速に進化してきた言語です。10年前の「当たり前」は、今では保守性やパフォーマンスの観点から非推奨になっていることもあります。
今回は、レガシーシステムの保守や段階的なリファクタリングに携わる開発者向けに、よくある「昔の書き方」から「現在の推奨」への置き換えパターンを、すぐに実装できるスニペット集としてまとめました。
C#開発の現場に入ると、新規開発でない限り意外と古いコードを見ることが多いと思います。 今まで受け継がれてきたシステムは大量にあり今でも10年以上前のシステムが元気に動いています。 そして開発者も容易に書き方を変えることはしません。プロジェクトととして統一する狙いもあると思います。それでもコードは確実に進化してます。ぜひ覚えておいてほしい「今と昔のコード」を集めてみました。
処理別リスト
今回、かなり長いのでここの一覧から気になる項目へ飛ぶことも可能です。
| No. | カテゴリ | パターン | リンク |
|---|---|---|---|
| 1 | 非同期処理 | Begin/End/BackgroundWorker → async/await | 詳細 |
| 2 | 非同期処理 | HttpWebRequest/WebClient → HttpClient | |
| 3 | コレクション | ArrayList/Hashtable → ジェネリックコレクション | 詳細 |
| 4 | リソース管理 | try/finally で Dispose → using/await using | |
| 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) だらけ → ?. / ??= / Nullable | 詳細 |
| 18 | プロパティ | 手書きプロパティ → 自動実装/式本体メンバー | 詳細 |
| 19 | インスタンス化 | new Type() 明示 → 目標型 new | 詳細 |
| 20 | データコピー | 手動コピー → with/分解(ValueTuple) | 詳細 |
| 21 | 設定管理 | app.config/Web.config → appsettings.json + Options | 詳細 |
| 22 | ログ出力 | Trace.WriteLine → ILogger | |
| W1 | WinForms | Cross-thread 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 | フリッカ対策:DoubleBuffered + OnPaint 集中 | |
| W8 | WinForms | ダイアログ後の重処理:同期 → 非同期 | |
| W9 | WinForms | ボタン多重起動:フラグ曖昧 → Interlocked or SemaphoreSlim | |
| W10 | WinForms | ConfigureAwait(false) の誤用 → UI 層で禁止 | |
| W11 | WinForms | フォーム終了時のタスク掃除:放置 → Cancel & 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で例外を検知する必要があった)
移行メモ
- 戻り値型を
TaskまたはTask<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はスタティック or シングルトンで共有することが推奨 -
.NET Core/.NET 5+ではIHttpClientFactoryを活用 -
クラウド/コンテナ環境で IP ローテーションが頻繁な場合、
PooledConnectionLifetimeを設定し、接続が古い宛先に張り付くのを防ぐこと(未設定だと接続が長生きし、DNS 更新に追随しない) - 従来の
WebClientは .NET 6+ では事実上 obsolete で、HttpClientへの移行が必須
コレクション・データ型関連
ArrayList で混在型を許すと、実行時に突然キャストエラーが発生します。型安全性を確保することで、コンパイル時にバグを防ぎ、ボクシング/アンボクシングのオーバーヘッドも消滅。パフォーマンスとバグ削減の両立です。
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 → using/await using
【Before】手動でDisposeを呼び出し
var fs = new FileStream(path, FileMode.Open);
try
{
var data = fs.ReadByte();
}
finally
{
fs?.Dispose(); // 明示的に呼び出す必要がある
}
【After】using ステートメント(同期)
using var fs = new FileStream(path, FileMode.Open);
var data = fs.ReadByte();
// スコープを抜ける時に自動Dispose
async の場合は 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以上が必須
LINQ・言語機能関連
手続き形のループコードは意図が読みづらく、保守時に間違いやすいです。関数形へ移行することで、「何をしているか」が一目瞭然になり、バグも減ります。レガシーコード体質からの脱却です。
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);
}
なぜ
- コード行数削減
- キャスト失敗時の例外や重複チェックを防げる
移行メモ
- 追加条件は && と組み合わせる
if (obj is Customer c && c.Age >= 20)
- switch のパターンやプロパティパターンとも相性が良い
if (obj is Customer { Status: "Active" } c)
- nullチェックを兼ねられる
if (obj is not Customer) return;
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漏れのバグがない - 網羅性チェックコンパイラサポート
- 複雑な条件も readable
移行メモ
- 複数ケースを1つの結果にまとめるなら:
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 classとrecord structで参照型/値型を使い分け可
10) DateTime.Now 乱用 → DateTimeOffset.UtcNow
【Before】ローカルタイムの罠
var now = DateTime.Now; // 夏時間やタイムゾーン変更で予期しない動作
【After】UTC で明確に
var now = DateTimeOffset.UtcNow; // 常に UTC
// 表示時にローカル化
Console.WriteLine(now.ToLocalTime());
なぜ
- タイムゾーン対応が明確
- 夏時間やDST(Daylight Saving Time)の罠が減る
- ログやデータベースに保存する際、一貫性が保たれる
移行メモ
- サーバーアプリケーションは UTC で統一し、表示時のみローカル化
-
DateTime.UtcNowではなくDateTimeOffset.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 Standard 仕様で高速
- 標準ライブラリなので依存削減
移行メモ
-
命名規則の差異(重要):両者とも既定では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 設計を見直す必要がある可能性 -
プライベートセッター/コンストラクタ:
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を使い続ける
データベース・セキュリティ関連
ADO.NET の同期 API は接続をブロック。スケーラビリティが頭打ちになります。非同期 API と ORM 活用で、高スループット、短応答時間を実現。セキュリティ(乱数生成)も堅牢に。
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ブロック内で
Null チェック・参照関連
if (obj != null) の連続チェックはコードを肥大化させ、null 参照例外(NullReferenceException)のリスクが常に伴います。Null 条件演算子と Nullable Reference Types で、null 安全を宣言的に確保できます。
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);
インスタンス作成関連
new Type() を明示すると、型変更時に呼び出し側も修正が必要。目標型 new なら型推論で対応でき、リファクタリングが容易になります。特にジェネリクスとの組み合わせで威力を発揮。
19) new Type() 明示 → 目標型 new
【Before】型を2回記述
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 のサポートが進んでいる
移行メモ
- コンテキストが明確でない場合は、省略しない方が読みやすい場合も
- 判断は開発チームのコーディング規約で
オブジェクトコピー関連
手動でプロパティをコピーするのは誤りやすく、新しいプロパティの追加で漏れが生じます。with キーワードを使えば、修正漏れなくイミュータブルなオブジェクト操作が可能。スレッド安全性も向上。
20) 手動コピー → with/分解(ValueTuple)
【Before】プロパティを1つずつ手書き
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型に限定される機能
設定・ロギング関連
app.config の管理は煩雑で、環境ごとの設定ミスが本番障害になったり、ログがバラバラだとトラブルシューティングに時間がかかります。統一的な設定・ログ基盤は、運用の安定性と効率を劇的に向上させます。
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) Cross-thread 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 コンテキストを捨てる
W2) BackgroundWorker → Task + IProgress + CancellationToken
【Before】イベントハンドラが複雑
var bw = new BackgroundWorker
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
bw.DoWork += (s, e) => { /* 処理 */ };
bw.ProgressChanged += (s, e) => progressBar.Value = e.ProgressPercentage;
bw.RunWorkerCompleted += (s, e) => MessageBox.Show("完了");
bw.RunWorkerAsync();
【After】標準の async/await で統一
private CancellationTokenSource? _cts;
private async void btnStart_Click(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();
btnStart.Enabled = false;
var progress = new Progress<int>(p => progressBar.Value = p);
try
{
await DoWorkAsync(_cts.Token, progress);
MessageBox.Show("完了");
}
catch (OperationCanceledException)
{
MessageBox.Show("キャンセルされました");
}
finally { btnStart.Enabled = true; }
}
private void btnCancel_Click(object sender, EventArgs e)
=> _cts?.Cancel();
private async Task DoWorkAsync(CancellationToken ct, IProgress<int> progress)
{
for (int i = 0; i < 100; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(100, ct);
progress.Report(i + 1);
}
}
なぜ
-
BackgroundWorkerより標準ライブラリで完結 - テスト容易(モック置換が簡単)
- 例外処理が統一的
移行メモ:
-
IProgress<T>は自動的に UI コンテキストで実行される -
CancellationTokenで キャンセル要求を統一的に処理
W3) Application.DoEvents() → 非推奨。分割 + await
【Before】再入/メッセージループ乱れの原因
for (int i = 0; i < 1000; i++)
{
HeavyStep();
Application.DoEvents(); // UI応答性確保のつもり…だが危険
}
【After】Task.Run で非同期化
// 非推奨:ループ内で Task.Run 連発、タスク過剰生成
for (int i = 0; i < 1000; i++)
{
await Task.Run(HeavyStep);
if (i % 10 == 0)
await Task.Yield(); // UI に制御を返す
}
// 推奨:単一の Task.Run 内で塊として処理、進捗は IProgress<T> でレポート
private async void btnRun_Click(object sender, EventArgs e)
{
var progress = new Progress<int>(p => progressBar.Value = p);
await Task.Run(() => ProcessAllSteps(progress));
}
private void ProcessAllSteps(IProgress<int> progress)
{
for (int i = 0; i < 1000; i++)
{
HeavyStep();
progress.Report((i + 1) * 100 / 1000); // UI に進捗を通知
}
}
なぜ
-
DoEvents()は予期しない再入やメッセージループ混乱を招く - 非同期化で安全に UI 応答性を確保
移行メモ
- UI がフリーズするほど重い処理は非同期化が必須
- ループ内で
Task.Runを連発するとタスク過剰生成。単一のTask.Run内で処理をまとめ、IProgress<T>で進捗をレポートするのが安定
W4) .Result/.Wait() でデッドロック → すべて await
【Before】UI スレッドでブロック、デッドロック
var text = httpClient.GetStringAsync(url).Result; // UI停止
label1.Text = text;
【After】非同期で待機
var text = await httpClient.GetStringAsync(url);
label1.Text = text;
なぜ
- UI の
SynchronizationContextでブロックするとデッドロック -
awaitなら UI スレッドが処理を実行しつつ待機
移行メモ
-
.Result/.Wait()は絶対禁止と考えるくらいが吉
W5) タイマー選択:Forms.Timer 優先 / PeriodicTimer 併用
【Before】System.Timers.Timer でスレッド問題
var t = new System.Timers.Timer(1000);
t.Elapsed += (s, e) => label1.Text = DateTime.Now.ToString();
// InvalidOperationException: クロススレッド
【After】Forms.Timer で UIスレッドで実行
var t = new System.Windows.Forms.Timer { Interval = 1000 };
t.Tick += (s, e) => label1.Text = DateTime.Now.ToString();
t.Start();
または .NET 6+ で PeriodicTimer + BeginInvoke
var pt = new PeriodicTimer(TimeSpan.FromSeconds(1));
_ = Task.Run(async () =>
{
while (await pt.WaitForNextTickAsync())
BeginInvoke(new Action(() =>
label1.Text = DateTime.Now.ToString()));
});
なぜ
-
Forms.Timerは UI スレッドでTickを発火 -
PeriodicTimerは非 UI スレッドなので、UI 更新はBeginInvokeで marshal
移行メモ
- シンプルなら
Forms.Timer - より詳細な制御が必要なら
PeriodicTimer
W6) GDI+ リソース(Pen/Brush/Bitmap/Graphics) を確実に Dispose
【Before】CreateGraphics + リソースリーク
void Draw()
{
var g = CreateGraphics(); // リソース取得
var pen = new Pen(Color.Red); // ハンドル確保
g.DrawLine(pen, 0, 0, 100, 0);
// Dispose漏れで GDI ハンドル枯渇
}
【After】OnPaint 内で using を使用
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using var pen = new Pen(Color.Red);
e.Graphics.DrawLine(pen, 0, 0, 100, 0);
// OnPaint終了時に自動Dispose
}
なぜ:
- GDI ハンドルリークでシステムリソース枯渇
-
OnPaintは効率的(描画タイミングが最適化)
移行メモ
-
CreateGraphics()は避け、OnPaint/OnPaintBackgroundで描画 -
using文で確実にリソース解放
W7) フリッカ対策:DoubleBuffered + OnPaint 集中
【Before】頻繁な Invalidate でちらつく
// デフォルト設定で Invalidate() 連発 → チラつき
【After】DoubleBuffer + 描画集約
public class SmoothPanel : Panel
{
public SmoothPanel()
{
this.SetStyle(
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint,
true
);
}
protected override void OnPaint(PaintEventArgs e)
{
// すべての描画をここに集約
e.Graphics.FillRectangle(Brushes.White, ClientRectangle);
e.Graphics.DrawString("Hello", Font, Brushes.Black, 10, 10);
}
}
なぜ
- 二重バッファリングで背景の描画をスキップ
- 描画を集約することで不必要な再描画を防止
移行メモ
- 複雑な図形や大量の描画ならさらに
Bitmapバッファも検討
W8) ダイアログ後の重処理:同期 → 非同期
【Before】ファイル読み込みで UI 停止
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
var bytes = File.ReadAllBytes(openFileDialog1.FileName); // UI停止
}
【After】非同期で読み込み
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
var path = openFileDialog1.FileName;
var bytes = await Task.Run(() => File.ReadAllBytes(path));
ProcessFile(bytes);
}
なぜ
- 大容量ファイルでも UI がフリーズしない
移行メモ
-
File.ReadAllBytesAsync(NET 6+) を使用も可
W9) ボタン多重起動:フラグ曖昧 → Interlocked or SemaphoreSlim
【Before】Boolean フラグで不完全
if (_isProcessing) return;
_isProcessing = true;
try { /* 処理 */ }
finally { _isProcessing = false; }
【After】Interlocked で原子的に
private int _gate = 0;
private async void btn_Click(object sender, EventArgs e)
{
if (Interlocked.CompareExchange(ref _gate, 1, 0) != 0)
return; // 既に処理中
btn.Enabled = false;
try
{
await RunAsync();
}
finally
{
btn.Enabled = true;
Interlocked.Exchange(ref _gate, 0);
}
}
または SemaphoreSlim で待機
private readonly SemaphoreSlim _sem = new(1, 1);
private async void btn_Click(object s, EventArgs e)
{
if (!await _sem.WaitAsync(0))
return; // 他のタスクが処理中、スキップ
try { await RunAsync(); }
finally { _sem.Release(); }
}
なぜ
- ボタン連打時の多重実行を確実に防止
移行メモ
-
SemaphoreSlimは待機スタックを持つため、順序性が保たれる - UI ボタンなら
SemaphoreSlim.WaitAsync(0)で即座に判定
W10) ConfigureAwait(false) の誤用 → UI 層で禁止
【Before】UI更新前に ConfigureAwait(false)
await SomeAsync().ConfigureAwait(false); // UIコンテキストを捨てる
label1.Text = "done"; // InvalidOperationException
【After】UI 層では ConfigureAwait つけない
await SomeAsync(); // 既定で UI コンテキストに復帰
label1.Text = "done";
なぜ
- UI コンテキストをキープし、UI スレッドで UI 更新可能
移行メモ
- UI 層:
ConfigureAwaitなし - ライブラリ層:
ConfigureAwait(false)で UI コンテキストを捨てる
W11) フォーム終了時のタスク掃除:放置 → Cancel & await 完了
【Before】バックグラウンドタスク放置
protected override void OnFormClosing(FormClosingEventArgs e)
{
base.OnFormClosing(e);
// バックグラウンドタスク実行中だが放置
}
【After】キャンセルして完了を待機(安全側)
private CancellationTokenSource? _closingCts;
private Task? _runningTask;
protected override void OnFormClosing(FormClosingEventArgs e)
{
// フォーム終了を一度阻止
if (_runningTask != null && !_runningTask.IsCompleted)
{
e.Cancel = true;
// クリーンアップを非同期で実施
_ = CleanupAndCloseAsync();
return;
}
base.OnFormClosing(e);
}
private async Task CleanupAndCloseAsync()
{
try
{
_closingCts?.Cancel();
if (_runningTask != null)
await _runningTask;
}
catch (OperationCanceledException) { }
finally
{
// クリーンアップ完了後、フォームを閉じる
Close();
}
}
代替:async void の最小形(リスク有)
// 注意:フォームが閉じ進む可能性。上記パターン推奨
protected override async void OnFormClosing(FormClosingEventArgs e)
{
_closingCts?.Cancel();
try
{
if (_runningTask != null)
await _runningTask;
}
catch (OperationCanceledException) { }
base.OnFormClosing(e);
}
なぜ
- 中途半端な I/O や GDI リソースを残さない
- グレースフルシャットダウン
移行メモ
-
注意:
async void OnFormClosingはフォーム終了が待たずに進む可能性あり。上記の「安全側」パターン(e.Cancel = true→ 非同期クリーンアップ →Close())を推奨 - 中途半端な I/O や GDI リソースを残さないため、クリーンアップ完了を確実に待機
W12) 高 DPI:既定任せ → 明示設定
【Before】高 DPI 未対応で UI がぼやける
<!-- 旧 manifest、高 DPI 未対応 -->
【After】.NET 5+ で明示的に設定
// Program.cs / Main
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// Form 側
public MyForm()
{
this.AutoScaleMode = AutoScaleMode.Dpi; // または Font
InitializeComponent();
}
なぜ
- 4K/2K ディスプレイで鮮明に表示
- にじみやぼやけが減る
移行メモ
-
PerMonitorV2が最新で推奨 - 既存 WinForms アプリなら必ず対応しておく
関連記事
- 【C#】同じ機能、違う書き方 - パフォーマンスで選ぶべきコードはどっち?
- 【C#】GCだけに任せるな!メモリ最適化攻略ガイド ※上級者向け
- 【C#】 最新バージョンの主要機能まとめ — 小さなアップデートで大きく変わるコード設計
- 【C#】バージョン間の隠れた機能変更と互換性の罠
- 【C#】上級者でも陥る5つの落とし穴と対策
- C# 14 (Preview) 技術レポート — Before / After で分かる新機能まとめ
参考資料
まとめ
C# は急速に進化し、モダンな言語機能が次々と追加されています。レガシーコードの保守時には、ここで紹介したパターンを参考に、小さく・安全に段階的に リファクタリングすることをお勧めします。
各パターンの背景にある「なぜ改善するのか」を理解することで、プロジェクト固有の状況に応じた判断ができるようになります。