この記事では主にパラメーター検証例外 (Argument Exception) について紹介します。主に ArgumentNullException、ArgumentException、ArgumentOutOfRangeException、DuplicateWaitObjectException の 4 種類があります。
〇.はじめに
前回の記事では、一般的な例外タイプをすべてリストしました。この記事では各カテゴリのアブノーマルな特徴を紹介していきます。
この記事では主にパラメータ検証例外 (Argument Exception) を紹介します。主に次の 4 種類があります。ArgumentNullException、ArgumentException、ArgumentOutOfRangeException、DuplicateWaitObjectException。
この一連の記事を表示するには、リンクをクリックしてください。C#例外詳細解説シリーズ。
1. パラメータ検証例外の概要
Exception
└─ SystemException
└─ ArgumentException ← 基类,带 ParamName
├─ ArgumentNullException ← 参数为 null
├─ ArgumentOutOfRangeException ← 参数值越界(带 ActualValue)
└─ DuplicateWaitObjectException ← 同步对象数组中有重复项
それらに共通する主な特徴は次のとおりです。どちらも ParamName を持ちます(ArgumentException によって提供される) により、呼び出し元はキャッチ後にどのパラメーターに問題があるかを正確に知ることができます。ログ、モニタリング、API エラー応答はすべて、このフィールドを直接使用できます。。これは、「パラメーター検証例外」と InvalidOperationException (オブジェクトのステータスが不正であり、呼び出し元によって渡されたエラーではありません) の最も重要な違いでもあります。
選び方は?
参数是引用类型且为 null? → ArgumentNullException
参数非 null,但字符串内容不合法? → ArgumentException(或 ThrowIfNullOrEmpty / ThrowIfNullOrWhiteSpace)
参数是数值/枚举/索引/长度越界? → ArgumentOutOfRangeException
参数是同步句柄数组且有重复? → DuplicateWaitObjectException(几乎不用自己抛)
参数本身合法,但当前对象状态不允许? → InvalidOperationException(不属于本类,别用错)
2. 各例外の概要と簡単な例
2.1 引数例外
セマンティクス: パラメーターは null ではありませんが、値自体は意味的に有効ではありません。形式が間違っており、不正な文字が含まれており、矛盾した組み合わせがあり、許可された列挙セット内にありません。
主要メンバー:パラメータ名カスタマイズ可能なメッセージ、InnerException のサポート (解析に失敗したネイティブ例外のラップに適しています)。
public void SetOutputPath(string path)
{
ArgumentNullException.ThrowIfNull(path);
if (path.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
throw new ArgumentException("路径不能包含通配符或非法字符", nameof(path));
if (Path.IsPathRooted(path) == false)
throw new ArgumentException("必须是绝对路径", nameof(path));
}
// 多个参数之间存在逻辑矛盾:把"哪个参数有问题"说清楚
public void Configure(TimeSpan timeout, int retryCount)
{
if (retryCount > 0 && timeout == TimeSpan.Zero)
throw new ArgumentException("启用重试时超时时间不能为零", nameof(timeout));
}
// 包装下层异常,保留根因
public Guid ParseId(string id)
{
ArgumentException.ThrowIfNullOrEmpty(id);
try { return Guid.Parse(id); }
catch (FormatException ex)
{
throw new ArgumentException($"'{id}' 不是有效的 GUID", nameof(id), ex);
}
}
.NET 7/8 以降の簡略化された記述:
| 方法 | バージョン | トリガー条件 | 投げる |
|---|---|---|---|
ArgumentException.ThrowIfNullOrEmpty(s) |
.NET 7+ | null または "" |
ヌル→ArgumentNullException空→ArgumentException |
ArgumentException.ThrowIfNullOrWhiteSpace(s) |
.NET 8+ | null/""/すべて空白 |
同上 |
ArgumentException.ThrowIfEqual / ThrowIfNotEqual |
.NET 8+ | 値の等価性が一致しません | ArgumentException |
public void Rename(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name); // 一条顶三条
}
該当するシナリオ: 通常/パス/電子メール/ファイル名の有効性、列挙型 IsDefined の検証、接続文字列キーワード、コレクション内で許可されない特定の要素、複数パラメーターの相互排他制約。
2.2 ArgumentNull例外
セマンティクス: メソッド コントラクトでは、「必須」参照パラメーターに null を渡す必要があります。これは呼び出し側のバグであり、「データが見つかりません」ではありません。
主要メンバー:ParamName;メッセージは次のように固定されています “Value cannot be null. (Parameter 'x')”。
// 传统写法(所有框架版本可用)
public void Save(string fileName, Stream content)
{
if (fileName == null)
throw new ArgumentNullException(nameof(fileName));
if (content == null)
throw new ArgumentNullException(nameof(content), "必须提供可写的内容流");
}
// .NET 6+ 推荐写法:不需要 nameof,靠 [CallerArgumentExpression] 自动捕获
public void Save(string fileName, Stream content)
{
ArgumentNullException.ThrowIfNull(fileName);
ArgumentNullException.ThrowIfNull(content);
}
該当するシナリオ:
- パブリック API/コンストラクターの依存関係注入パラメーター (
IService、ILoggerデリゲート コールバック)。 - ツール メソッドのコア入力パラメーター (パス、SQL、式ツリー、コレクション)。
- 適用できない: オプションのパラメータ (デフォルト値を指定)、「見つからない」状況 (null を返す必要があります /
TryXxx/ 投げるKeyNotFoundException)。
特に注意が必要な 2 つの点:
① ThrowIfNull 直接使用することはできません Nullable(これはボックス化され、奇妙なセマンティクスを持ちます)。手動で判断するか、 if (value is null) モデル;
② .NET 7 それ以来 void* ポインターのオーバーロード、アンマネージド相互運用シナリオでは、ターゲット フレームワークに注意を払う必要があります。
ArgumentNullException は null をインターセプトできますが、”” はインターセプトしません。 ArgumentNullException.ThrowIfNull(“”) は直接解放します。 “” (string.Empty と同等) は、ヒープ上のアドレスと Length プロパティ (値 0) を持つ、null ではない実際の文字列オブジェクト インスタンスです。 ThrowIfNull は内部的に 1 つのことだけを実行します: if (arg is null) throw … したがって、空の文字列は完全に正当であり、チェックはサイレントにパスします。
| 書き方 | null |
"" |
" " |
"abc" |
|---|---|---|---|---|
ThrowIfNull(s) |
❌ 投げる | ✅ 合格 | ✅ 合格 | ✅ 合格 |
ThrowIfNullOrEmpty(s) (.NET 7+) |
❌ 引数NullException | ❌ ArgumentException をスローする | ✅ 合格 | ✅ 合格 |
ThrowIfNullOrWhiteSpace(s) (.NET 8+) |
❌ANEを投げる | ❌ 投げAE | ❌ ArgumentException をスローする | ✅ 合格 |
2.3 引数範囲外例外
セマンティクス:パラメータは数値または順序数量メソッドが約束した有効範囲を超えている。それはです ArgumentException のサブクラスなのでキャッチします ArgumentException それも釣れます。
主要メンバー:ActualValue(呼び出し元から渡された実際の値を取得することは、オンラインの問題のトラブルシューティングに非常に役立ちます)。ParamName。
// 従来の 3 つのパラメータの構造: paramName、actualValue、message public byte[] ReadChunk(int count) { if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count), count, "0 より大きくなければなりません"); } // インデックス/容量クラス: 範囲外の値と上限の両方をメッセージに書き込みます。これは、BCL public T At の一貫したスタイルです。(IList list, int インデックス) { ArgumentNullException.ThrowIfNull(list); if ((uint)index >= (uint)list.Count) throw new ArgumentOutOfRangeException(nameof(index),index,$"インデックスが必要です [0, {list.Count}) 范围内");
}
.NET 8+ 的数值校验全家桶(推荐优先用):
ArgumentOutOfRangeException.ThrowIfNegative(count); // < 0
ArgumentOutOfRangeException.ThrowIfZero(count); // == 0
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); // <= 0
ArgumentOutOfRangeException.ThrowIfGreaterThan(count, 100);// > 100
ArgumentOutOfRangeException.ThrowIfLessThan(count, 1); // < 1
ArgumentOutOfRangeException.ThrowIfEqual(flag, 0); // == 0
适用场景:集合索引与容量、分页 pageSize/pageIndex、超时时间、重试次数、端口号、缓冲区大小、枚举转整型后的范围、日期区间(start > end)、Enum.ToObject 前的校验。
经验:“小于等于零”比“小于零”更常用。BCL 里表示数量、长度的参数几乎都要求 > 0,所以 ThrowIfNegativeOrZero 出场率远高于 ThrowIfNegative。
2.4 DuplicateWaitObjectException
语义:一个同步对象(WaitHandle)数组里出现了重复项,导致等待语义不明确(同一个内核句柄被等待两次,唤醒计数无法确定)。
- 继承链:
ArgumentException→ 所以它也有ParamName。 - HRESULT:
COR_E_DUPLICATEWAITOBJECT (0x80131529)。 - 谁在抛:几乎全是运行时自己在抛 ——
WaitHandle.WaitAll、WaitHandle.WaitAny发现数组中有重复 OS 句柄时;某些Monitor/线程原语实现路径也会产生。 - 你几乎不需要手动抛它。它属于”框架级契约异常”,就像
IndexOutOfRangeException一样,是平台替你表达的约束。
唯一可能自己抛的场景:自己写的并发原语(比如:封装一个 WaitAnyEx、自定义 Barrier、基于句柄池的多路复用器),并且 API 也接受 WaitHandle[] または同様の「オブジェクト コレクション」パラメータ。この場合、BCL の動作と一致させるために、次のようにスローできます。
public static int WaitAnyEx(WaitHandle[] handles, TimeSpan timeout)
{
ArgumentNullException.ThrowIfNull(handles);
if (handles.Length == 0)
throw new ArgumentException("等待数组不能为空", nameof(handles));
// 用引用相等判断重复(与 WaitHandle 内部逻辑一致)
for (int i = 0; i < handles.Length; i++)
for (int j = i + 1; j < handles.Length; j++)
if (ReferenceEquals(handles[i], handles[j]))
throw new DuplicateWaitObjectException(nameof(handles));
return WaitHandle.WaitAny(handles, timeout);
}
発信者は何をすべきでしょうか?:一般的にキャッチして再試行しないでください(再試行結果は同じです)、ログを記録し、受信アレイのソースを修復する必要があります。コードが古いフレームワーク/UWP と互換性がある場合は、直接キャッチします ArgumentException より安全です (この例外は一部の SKU ではエクスポートされません)。
3. 最初に ThrowIf シリーズを使用することが推奨される理由 (入力を減らすためだけではありません)
3.1 パラメータ名を自動的に追加して nameof copy エラーを排除する
// 这种 bug 真实存在:检查的是 a,写的却是 nameof(b)
if (a == null) throw new ArgumentNullException(nameof(b)); // 编译器不报错!
ArgumentNullException.ThrowIfNull(a); // 通过 [CallerArgumentExpression("argument")] 自动填入 "a"
3.2 パフォーマンスの向上
これらの方法は、 [DoesNotReturn] + インライン対応の静的ヘルパーである JIT は、特定して最適化することができ、非例外パス上で例外オブジェクトを構築するオーバーヘッドを回避します (例外オブジェクトは実際に障害が発生した場合にのみ作成されます)。
.NETアナライザー CA1512 直接します if (...) throw new ArgumentOutOfRangeException(...) 黄色でマークされ、対応するものと交換するように求められます ThrowIfXxx。
3.3 バージョンの簡単な比較
| 必要 | 古い書き方 | 最小バージョン |
|---|---|---|
| null 以外の | if (x==null) throw ... |
.NET6 ThrowIfNull |
| 空でない文字列 | if (string.IsNullOrEmpty(s)) throw ... |
.NET 7 ThrowIfNullOrEmpty |
| 空白以外の文字列 | if (string.IsNullOrWhiteSpace(s)) throw ... |
.NET 8 ThrowIfNullOrWhiteSpace |
| 数値範囲 | if (n <= 0) throw ... |
.NET 8 ThrowIfNegativeOrZero 待って |
| 対象フレームワークは上記バージョンより下位です | 自分でヘルパーの層をシールします (以下を参照) | 任意 |
低バージョン フレームワーク向けのセルフレスキュー ソリューション(BCL のアイデアをコピーして、数十行で完成させます):
internal static class Guard
{
public static void ThrowIfNull([NotNull] object? arg,
[CallerArgumentExpression(nameof(arg))] string? name = null)
{
if (arg is null) throw new ArgumentNullException(name);
}
public static void ThrowIfNullOrEmpty(string? arg,
[CallerArgumentExpression(nameof(arg))] string? name = null)
{
if (arg is null) throw new ArgumentNullException(name);
if (arg.Length == 0) throw new ArgumentException("值不能为空字符串", name);
}
public static void ThrowIfOutOfRange(T value, T min, T max,
[CallerArgumentExpression(nameof(value))] string? name = null) where T : IComparable
{
if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0)
throw new ArgumentOutOfRangeException(name, value, $"必须在 [{min}, {max}] 内");
}
}
知らせ:[CallerArgumentExpression] デフォルト値を持つ文字列のオプションパラメータのみを変更できます。属性内の名前はターゲットの仮パラメータと厳密に一致している必要があります。そうでない場合は、暗黙的に無効になります。
4. ベスト プラクティス リストと完全な例
以下に、いくつかの簡単なシナリオと推奨される処理方法を示します。
- チェック境界でのみスローされます: パブリック API、コンストラクター、保護された仮想メソッド エントリ。プライベート メソッドは内部で複数の目的で使用されます
Debug.Assert; パフォーマンス重視のホット パスでは冗長なチェックを実行しないでください (JIT インライン化によって排除できる単純な Null チェックを除く)。 - 固定順序: 最初に null、次にコンテンツ/フォーマット、最後に範囲制約とクロスパラメータ制約。このようにして、失敗メッセージは常に「最もエラーが多い」ことを示しており、これは安定していて予測可能です。
- 常に使用する
nameofまたは ThrowIf 自動キャプチャ文字列を決して手書きしないでください"path"。リファクタリングと名前変更を行う場合、前者はコンパイルしてエラーを報告しますが、後者はコンパイルしません。 - 手で投げないでください
NullReferenceException。これは、実行時に null を逆参照した結果です。 null パラメータは「呼び出し元のデフォルト」であり、意味的にはそうである必要があります。ArgumentNullException。この 2 つは、アラームの監視、デバッガーの動作、Roslyn 分析の点で異なります。同じ理由で使用しないでくださいException、ApplicationExceptionパラメータの検証を行います。 - 「パラメータエラー」と「ステータスエラー」を区別する: ファイルパスは正当ですが、ファイルが存在しません → スローします
FileNotFoundException;ストリームは閉じられていますが、まだ読む必要があります→ObjectDisposedException; イテレータの開始後にコレクションを変更します →InvalidOperationException。これらはいずれも、このセクションの引数ファミリーには属しません。 - DTO/インターフェイス境界には検証フレームワークを使用し、ドメイン層には例外を使用します: ASP.NET Core では、パラメーターを入力するときに FluentValidation/データ アノテーション (400 を返す) を使用し、サービス レイヤー内で引数例外 (500 または統一エラー コードを返す) を使用することをお勧めします。この 2 つを混在させると、フロントエンドでフィールド レベルのエラーではなくスタック エラーが発生します。
- キャッチするときはサブクラスで細分化してみてください:
try { svc.Do(x, n); }
catch (ArgumentNullException e) when (e.ParamName == nameof(x)) { /* 必传缺失 */ }
catch (ArgumentOutOfRangeException e) { /* 用 e.ActualValue 做降级 */ }
catch (ArgumentException) { /* 其他参数问题 */ }
- 列挙型とジェネリック型:
Enum.IsDefined不正な整数をチェック →ArgumentException;ジェネリックwhere T : classときに使用されますThrowIfNull制約のないジェネリックの場合は、null を手動でチェックし、明確なパラメーター名を指定します。
上記のシナリオをまとめた簡単な例:
public sealed class LogRotator
{
private readonly string _dir;
private readonly int _maxFiles;
private readonly long _maxBytes;
public LogRotator(string directory, int maxFiles, long maxBytesPerFile)
{
ArgumentException.ThrowIfNullOrWhiteSpace(directory);
if (directory.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
throw new ArgumentException("目录名包含非法字符", nameof(directory));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxFiles);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxBytesPerFile);
_dir = Path.GetFullPath(directory);
_maxFiles = maxFiles;
_maxBytes = maxBytesPerFile;
}
public void Rotate(ICollection files, int startIndex, int count)
{
ArgumentNullException.ThrowIfNull(files);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
if (startIndex + count > files.Count)
throw new ArgumentOutOfRangeException(nameof(count), count,
$"从 {startIndex} 开始的 {count} 个元素超出集合大小 {files.Count}");
var handleArray = files.Select(f => new ManualResetEvent(false)).ToArray();
// 这里如果数组里有重复句柄,WaitAny 会自己抛 DuplicateWaitObjectException
// —— 我们不用、也不应该替它抛
}
}
5. 簡単なまとめ
ArgumentNullException 「伝わるかどうか」は関係ない。ArgumentException 「メッセージが正しいかどうか」に関わらず、ArgumentOutOfRangeException 「境界を越えて広がる」でもいいDuplicateWaitObjectException 実行時に同期プリミティブとして管理されており、基本的には触れる必要はありません。
フレームワークの上位バージョンにアップグレードできる場合はそれを使用してください ThrowIfXxx このシリーズは短くて速く、間違ったパラメータ名を書くことはありません。