395
405

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

レガシーC#コード対比集(昔こう→今こう)

Last updated at Posted at 2025-10-21

はじめに

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】コールバック地獄

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

【After】async/await で直線的に

C#
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

C#
using var wc = new WebClient();
string s = wc.DownloadString(url); // 同期メソッド

【After】HttpClient + DI

C#
// 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】キャストが必要、型安全性なし

C#
var list = new ArrayList();
list.Add(1);
list.Add("hello"); // 混在してもコンパイルエラーなし

int x = (int)list[0];  // ボクシング + キャスト

【After】型安全でパフォーマンス向上

C#
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を呼び出し

C#
var fs = new FileStream(path, FileMode.Open);
try
{
    var data = fs.ReadByte();
}
finally 
{ 
    fs?.Dispose();  // 明示的に呼び出す必要がある
}

【After】using ステートメント(同期)

C#
using var fs = new FileStream(path, FileMode.Open);
var data = fs.ReadByte();
// スコープを抜ける時に自動Dispose

async の場合は await using

C#
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】命令形プログラミング

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

【After】関数形プログラミング

C#
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】読みにくく、保守しづらい

C#
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】文字列補間で見やすく

C#
var s = $"{id} - {name}: {status}";
var f2 = $"{value:D3}"; // 書式指定の例(010)

なぜ

  • 読みやすく、タイプミスが減る
  • 内部的には効率的に最適化されている

移行メモ

  • ループ内での大量連結は StringBuilder を維持する
  • フォーマット指定(:D3 など)も補間内で使用可: $"{value:D3}"

7) is + キャスト → パターンマッチング

【Before】キャスト前後で同じ型をチェック

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

【After】パターンマッチで一度に

C#
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 漏れのリスク

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

【After】宣言的で簡潔

C#
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】プロパティを何度も手書き

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

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

【After】record で簡潔に

C#
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】ローカルタイムの罠

C#
var now = DateTime.Now;  // 夏時間やタイムゾーン変更で予期しない動作

【After】UTC で明確に

C#
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】危険性高く、保守困難

C#
// BinaryFormatter は非推奨(セキュリティリスク)
// Newtonsoft.Json の場合
var json = JsonConvert.SerializeObject(obj);

【After】標準の System.Text.Json

C#
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 で対応可
  • 循環参照:既定では失敗。JsonSerializerOptionsReferenceHandler = ReferenceHandler.Preserve を設定
  • 多態(継承):.NET 7+ は [JsonPolymorphic]/[JsonDerivedType] で対応。古いコードは DTO 設計を見直す必要がある可能性
  • プライベートセッター/コンストラクタNewtonsoft.Json より制限が厳しい場合がある。[JsonConstructor] または public に変更を検討
  • [JsonPropertyName] で JSON キーをカスタマイズ

スレッド・並行処理関連

Thread の直接操作はデッドロックやメモリリークの温床。複雑さも増します。Task や非同期 API に統一することで、スケーラビリティが向上し、デバッグもシンプルになります。

12) Thread 直使用 → Task/ThreadPool

【Before】手動でスレッド管理

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

【After】Task で統一

C#
await Task.Run(Work);

なぜ

  • ThreadPool の恩恵を受け、自動スケーリング
  • キャンセルと例外ハンドリングが統合
  • async/await と一貫性が取れる

移行メモ

  • 長寿命バックグラウンドタスクは IHostedService (ASP.NET Core) を検討
  • CancellationToken を活用した グレースフルシャットダウン

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

【Before】イベントベース、エラーハンドリング難

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

【After】PeriodicTimer で非同期対応

C#
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】スレッドをブロック

C#
using var cmd = new SqlCommand(sql, conn);
using var r = cmd.ExecuteReader();  // 同期、スレッドブロック
while (r.Read()) { /* 行ごとの処理 */ }

【After】非同期で待機

C#
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 へ

C#
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】予測可能

C#
var rnd = new Random();
var val = rnd.Next();  // シード値が現在時刻ベース

【After】予測不可能で安全

C#
// .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】条件分岐で冗長

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

【After】フィルタで意図を明確に

C#
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 チェック漏れのリスク

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

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

【After】演算子で簡潔に

C#
name = user?.Name;  // user が null なら null を返す

config ??= DefaultConfig();  // null 時だけ代入

さらに Nullable Reference Types 導入

C#
#nullable enable

public string? GetUserName(User? user)
{
    return user?.Name;  // 型安全性が保証される
}

なぜ

  • Null 参照例外が減る
  • コンパイル段階で null 安全性チェック
  • 可読性向上

移行メモ

  • #nullable enable は段階的に導入可能(ファイル単位)
  • 既存コードとの互換性に注意

プロパティ・メンバー関連

冗長なプロパティ定義は変更に弱く、プロパティ追加時に複数行を修正する必要があります。自動実装プロパティなら、修正漏れもなく、コード行数も大幅削減。保守の心理的負担が減ります。

18) 手書きプロパティ → 自動実装/式本体メンバー

【Before】ボイラープレート多い

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

【After】自動実装で短く

C#
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回記述

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

【After】型推論で省略

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

なぜ

  • ノイズ削減で読みやすく
  • IDE のサポートが進んでいる

