集合与索引类异常【C# Exception 详解系列(三)】 – 橙子家

集合与索引类异常【C# Exception 详解系列(三)】 – 橙子家


This article mainly introduces: collection and index exceptions, mainly including these types: IndexOutOfRangeException, KeyNotFoundException, ArrayTypeMismatchException, RankException, InvalidOperationException.

〇.Foreword

The previous article has listed all the common abnormal types. This article continues to introduce the abnormal characteristics of each category.

This article mainly introduces:Collection and index exceptionsmainly including these types:IndexOutOfRangeException、KeyNotFoundException、ArrayTypeMismatchException、RankException、InvalidOperationException。

Click the link to view this series of articles:C# Exception detailed explanation series.

1. Overview of collection and index exceptions

abnormal Who threw it One sentence reason Preferred alternative
IndexOutOfRangeException array, string,Span、Memorymultidimensional array Subscript <0 or ≥ Length judge Length/Count、^ and scope,ElementAtOrDefault
KeyNotFoundException Dictionary/ConcurrentDictionary/SortedDictionary of [key] key does not exist TryGetValue、GetValueOrDefault(.NET 8+)、TryAdd
ArrayTypeMismatchException When writing a covariant array The actual number assembly at runtime cannot fit into this type. Don’t use covariant arrays, use List/IReadOnlyList
RankException Array.Copy/CopyTo/ConstrainedCopy wait The source and target dimensions (Rank) are different judge Rank、GetLength(d)distinguish between jagged arrays and multidimensional arrays
InvalidOperationException Enumerator version verification, empty set element retrieval, etc. The collection was modified/the object is in an error state Reverse order traversal, snapshot,RemoveAllconcurrent collection

These 5 anomalies have one thing in common:They almost all mean “the program logic is wrong” rather than “the user input is bad”. Therefore the general strategy is “use checks or safer APIs to eliminate exceptions before they occur” rather than in the outer layer catch. The only exception is InvalidOperationExceptionhalf of its scenarios are state machine/concurrency issues, which need to be avoided by design.

core principles: Anything that can be judged by length,Try* If the API, immutable snapshots or type system (generic immutability) block it at compile time/call time, don’t leave it to the runtime.

Among these five anomalies, except InvalidOperationException Half of the scenarios need to be solved by concurrent design, and the remaining four can achieve “zero online occurrence”.

集合与索引类异常【C# Exception 详解系列(三)】 – 橙子家

2. Introduction and simple examples of each exception

2.1 IndexOutOfRangeException – out-of-bounds access

Trigger condition: subscript < 0 or ≥ Length。

Note that it is an exception to the “array/contiguous memory” semantics:string[i]、T[]、Spanmulti-dimensional arrays will throw it; and List[i] Throwing is ArgumentOutOfRangeException——This is the easiest thing to remember when interviewing and troubleshooting.

// ① 切分后想当然地取下标(外部数据最常见)
var parts = "a,b".Split(',');
var third = parts[2];                 // 💥 IndexOutOfRangeException

// ② 差一错误(<=)
int[] a = new int[5];
for (int i = 0; i <= a.Length; i++)   // 💥 i == 5 时
    a[i] = i;

// ③ C# 8 的 ^ 运算符:^0 本身就是非法的
var last = a[^0];                     // 💥 永远是越界,编译不报错

// ④ 多维数组按“扁平思维”访问
int[,] m = new int[3, 4];
var v = m[0, 12];                     // 💥 每维独立校验

// ✅ 正确姿势
if (parts.Length > 2) third = parts[2];          // 明确长度判断
third = parts.ElementAtOrDefault(2) ?? "";       // LINQ,性能略低但安全
var last = a[^1];                                // ^1 才是最后一个
// Span/Memory 场景用 TrySlice / Slice 前先判 Length
if (span.Length >= 3) var chunk = span.Slice(0, 3);

Special attention:

  • performance level: JIT will “promote” the bounds check inside the loop to outside the loop (bounds check elision), soManually add if (i < len) It does not slow down, but often helps JIT eliminate checks.;butDon’t use unsafe Pointers to bypass checks in exchange for performancecrossing the boundary will cause memory corruption, which is difficult to reproduce.
  • Empty set collection [0] It is one of the most frequent sources of transgression online, especially FirstOrDefault() There is no default value after that,Split/The regular matching result is empty and the JSON array is empty.
  • Multiple threads share a variable index (shared int index++) will occasionally cross the boundary. This type of bug only appears during stress testing.

2.2 KeyNotFoundException – dictionary key does not exist

// 异常触发示例
var cfg = new Dictionary { ["Host"] = "localhost" };
var port = cfg["Port"];             // 💥 The given key 'Port' was not present in the dictionary.

