Skip to content
C#
常见陷阱与最佳实践

常见陷阱与最佳实践

本章汇总 AutoCAD .NET 二次开发中最常见的问题和推荐做法,帮你少踩坑。


陷阱 1:事务外访问 DBObject

// ❌ 致命错误 —— 访问已释放的对象
Line line;
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    line = tr.GetObject(id, OpenMode.ForRead) as Line;
    tr.Commit();
}
double len = line.Length;   // 💥 AccessViolationException!

// ✅ 事务内提取所需数据
double length;
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    Line line = tr.GetObject(id, OpenMode.ForRead) as Line;
    length = line.Length;          // 在事务内读取到内存
    // 或持有 ObjectId,需要时重新 GetObject
    tr.Commit();
}
Console.WriteLine(length);

💡 思路:在事务内将需要的值提取为 C# 原生类型(double, string, Point3d),事务外使用这些值。ObjectId 是值类型,事务外也可持有。


陷阱 2:忘记 AddNewlyCreatedDBObject

// ❌ 新对象未纳入事务
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    Circle circle = new Circle(center, Vector3d.ZAxis, 50);
    modelSpace.AppendEntity(circle);
    // 缺少 tr.AddNewlyCreatedDBObject(circle, true);
    tr.Commit();   // 💥 eNullObjectId 或崩溃
}

// ✅ 正确
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    Circle circle = new Circle(center, Vector3d.ZAxis, 50);
    modelSpace.AppendEntity(circle);
    tr.AddNewlyCreatedDBObject(circle, true);    // 纳入事务
    tr.Commit();   // ✅
}

陷阱 3:忘写 Commit

// ❌ 事务未提交
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    Line line = tr.GetObject(id, OpenMode.ForWrite) as Line;
    line.ColorIndex = 1;
    // 忘记 tr.Commit() —— 离开 using 时自动回滚
}
// 颜色未改变!

🚨 最隐蔽的 Bug:代码逻辑完全正确,但操作后毫无效果。原因通常是忘了 Commit()。在事务包装方法中将 Commit() 内建是好的实践。


陷阱 4:遍历中修改集合

// ❌ 在遍历模型空间时添加/删除实体
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    BlockTableRecord ms = (BlockTableRecord)tr.GetObject(
        bt[BlockTableRecord.ModelSpace], OpenMode.ForRead);

    foreach (ObjectId id in ms)      // 正在遍历
    {
        Entity ent = tr.GetObject(id, OpenMode.ForWrite) as Entity;
        ent.Erase();                  // 💥 遍历期间修改集合!
    }
    tr.Commit();
}

// ✅ 先收集 ObjectId,再批量操作
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    BlockTableRecord ms = (BlockTableRecord)tr.GetObject(
        bt[BlockTableRecord.ModelSpace], OpenMode.ForRead);

    var idsToModify = new List<ObjectId>();
    foreach (ObjectId id in ms)
    {
        Entity ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
        if (ent is Line)
            idsToModify.Add(id);
    }

    foreach (ObjectId id in idsToModify)
    {
        Entity ent = tr.GetObject(id, OpenMode.ForWrite) as Entity;
        ent.ColorIndex = 5;
    }
    tr.Commit();
}

陷阱 5:对象打开模式不当

// ❌ 使用 ForWrite 读取不修改的对象
Circle circle = tr.GetObject(id, OpenMode.ForWrite) as Circle;
double radius = circle.Radius;   // 只是读取,但用了 ForWrite
// 问题:不必要的写锁、产生撤销数据、文件膨胀

// ✅ 只读操作用 ForRead
Circle circle = tr.GetObject(id, OpenMode.ForRead) as Circle;
double radius = circle.Radius;

⚡ 使用 ForRead 而非 ForWrite 读取数据,显著减少 DWG 撤销数据大小并提高多文档并发性能。


陷阱 6:引用 Copy Local 未设为 False

// 问题:将 acmgd.dll / acdbmgd.dll 复制到了输出目录
// 症状:FileLoadException, 类型不匹配, 找不到方法
// 解决:项目引用 → 属性 → Copy Local = False

🔬 原因:AutoCAD 的托管 DLL 是特定版本的原生 C++/CLI 包装,与 AutoCAD.exe 绑定的原生代码耦合。如果把 DLL 复制到输出目录,可能加载了错误版本的 DLL。


陷阱 7:忘记 DowngradeOpen 导致死锁

// ❌ 升级后未降级,长期持有写锁
Entity ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (needModify)
{
    ent.UpgradeOpen();
    ent.Layer = "NEW";
    // 忘记 DowngradeOpen()
}

// ✅ 修改后降级
Entity ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (needModify)
{
    ent.UpgradeOpen();
    ent.Layer = "NEW";
    ent.DowngradeOpen();
}

陷阱 8:事件处理器内存泄漏

// ❌ 事件未取消订阅,GC 无法回收
Document doc = Application.DocumentManager.MdiActiveDocument;
doc.CommandEnded += OnCommandEnded;
// 如果插件卸载时未 -=,EventSource 持有 Handler 引用 → 内存泄漏

// ✅ 实现 IExtensionApplication 管理生命周期
public class MyPlugin : IExtensionApplication
{
    public void Initialize()
    {
        Application.DocumentManager.DocumentCreated += OnDocCreated;
        // ...
    }

    public void Terminate()
    {
        Application.DocumentManager.DocumentCreated -= OnDocCreated;
        // ...
    }
}

最佳实践清单

事务管理

  • 💡 使用 using 声明保证事务 Dispose
  • 💡 只读遍历用 StartOpenCloseTransaction 提升性能
  • 💡 包裹工具方法使 Commit() 自动化,防止遗忘
  • 💡 事务外只持有 ObjectId,不持有 DBObject

实体操作

  • 💡 添加新实体后立即调用 AddNewlyCreatedDBObject
  • 💡 as 类型转换优于强制转换,配合 null 检查
  • 💡 遍历 BTR 时,先收集 ID 列表,再操作

性能

  • 💡 批量操作尽量用一个事务,避免逐对象开新事务
  • 💡 遍历大量实体时用 ForRead 而非 ForWrite
  • 💡 用 OpenCloseTransaction 进行只读查询
  • 💡 ResultBuffer 构建大数据时使用 using

调试

  • 💡 命令入口处 try-catch 并 WriteMessage 输出异常信息
  • 💡 使用 ed.WriteMessage 而非 Console.WriteLine 做日志输出
  • 💡 CAD 崩溃时检查 Windows 事件查看器 → 应用程序日志 → .NET Runtime 错误

部署

  • 💡 Copy Local = False
  • 💡 使用 .bundle 包格式部署
  • 💡 针对特定 AutoCAD 版本编译(标定 net48 和引用版本)
  • 💡 发布前用 Release 模式编译

常见错误码速查

错误 可能原因
eNullObjectId 忘记 AddNewlyCreatedDBObject
eWasErased 对象已被删除,但还持有其 ObjectId
eInvalidOpenState 对已被 Dispose 的对象操作
eWrongDatabase 对象属于其他 Database(如侧载的 DWG)
eNotThatKindOfClass 类型转换错误(如把 Circle 当 Line)
AccessViolationException 事务外访问 DBObject 或空指针
InvalidOperationException 在遍历中修改集合