移行メモ

  • コンテキストが明確でない場合は、省略しない方が読みやすい場合も
  • 判断は開発チームのコーディング規約で

オブジェクトコピー関連

手動でプロパティをコピーするのは誤りやすく、新しいプロパティの追加で漏れが生じます。with キーワードを使えば、修正漏れなくイミュータブルなオブジェクト操作が可能。スレッド安全性も向上。

20) 手動コピー → with/分解(ValueTuple)

【Before】プロパティを1つずつ手書き

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

【After】with で指定変更のみ

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

ValueTuple で分解

C#
var (min, max) = GetRange();

なぜ

  • ボイラープレート削減
  • 不変性が保たれる

移行メモ

  • record 型に限定される機能

設定・ロギング関連

app.config の管理は煩雑で、環境ごとの設定ミスが本番障害になったり、ログがバラバラだとトラブルシューティングに時間がかかります。統一的な設定・ログ基盤は、運用の安定性と効率を劇的に向上させます。

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

【Before】XML で階層構造が作りづらい

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

【After】JSON で階層的、環境別に対応

json
{
  "Api": { 
    "Key": "secret", 
    "Url": "https://api.example.com" 
  },
  "Logging": { "Level": "Information" }
}
C#
// 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】テキストベースで検索困難

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

【After】構造化ログで機械可読

C#
_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 チェックが何度も

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

【After】async/await で自動的にUIに復帰

C#
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】イベントハンドラが複雑

C#
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 で統一

C#
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】再入/メッセージループ乱れの原因

C#
for (int i = 0; i < 1000; i++)
{
    HeavyStep();
    Application.DoEvents();  // UI応答性確保のつもり…だが危険
}

【After】Task.Run で非同期化

C#
// 非推奨:ループ内で 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 スレッドでブロック、デッドロック

C#
var text = httpClient.GetStringAsync(url).Result;  // UI停止
label1.Text = text;

【After】非同期で待機

C#
var text = await httpClient.GetStringAsync(url);
label1.Text = text;

なぜ

  • UI の SynchronizationContext でブロックするとデッドロック
  • await なら UI スレッドが処理を実行しつつ待機

移行メモ

  • .Result/.Wait() は絶対禁止と考えるくらいが吉

W5) タイマー選択:Forms.Timer 優先 / PeriodicTimer 併用

【Before】System.Timers.Timer でスレッド問題

C#
var t = new System.Timers.Timer(1000);
t.Elapsed += (s, e) => label1.Text = DateTime.Now.ToString(); 
// InvalidOperationException: クロススレッド

【After】Forms.Timer で UIスレッドで実行

C#
var t = new System.Windows.Forms.Timer { Interval = 1000 };
t.Tick += (s, e) => label1.Text = DateTime.Now.ToString();
t.Start();

または .NET 6+ で PeriodicTimer + BeginInvoke

C#
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 + リソースリーク

C#
void Draw()
{
    var g = CreateGraphics();        // リソース取得
    var pen = new Pen(Color.Red);    // ハンドル確保
    g.DrawLine(pen, 0, 0, 100, 0);
    // Dispose漏れで GDI ハンドル枯渇
}

【After】OnPaint 内で using を使用

C#
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 でちらつく

C#
// デフォルト設定で Invalidate() 連発 → チラつき

【After】DoubleBuffer + 描画集約

C#
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 停止

C#
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
    var bytes = File.ReadAllBytes(openFileDialog1.FileName);  // UI停止
}

【After】非同期で読み込み

C#
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 フラグで不完全

C#
if (_isProcessing) return;
_isProcessing = true;
try { /* 処理 */ }
finally { _isProcessing = false; }

【After】Interlocked で原子的に

C#
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 で待機

C#
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)

C#
await SomeAsync().ConfigureAwait(false);  // UIコンテキストを捨てる
label1.Text = "done";  // InvalidOperationException

【After】UI 層では ConfigureAwait つけない

C#
await SomeAsync();  // 既定で UI コンテキストに復帰
label1.Text = "done";

なぜ

  • UI コンテキストをキープし、UI スレッドで UI 更新可能

移行メモ

  • UI 層: ConfigureAwait なし
  • ライブラリ層: ConfigureAwait(false) で UI コンテキストを捨てる

W11) フォーム終了時のタスク掃除:放置 → Cancel & await 完了

【Before】バックグラウンドタスク放置

C#
protected override void OnFormClosing(FormClosingEventArgs e)
{
    base.OnFormClosing(e);
    // バックグラウンドタスク実行中だが放置
}

【After】キャンセルして完了を待機(安全側)

C#
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 の最小形(リスク有)

C#
// 注意:フォームが閉じ進む可能性。上記パターン推奨
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 がぼやける

xml
<!-- 旧 manifest、高 DPI 未対応 -->

【After】.NET 5+ で明示的に設定

C#
// 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# は急速に進化し、モダンな言語機能が次々と追加されています。レガシーコードの保守時には、ここで紹介したパターンを参考に、小さく・安全に段階的に リファクタリングすることをお勧めします。

各パターンの背景にある「なぜ改善するのか」を理解することで、プロジェクト固有の状況に応じた判断ができるようになります。

395
405
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
395
405

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?