はじめに
C#はとても多くの機能を持つ言語ですが、その柔軟性と表現力の高さゆえに、上級者でも陥りやすい落とし穴がいくつか存在します。
今回は、実務でよく見かける注意すべき5つのポイントをまとめてみました。
1. usingステートメントの不適切な使用 - 非同期処理との組み合わせで起こるリソースリーク
非同期処理中にリソースが予期せず解放される可能性があり、特にファイル操作やデータベース接続で注意が必要です。
問題のあるコード
C#
public async Task ProcessFileAsync(string path)
{
// 問題点: 非同期処理が完了する前にStreamがDisposeされる可能性がある
using var stream = new FileStream(path, FileMode.Open);
await ProcessDataAsync(stream);
}
対策
C#
public async Task ProcessFileAsync(string path)
{
// await usingを使用することで、非同期処理完了までリソースを保持
await using var stream = new FileStream(path, FileMode.Open);
await ProcessDataAsync(stream);
}
// または
public async Task ProcessFileAsync(string path)
{
// ConfigureAwait(false)を使用してデッドロックを防ぐ
using var stream = new FileStream(path, FileMode.Open);
await ProcessDataAsync(stream).ConfigureAwait(false);
}
重要ポイント
- 非同期処理を含むメソッドでは
await using
を優先的に使用する - ライブラリコードでは
ConfigureAwait(false)
の使用を検討 - リソースの解放タイミングを意識する
2. Task.Resultの危険な使用 - デッドロックを引き起こす可能性のある同期的待機
UIスレッドでの使用時に特に危険で、アプリケーション全体がフリーズする可能性があります。
問題のあるコード
C#
public class DataService
{
public string GetData()
{
var task = FetchDataAsync();
return task.Result; // デッドロックの可能性
}
private async Task<string> FetchDataAsync()
{
await Task.Delay(1000);
return "data";
}
}
対策
C#
public class DataService
{
// 推奨: 非同期メソッドとして実装
public async Task<string> GetDataAsync()
{
return await FetchDataAsync().ConfigureAwait(false);
}
// どうしても同期的に呼び出す必要がある場合
public string GetData()
{
// Task.Runで新しいスレッドで実行することでデッドロックを回避
return Task.Run(FetchDataAsync).Result;
}
}
重要ポイント
- 可能な限り非同期メソッドを使用する
-
Task.Result
やTask.Wait()
の使用を避ける - 同期呼び出しが必要な場合は
Task.Run
を検討する
3. 例外ハンドリングの過剰な一般化 - 予期せぬエラーの見逃しにつながる広すぎる例外捕捉
具体的な例外タイプを指定せずに包括的なExceptionをキャッチすることで、重要なエラーを見逃す可能性があります。
問題のあるコード
C#
public void ProcessData(string data)
{
try
{
// 様々な処理
var result = JsonSerializer.Deserialize<DataModel>(data);
// さらに処理
}
catch (Exception ex)
{
Logger.Log(ex);
throw;
}
}
対策
C#
public void ProcessData(string data)
{
try
{
// 具体的な例外タイプでキャッチ
var result = JsonSerializer.Deserialize<DataModel>(data);
// さらに処理
}
catch (JsonException ex)
{
Logger.Log("JSON deserialize error", ex);
throw new DataProcessingException("Invalid data format", ex);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("specific error"))
{
Logger.Log("Specific operation error", ex);
throw new DataProcessingException("Operation failed", ex);
}
}
重要ポイント
- 具体的な例外タイプを使用する
-
when
句で例外をさらに絞り込む - カスタム例外を適切に活用する
4. 構造体の誤った変更 - Value Typeの特性を考慮しないメンバー変更
構造体はValue Typeであり、メソッド内での直接変更は新しいインスタンスとして返す必要があります。
問題のあるコード
C#
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
public void Move(int dx, int dy)
{
X += dx; // 警告:これは期待通りに動作しない可能性がある
Y += dy;
}
}
対策
C#
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
// 新しいインスタンスを返す
public Point Move(int dx, int dy)
{
return new Point(X + dx, Y + dy);
}
}
// 使用例
var point = new Point(1, 1);
point = point.Move(2, 2); // 新しいインスタンスを作成
重要ポイント
- 構造体は不変(immutable)として設計する
- メソッドは新しいインスタンスを返す
-
readonly struct
を活用する
5. ConfigureAwait(false)の不適切な省略 - ライブラリコードでのデッドロック可能性
ライブラリ開発時に特に重要で、呼び出し元のスレッドコンテキストに依存することでデッドロックが発生する可能性があります。
問題のあるコード
C#
public class LibraryClass
{
public async Task<string> ProcessDataAsync()
{
// 呼び出し元のSynchronizationContextを伝播
var data = await FetchDataAsync();
return await TransformDataAsync(data);
}
}
対策
C#
public class LibraryClass
{
public async Task<string> ProcessDataAsync()
{
// ConfigureAwait(false)でコンテキスト伝播を防ぐ
var data = await FetchDataAsync().ConfigureAwait(false);
return await TransformDataAsync(data).ConfigureAwait(false);
}
}
重要ポイント
- ライブラリコードでは必ず
ConfigureAwait(false)
を使用する - UI依存のコードを除き、可能な限り使用を検討する
- デッドロックのリスクを理解する
まとめ
これらの落とし穴を理解し、適切な対策を講じることで、より信頼性の高いC#コードを書くことができます。
重要なポイントまとめ
- 非同期処理では
await using
とConfigureAwait(false)
を適切に使用する - UI threadでの同期的待機を避け、非同期メソッドを優先する
- 例外は具体的な型でキャッチし、
when
句で条件を絞り込む - 構造体は
readonly
で不変にし、メソッドは新しいインスタンスを返す - ライブラリコードでは必ず
ConfigureAwait(false)
を使用する