C# 7.0以前では、throwは「文(Statement)」としてのみ扱われていたため、if文などのブロック内で記述する必要がありました。しかし、C# 7.0以降ではthrowを「式(Expression)」として記述できるようになったため、条件演算子(?:)やnull合体演算子(??)の右側など、値を返すことが期待される場所に直接例外のスローを記述できるようになりました。
これにより、コンストラクタやプロパティのsetterにおけるバリデーション(妥当性確認)ロジックを、非常に簡潔に記述できます。ここでは、APIクライアントの設定クラスを題材に、この機能の実践的な使い方を解説します。
throw式の概要
throw式を使用すると、計算結果や代入値が必要な文脈において、「特定の条件を満たさない場合は値を返す代わりに例外を投げる」という記述が可能になります。
主に以下の場面で活用されます。
- 条件演算子(三項演算子):
条件 ? 値 : throw new Exception() - null合体演算子:
値 ?? throw new Exception() - 式形式のメンバー定義:
=>の右側
実践的なコード例:設定値のバリデーション
以下のコードは、API接続に必要な「ベースURL」と「タイムアウト設定」を管理するクラスです。コンストラクタの引数チェックにおいて、従来のif文を使わず、演算子の中で直接例外をスローしています。
using System;
namespace ApiConfiguration
{
public class ConnectionSettings
{
public string BaseUrl { get; }
public int TimeoutSeconds { get; }
// コンストラクタ
public ConnectionSettings(string baseUrl, int timeoutSeconds)
{
// 1. null合体演算子 (??) を使用したnullチェック
// baseUrl が null でなければそのまま代入し、null なら ArgumentNullException をスローします。
BaseUrl = baseUrl ?? throw new ArgumentNullException(nameof(baseUrl));
// 2. 条件演算子 (?:) を使用した値の範囲チェック
// タイムアウト値が正の数であれば代入し、そうでなければ ArgumentOutOfRangeException をスローします。
TimeoutSeconds = (timeoutSeconds > 0)
? timeoutSeconds
: throw new ArgumentOutOfRangeException(nameof(timeoutSeconds), "タイムアウトは0より大きい値である必要があります。");
}
// 3. (応用) 式形式のメンバーでの利用
// 取得しようとしたプロパティが不正な状態であれば例外を投げる例
private string _status;
public string Status
{
get => _status;
set => _status = value ?? throw new ArgumentNullException(nameof(value));
}
}
class Program
{
static void Main()
{
try
{
// 正常なケース
var validSettings = new ConnectionSettings("https://api.example.com", 30);
Console.WriteLine($"設定完了: {validSettings.BaseUrl}, {validSettings.TimeoutSeconds}秒");
// 異常なケース1: URLにnullを指定
// var error1 = new ConnectionSettings(null, 30);
// 異常なケース2: タイムアウトに不正な値を指定
var error2 = new ConnectionSettings("https://api.example.com", -5);
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"[Nullエラー] {ex.Message}");
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine($"[範囲エラー] {ex.Message}");
}
}
}
}
実行結果(異常なケース2を実行した場合)
[範囲エラー] タイムアウトは0より大きい値である必要があります。 (Parameter 'timeoutSeconds')
技術的な解説
1. null合体演算子 (??) との組み合わせ
これまで、引数のnullチェックは以下のように記述するのが一般的でした。
// 従来の方法
if (baseUrl == null) throw new ArgumentNullException(nameof(baseUrl));
BaseUrl = baseUrl;
throw式を使用することで、これを1行の代入文として記述できます。これは「ガード節(Guard Clause)」としての役割を果たしつつ、コードの行数を削減し、初期化の意図を明確にします。
2. 条件演算子 (?:) との組み合わせ
条件演算子の第2項または第3項にthrowを記述できます。 ただし、条件演算子の両方の項(真の場合と偽の場合)が互換性のある型である必要があります。throw式はいかなる型にも適合するため、片方がstringやintであっても、もう片方にthrowを記述することが可能です。
3. null合体代入演算子 (??=) との組み合わせ (C# 8.0以降)
C# 8.0からは、??=演算子の右側でもthrow式を使用できます。これにより、プロパティや変数がnullの場合にのみ代入(あるいは例外スロー)を行うロジックを簡潔に書けます。
// キャッシュがnullならロードし、ロード結果もnullなら例外を投げる
cache ??= LoadData() ?? throw new InvalidOperationException("データのロードに失敗しました。");
まとめ
throw式を活用することで、特にコンストラクタやプロパティのsetterにおけるバリデーションロジックを簡潔かつ宣言的に記述できるようになります。コードの可読性を高めるための有効な手段として、null合体演算子や条件演算子との併用を積極的に検討してください。