// ✅ 单次查找:TryGetValue(避免 ContainsKey + 索引器 的两次哈希)
if (cfg.TryGetValue("Port", out var port)) { ... }

// ✅ .NET 6+:直接给默认值,语义最干净
var port = cfg.GetValueOrDefault("Port", "5432");

// ✅ 需要“没有就插入”时,避免两次查找(.NET 7+)
ref var val = ref CollectionsMarshal.GetValueRefOrAddDefault(cfg, "Port", out bool exists);
val = "5432";   // ref 直接写回,零额外分配、零二次哈希

// ✅ 并发场景
var cd = new ConcurrentDictionary();
cd.GetOrAdd("k", _ => Compute());   // 不要 cd["k"] ?? cd["k"] = ...

Special attention:

  1. key equivalence: Custom type keys must be rewritten at the same time GetHashCode and Equals(record/record struct has been synthesized); useMutable objects or arrays as keysIt is a classic trap – the object is modified after being entered into the dictionary, the hash bucket does not match, and the key is “lost”, which appears to be intermittent. KeyNotFoundException。
  2. Capitalization and culture: String keys are case-sensitive by default and use sequential comparison; configuration/protocol parsing suggestions StringComparer.OrdinalIgnoreCase。
  3. concurrent:Dictionary<,> Not thread-safe, multi-threaded reading and writing may not only throw KeyNotFoundExceptionthere may also be an internal infinite loop/data corruption. More reading and less writing are available ConcurrentDictionary Or read-write lock.
  4. value type key(such as tuples, structs) will cause boxing and large memory overhead; consider high-frequency scenarios Span-based search or ImmutableDictionary。
  5. When deserializing or configuring missing fields, don’t rely on exceptions to determine whether a field exists – use TryGetValue Only then can we distinguish between “missing configuration” and “wrong program”.

2.3 ArrayTypeMismatchException – writing trap of covariant array

Arrays of reference types allow covariance (string[] → object[]), but the runtime object is still an array of subtypes, soType verification is required for every write. If it fails, an exception will be thrown at the line written.。An error is never reported at the assignment point, and the error is postponed to runtime.

object[] arr = new string[1];
arr[0] = "hi";   // ✅
arr[0] = 42;     // 💥 ArrayTypeMismatchException
arr[0] = null;   // ✅ null 永远兼容
// object[] b = new int[3];  // ❌ 值类型不参与协变,编译期就拦住
# scene illustrate
1 params object[] When the actual parameter happens to be a covariant array, it is transparently transmitted at zero cost and without cladding.Log("api", tags) inside tags Still String[]it will explode when written in the method.(object[])tags Just reference conversion cannot save it;(object)tags Only cladding, but the semantics become “an element”
2 Array.SetValue /reflection The parameters are objectthe compiler has no information, and everything is postponed to the runtime storage point.
3 Array.Copy When the dimensions are the same but an element cannot be assigned, element-by-element verification may occur. It may take half of the copying before exploding, indicating that the target array has been contaminated. Use “all or nothing” Array.ConstrainedCopy
4 Generic handle T[] promoted to object[]/Array to write where T : class hour object[] o = arr; Compile passed;dynamic d = arr; d[0]=42; Bypass compilation checks completely
5 IList Covariant writing IList 💥
6 Unsafe.As / MemoryMarshal.Cast Even the runtime verification is bypassed – no exception is thrown, and the heap is directly damaged. This is a counterexample to the illusion that covariance is at most just throwing exceptions.

cost:

  • Type safety: Errors are pushed from compile time to runtime, and the stack is far away from the root cause.
  • Performance: Insert type verification for each write;Array.Copy When the element types are inconsistent, it degenerates into element-by-element copying and loses memmove Fast path.
  • API: The same method passes literals normally, but passes an existing array and it explodes, forming a “Heisenberg-style” interface.
// 【如何避免?】
// A. 对外只用只读视图,物理上写不进去
void Print(IReadOnlyList items) { }
void Process(ReadOnlySpan data) { }

// B. params 方法体视为只读;要副本就造真 Object[]
object[] snap = new object[args.Length];
Array.Copy(args, snap, args.Length);   // ⚠️ (object[])args.Clone() 救不了,仍是 String[]

// C. 泛型写回保持 T[] 本体
static void Fill(T[] arr, T val) => Array.Fill(arr, val);

// D. 异构容器别用数组:元组/record、List

