List未实例化就调用Add会抛NullReferenceException;正确写法是new List()或C#9+的[];类字段需在构造函数或声明时初始化。

为什么 List<t></t> 初始化后 Add 会报“未将对象引用设置到对象的实例”
这是最常踩的坑:声明了 List<t></t> 变量但没用 new 实例化。C# 中所有引用类型变量默认值是 null,调用 Add 就会直接抛 NullReferenceException。
- 错误写法:
List<string> names;</string>→ 后续names.Add("a")必崩 - 正确写法:
List<string> names = new List<string>();</string></string>或 C# 9+ 的List<string> names = [];</string> - 如果在类字段中声明,记得在构造函数里初始化,或用
= new List<t>()</t>直接赋默认值
List<t></t> 和 Array 在扩容和性能上到底差在哪
List<t></t> 底层用数组实现,但自动管理容量;Array 长度固定。关键差异不在“能不能变长”,而在“怎么变长”和“何时变长”。
- 首次创建空
List<int>()</int>,内部数组长度为 0;第一次Add后自动分配长度为 4 的数组 - 当容量不足时,
List<t></t>默认按 2 倍扩容(如从 4→8→16),每次扩容都要Array.Copy,有性能开销 - 如果已知最终大小(比如要存 1000 条日志),直接用
new List<log>(1000)</log>预分配容量,避免多次复制 -
Array没有扩容机制,想“变长”只能Array.Resize(本质也是新建+复制),不如List<t></t>封装得干净
用 foreach 遍历 List<t></t> 时修改集合会怎样
运行时直接抛 InvalidOperationException:“集合已修改;可能无法执行枚举操作”。这不是语法限制,而是 List<t>.Enumerator</t> 内部维护了一个版本号(_version),只要调用 Add、Remove、Clear 等任何改变结构的方法,版本号就变,枚举器检测到不匹配就炸。
- 安全做法:需要边遍历边删,改用
for (int i = list.Count - 1; i >= 0; i--)倒序删 - 或者先收集待删项:
var toRemove = list.Where(x => x.IsExpired).ToList();,再list.RemoveAll(x => x.IsExpired) - 注意:
list[i] = newValue(仅改值)不会触发版本号变更,允许在foreach中做
List<t></t> 转成数组或只读集合的几种方式及适用场景
不是所有“转”都等价——有的只是视图包装,有的是深拷贝,有的还带线程安全语义。
-
list.ToArray():返回新数组,内容拷贝,后续修改互不影响;适合传给只接受T[]的老 API -
list.AsReadOnly():返回ReadOnlyCollection<t></t>,底层仍指向原List<t></t>,只是封掉了修改方法;适合暴露给外部但不想让对方改内部状态 -
list.ToList():看似多余,实则有用——比如你有一个IEnumerable<t></t>,调用.ToList()强制立即执行并缓存结果,避免多次枚举副作用 - 别用
(IList<t>)list</t>强转来“假装只读”,这毫无意义,接口转换不改变可变性
实际项目里最容易被忽略的是预分配容量和枚举时修改集合这两条。前者影响性能,后者直接 crash,而且错误堆栈不直观——它不报在 Add 那行,而报在 foreach 的 MoveNext 里。


















