常见陷阱
本章汇总 C# 最常见、最隐蔽的陷阱。这些坑在 AutoCAD/Revit 开发中也频繁出现,提前了解可以省去大量调试时间。
1. 字符串拼接性能陷阱
// ❌ 循环中直接拼接 —— 每次创建新字符串,O(n²)
string result = "";
for (int i = 0; i < 100000; i++)
result += i.ToString(); // 生成 100000 个临时字符串!
// ✅ 使用 StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 100000; i++)
sb.Append(i);
string result = sb.ToString();
// ✅ 或使用 string.Join
string result = string.Join("", Enumerable.Range(0, 100000));🔬
string在 C# 中是不可变的。每次+=都会创建全新的字符串对象并复制旧内容。这在 CAD 导出大量坐标数据时尤为致命。
2. 值类型装箱 (Boxing)
// ❌ 装箱:值类型隐式转为 object,在堆上分配
int value = 42;
object boxed = value; // 装箱发生
int unboxed = (int)boxed; // 拆箱
// ❌ 常见的隐式装箱场景
string s = $"数字:{42}"; // int → object
Console.WriteLine("值:{0}", 42); // int → object(params object[])
var list = new ArrayList(); // 古老的非泛型集合
list.Add(42); // 装箱⚡ AutoCAD 的
ResultBuffer中存储TypedValue时使用值类型没有问题,但如果将其转为object数组就会产生大量装箱。
3. foreach 中修改集合
// ❌ 运行时抛出 InvalidOperationException
var list = new List<int> { 1, 2, 3, 4, 5 };
foreach (var item in list)
{
if (item % 2 == 0)
list.Remove(item); // 💥 InvalidOperationException
}
// ✅ 正确做法 1:倒序 for 循环
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i] % 2 == 0)
list.RemoveAt(i);
}
// ✅ 正确做法 2:LINQ 生成新集合
var filtered = list.Where(x => x % 2 != 0).ToList();🚨 AutoCAD 中选择集、Revit 中元素集合的遍历同理——遍历过程中不应修改源集合。
4. async void 陷阱
// ❌ async void:异常无法被捕获,直接崩溃
public async void ExecuteAsync()
{
await Task.Delay(1000);
throw new Exception("无法被捕获!"); // 进程级崩溃
}
// ✅ 事件处理器用 async void,但内部必须 try-catch(唯一的例外场景)
button.Click += async (s, e) =>
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
};
// ✅ 其余所有场景一律使用 async Task
public async Task ExecuteAsync()
{
await Task.Delay(1000);
throw new Exception("可以被 await 捕获");
}5. Null 引用
// ❌ 直接访问
string? s = GetPossiblyNull();
int len = s.Length; // ❌ 可能 NullReferenceException
// ✅ 空检查模式
if (s is not null)
Console.WriteLine(s.Length);
// ✅ 空条件操作符
int? len = s?.Length; // s 为 null 则整个表达式为 null
// ✅ 空合并
string value = s ?? "默认值";
// ✅ 空合并赋值
s ??= "默认值"; // 仅当 s 为 null 时赋值
// 🚨 AutoCAD/Revit API 典型陷阱
Entity entity = tr.GetObject(id, OpenMode.ForRead) as Entity;
// entity 可能为 null(id 无效或已删除的类型错误)
double area = entity.Area; // 💥 NullReferenceException6. using 遗漏导致资源泄漏
// ❌ 未释放
var reader = new StreamReader(path);
string content = reader.ReadToEnd();
// reader 未被 Dispose,文件句柄泄漏
// ✅ using 声明
using var reader = new StreamReader(path);
string content = reader.ReadToEnd();
// 🚨 AutoCAD 事务未释放是最常见的资源泄漏
void BadCode(Database db)
{
Transaction tr = db.TransactionManager.StartTransaction();
var line = tr.GetObject(id, OpenMode.ForRead) as Line;
// tr 未 Commit 也未 Dispose —— 资源泄漏!
}
void GoodCode(Database db)
{
using var tr = db.TransactionManager.StartTransaction();
var line = tr.GetObject(id, OpenMode.ForRead) as Line;
tr.Commit();
}7. LINQ 延迟执行陷阱
// 🚨 陷阱:查询在迭代时才执行,捕获的变量值已改变
int threshold = 10;
var query = entities.Where(e => e.Length > threshold);
threshold = 50; // 修改了捕获的变量
// 迭代时 threshold 值为 50,不是 10!
var result = query.ToList(); // 使用 threshold = 50
// ✅ 立即执行
int threshold = 10;
var result = entities.Where(e => e.Length > threshold).ToList();
// 之后修改 threshold 不影响 result8. 浮点数比较
// ❌ 直接比较浮点数
double x = 0.1 + 0.2;
if (x == 0.3) { } // false! 浮点精度问题
// ✅ 使用容差比较
const double Tolerance = 1e-9;
if (Math.Abs(x - 0.3) < Tolerance) { }
// AutoCAD 中的 Point3d 比较
if (point1.DistanceTo(point2) < Tolerance.Global.EqualPoint)
{
// 两点可视为重合
}9. 闭包中的循环变量
// ❌ C# 5 之前的陷阱(已修正),但仍有变种
var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i)); // 捕获变量 i
}
foreach (var action in actions)
action(); // 输出 5, 5, 5, 5, 5!全部是循环结束时的值
// ✅ 复制局部变量
for (int i = 0; i < 5; i++)
{
int local = i; // 每次迭代创建新变量
actions.Add(() => Console.WriteLine(local));
}
// 输出 0, 1, 2, 3, 4
// ✅ C# 5+ foreach 中自动修正
foreach (var item in items)
{
actions.Add(() => Console.WriteLine(item)); // 每次迭代 item 独立
}10. 重写 Equals 不重写 GetHashCode
// ❌ 只重写 Equals
public class Point
{
public int X, Y;
public override bool Equals(object? obj) => obj is Point p && p.X == X && p.Y == Y;
// 未重写 GetHashCode!
}
var set = new HashSet<Point>();
set.Add(new Point { X = 1, Y = 2 });
bool contains = set.Contains(new Point { X = 1, Y = 2 }); // false!HashSet 先查 hash💡 C# 9+ 的 record 类型自动生成正确的 Equals 和 GetHashCode。在 .NET Framework 4.8 中,如果自定义类型要放入
Dictionary或HashSet作为 key,务必重写两者。
11. 空合并操作符的短路
// ?? 和 ??= 是短路的
string? GetValue() => null;
string result = GetValue() ?? ExpensiveFallback(); // GetValue() 返回 null 才执行 ExpensiveFallback
// ?. 也是短路的
int? length = obj?.Items?.Count; // obj 或 Items 为 null 时返回 null,不抛异常12. 对集合使用空值
// ✅ 返回空集合而非 null
public List<Entity> GetEntities()
{
// ❌ return null;
return new List<Entity>(); // 避免调用方检查 null
}
// ✅ 使用 Enumerable.Empty<T>()(零分配)
public IEnumerable<Entity> GetEntities()
{
return Enumerable.Empty<Entity>(); // 返回共享的静态空实例
}