Several common troubleshooting points:

  1. criterion: An array or array of “read base class, write hope subclass” appears IListis a reserve; if replaced by an immutable generic collection, the problem disappears on the spot.
  2. Three-piece diary set:array.GetType().Name(real runtime type),GetElementType()value to be written GetType()。
  3. Check “Break when thrown” in VS exception settings, it does not interrupt by default.
  4. Add one more test: pass in the derived class array + write incompatible values.
  5. Don’t catch it downgrade——This is a logical error, not a data problem.
  6. non-generic collection (ArrayList) will not throw this, but inTake out and force transfertime throw InvalidCastExceptiondon’t be confused.

2.4 RankException – Dimension mismatch

Rank is the number of dimensions of the array (int[,] → 2,int[][] → 1). This exception basically only appears inArray copy family methodsmiddle:

int[,] src = new int[2, 3];
int[] dst = new int[6];
src.CopyTo(dst, 0);             // 💥 RankException(2 维拷不进 1 维)

int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4 } };
int[,] rect = new int[2, 2];
Array.Copy(jagged, rect, 2);    // 💥 RankException(jagged 的 Rank=1,且元素类型也不兼容)

// ✅ 先校验再按维遍历
void CopyChecked(Array src, Array dst)
{
    if (src.Rank != dst.Rank) throw new ArgumentException("维度数必须一致");
    for (int d = 0; d < src.Rank; d++)
        if (src.GetLength(d) != dst.GetLength(d))
            throw new ArgumentException($"第{d}维长度不等");
    src.CopyTo(dst, 0);
}

Special attention:

  • jagged array T[][] ≠ multidimensional array T[,]. The former is a “one-dimensional array within a one-dimensional array”, and each row can have different lengths and can be discontinuous on the managed heap; the latter is a single continuous block. Mutual copying between the two will lead to problems, and serialization (BinaryFormatter/old protocol) interoperability is often pitted.
  • Array.CreateInstance can makenon-zero lower boundarray(GetLowerBound(d) != 0), this kind of array will behave strangely when passed to many APIs that only recognize SZArray; use Array.IsSZArray(reflection/internal) or unified requirements GetLowerBound(d)==0 Be defensive.
  • APIs that only accept one-dimensional arrays (such as Array.Sort、BinarySearch,most Buffer/Marshal operation) usually throws when passing multi-dimensional array ArgumentException rather than RankException——Don’t do it just because you didn’t see it RankException I thought the dimensions were okay.
  • High-performance scenarios are preferred Span/Memory(Natural one-dimensional continuous)say goodbye to dimensionality problems completely.

2.5 InvalidOperationException – Collection modified during enumeration

The enumerator holds internally _versionthe collection will increase automatically every time it is added, deleted or modified;MoveNext() If version inconsistency is found, it will be thrown immediately. "Collection was modified; enumeration operation may not execute."

var list = new List { 1, 2, 3 };
foreach (var x in list)
    if (x == 2) list.Remove(x);        // 💥 下一轮 MoveNext 抛 InvalidOperationException

var dict = new Dictionary{["a"]=1};
foreach (var k in dict.Keys) // 会抛异常的是增删操作(Add、Remove、Clear),它们会递增 _version
    dict.Remove(k);          // 💥 抛异常,InvalidOperationException
    // dict[k] = 2;          // 更新已有键的值时不会递增 _version,因此枚举器在下一轮 MoveNext 检测到版本一致,不会抛出异常

// ✅ 1) 倒序索引遍历(删除不移位影响后续下标)
for (int i = list.Count - 1; i >= 0; i--)
    if (list[i] == 2) list.RemoveAt(i);

// ✅ 2) 先快照再遍历(简单粗暴,小集合首选)
foreach (var x in list.ToList())
    if (x == 2) list.Remove(x);

// ✅ 3) 用集合自身的高效批量 API
list.RemoveAll(x => x == 2);           // 一次遍历、无额外分配

// ✅ 4) 收集再删除(跨多个集合/复杂条件)
var toRemove = new List();
foreach (var kv in dict) if (kv.Value < 0) toRemove.Add(kv.Key);
foreach (var k in toRemove) dict.Remove(k);

// ✅ 5) 真正的并发读写
var cd = new ConcurrentDictionary();
// 注意:ConcurrentDictionary 的枚举器是“某一时刻的快照”,修改不抛异常,
// 但可能看到重复/遗漏项——业务上仍需按幂等设计。

Special attention:

  • Delayed execution is the invisible killer:var q = list.Where(...) It’s just an expression tree. The actual enumeration may occur after several layers of calls, or even in another thread, when the original collection has already been changed. need as early as possible .ToList()/.ToArray() Materialize, or pass in an immutable collection.
  • Thread safety ≠ do not throw this exception: Multiple threads share one Dictionary Even without modification, it may be thrown away due to damage to the internal structure. IndexOutOfRangeException Or an infinite loop.ConcurrentDictionary It only guarantees its own consistency and does not guarantee “global quiescence during enumeration”.
  • Pitfalls of structure enumerators:List.GetEnumerator() Returns struct if boxed into IEnumerator(like foreach (var x in (IEnumerable)list)),every time Current They are all copies, and there may be a strange phenomenon of “always stopping at the first item”; at the same time, the modification detection is still in effect.
  • Other common sources of the same exception (to avoid misjudgment when searching logs):Single()/First() empty collection,Queue.Dequeue/Stack.Pop null,IEnumerator not yet MoveNext Study CurrentLINQ OrderBy Comparator violates contract, EF Core’s DbContext concurrent use,Task repeat Start、yield return Illegal status in iterator.
  • The most typical in ASP.NET Core: use the mutable collection injected as Scoped/Singleton as a global cache and update it during enumeration – or replace it with ConcurrentDictionaryor lock + Snapshot.

3. General checklist

  1. Classify first:bring Index/Key/Rank/TypeMismatch of = bug;InvalidOperationException = status or concurrency issues. The first four categories are not recommended for production use try/catch Swallowed, it should be allowed to explode and take context.
  2. Logs must contain “boundary information”: Remember when throwing index / Length / Count / key / actualType / Rank / GetLength(d)90% of out-of-bounds cases can be located by looking at this line.
  3. Boundary use case single test: Empty set, single element, exactly full, one more, zero-length array, empty string, split result, non-zero lower bound array, interleaved array.
  4. Replace handwritten subscripts with language features:foreach、^ and ..、Spanpattern matching,ref Return; used when subscripting is required for (int i = 0; i < c.Count; i++) And cache the upper limit to local variables (not only to prevent concurrent tampering with the length, but also to help JIT eliminate boundary checks).
  5. Dictionaries always go TryGetValue / GetValueOrDefault; Need to use “calculate and insert if it does not exist” GetOrAdd or GetValueRefOrAddDefault。
  6. Eliminate covariant arrays at the API design level:for parameters IReadOnlyList/ReadOnlySpanreturn an immutable collection;params object[] Be careful with overloading the actual array type passed in.
  7. concurrent: Mutable collections cannot be locked or thread bounded; use them if you need to share them. Concurrent* or Immutable*;Anything that needs to be updated during enumeration must be “snapshot or new replacement”.
  8. Debugging Tips: VS Lip IndexOutOfRangeException Check “Break on this exception type” (it is sometimes not interrupted in the default settings); for occasional problems, use Debug.Assert(index >= 0 && index < arr.Length)assertion failure occurs earlier and with more complete information than exceptions.

4. A small summary

Although these five anomalies have different trigger points, theyThe essence is that the contract is broken, not “the data is bad”。

  • IndexOutOfRangeException / KeyNotFoundException 是“Transgression and Absence”which means no verification is done before access;
  • ArrayTypeMismatchException / RankException 是“Type does not match dimension”resulting from deferring information that should be guaranteed by the compiler or the caller to the runtime (covariant arrays, multi-dimensional array copies);
  • InvalidOperationException is“Wrong status”mostly modifying the collection during enumeration or concurrently sharing mutable state.

Their common characteristics are:The location where the exception is thrown is often far away from the root cause.——No error will be reported at the assignment point, but an error will be reported at the writing point; the enumerator will only report an error in the next round of MoveNext; params object[] The behavior depends on whether the caller passes a literal or an existing array. therefore,Catching them as “recoverable errors” is almost always a mistake; the correct approach is to eliminate them at entry and boundaries.

When it comes to engineering practice, you can only remember three rules:

  1. Use Try* and length judgment to replace subscripts and indexers(TryGetValue, GetValueOrDefault, ElementAtOrDefault, reverse order traversal, RemoveAll);
  2. Use the type system to block covariance and dimensionality issues(Only IReadOnlyList is exposed to the outside world / ReadOnlySpan,params object[] Treat it as read-only. If you want a copy, create a real object.[]generic code never converts T[] promoted to object[]);
  3. Keep mutable collections from being locked or thread bounded, and snapshots or new replacements will be made during enumeration.。

Coupled with the boundary information that must be recorded in the log (Length/Count/key/Rank/real runtime type), the check “Interrupt when thrown” in VS, and the single test of empty collection and difference by one, the first four exceptions can be zero online, and the remaining InvalidOperationException can basically be converged by the concurrency design.

In one sentence:Anything that can be blocked by length judgment, Try* API, immutable snapshots, and generic immutability should not be left to the runtime.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *