cad.net 事务栈

事务栈

设计概念

事务栈主要是作为一个代理类来保证流程的顺序正确,
不耦合官方的API,做一个代理类是很正常的,
例如中望API缺失,而你的下游代码又不想一个个预处理,就可以通过代理类提供虚假API先.
它才13个指针(9个符号表+4个本类)+1个表记录字典,不存在性能瓶颈,
而且是符号表是惰性求值的,优化也只是改为结构体放到非托管堆,而不是放弃事务栈结构.
它极大方便取值了呀.
函数多次跳转这种开销是可以通过内联标记进行优化的.
目的是多种场景的异常提示,避免陷入不知情的分析.

无论如何你都需要看一次事务栈内部做了什么,
因为它处理的问题非常多,它的复杂性是根据业务场景而来,
你裸写这些业务场景很大程度上面能够避免问题,
但是一旦遇到问题,你可能需要重构整个工程的全部分支,
例如DBTrans.Task它究竟在规避什么问题.

和原生事务一样,事务栈也保证能够嵌套事务.
只是不推荐,为什么呢?见调用例子就知道了.

我们没有从静态字段直接取出时候创建,为什么不做呢?
因为new就是创建,Top就是栈顶取出,
免得Top出现new来,语义层面要合理.
使用的时候,命令上面new一次,其他子函数全部是Top.

public class TestCommands {
    [CommandMethod(nameof(TestDBTrans))]
    public void TestDBTrans() {
        // 创建事务栈,using在函数最后默认提交和释放.
        // 默认构造函数获取当前文档,因为执行命令必然有当前文档.
        using DBTrans tr = new();
        // 不需要传事务给子函数.
        SubFunc();
    }

    private void SubFunc() {
        // 从事务栈顶获取当前事务
        var tr = DBTrans.Top;

        // 验证ForEach函数,它得到id.IsOk的
        tr.BlockTable.ForEach(record => {
            tr.Editor?.WriteMessage(record.Id.ToString());
        });

        // 验证迭代器接口,它没有过滤任何id.
        foreach(var id in tr.BlockTable) {
            tr.Editor?.WriteMessage(id.ToString());
        }

        // 获取ids略...
        foreach(var id in ids) {
            // 因为Acad的图元读取出来是放在非托管堆和CPP交互的,
            // 需要using约束图元生命周期,通过作用域释放对象,避免遗忘释放,例如面域就会打印信息.
            // 通过强转避免可空类型空值传导,也就是明确不为空.
            using var ent = (Entity)tr.GetObject(id);

            // 如果需要判断类型用is,而不是as,is还是指令来着.
            if (ent is BlockReference brf) {

            }
        }

    }
}

任何静态字段本质上是跨文档,但是文档不一定存在(后台开图),
所以我们是用字典进行跨数据库,通过_dBTrans[db]来获取缓存的信息,
这样前台或者后台数据库都能索引了.

扩展功能

后台常开多个数据库,用于提取数据,怎么找到后台已经打开的数据库呢?
靠遍历文档集合是不行的,因为后台打开压根没有加入文档呀.
遇到此场景,调用者自行创建一个fdbMap<file, db>,
再通过DBTrans.GetTop(db)获取事务,两次O(1)寻址就能得到事务.

各种寻址:
文件找文档 fdocMap<file, doc>
文件找数据库 fdbMap<file, db> 后台db没有doc需要独立建立映射表
文件找事务
var db = fdbMap[file];
var tr = DBTrans.GetTop(db);
DBTrans.TryGetTop(db, out var tr);

文档找数据库 doc.Database
文档找文件 doc.Name

数据库找文档 Acaop.DocumentManager.GetDocument(db);
数据库找文件 db.Filename
数据库找事务
var tr = DBTrans.GetTop(db);
DBTrans.TryGetTop(db, out var tr);

工作数据库和当前文档数据库是不一样的,
场景: 后台开图或者创建数据库,后台文件切换布局.
要设置工作数据库才能访问布局管理器,它是CAD提供的唯一通道.
否则无法获取和设置布局数据.
当切换布局之后,保存事务前需要恢复为旧工作数据库,否则会致命错误.
原因是每次提交都表示你要关闭,
而关闭后工作数据库仍然显示了释放的数据库,这肯定不对了.
我加在了提交事务上面的报错.

原生事务

常规事务:
从db寻址,我记得doc也有,它还是父类.

var tr = db.TransactionManager.StartTransaction();

无撤事务: StartOpenCloseTransaction
它是常规事务派生,重写了很多,
官方资料说它是无撤回视图的事务.

var doc = Acaop.DocumentManager.MdiActiveDocument;
using var tr = doc.TransactionManager.StartOpenCloseTransaction();

不过也有人说它报错位置很多,
布局管理器不能用....没有测试过,怀疑
而且无论前台还得后台取值和修改必须要经过工作数据库
https://www.cnblogs.com/JJBox/p/18697838
网上一些资料不对,它没有Session相关的函数.

替换句柄需要用 StartOpenCloseTransaction()
https://www.cnblogs.com/JJBox/p/12489648.html

兜底策略

1,原生事务原理
对象开启是会放在非托管堆(和Arx交互),再通过事务容器进行释放对象.
这个容器在tr.GetAllObjects()中可以看见,
少量对象,例如"面域"是绕过此机制的.

2,提权和降权
Acad偷懒,任何提权过的对象都不会降权,即使你已经使用了降权语句.
所以先读模式打开,再提权写入,避免事务崩溃.
https://www.cnblogs.com/JJBox/p/10798940.html
但是基于对称设计来说,我认为还是要保留using(obj.ForWrite())
只是事务栈和符号表模板内部采用最轻方式实现.

3,事务崩溃
事务容器存放超过一百万条多段线数据量会事务崩溃,
所以尽可能不要依赖兜底策略释放,
因此才有tr.GetObject()使用using原则.
如果你要添加超过一百万根多段线,要分多个事务进行.

4,若我们的事务栈的tr.GetObject()方法中
也加入一个HashSet<DBObject>进行兜底,岂不是连同"面域"也能释放?
不过良好编程习惯还是建议我们不要这样做,虽然可以但不做.
这就是为什么有代理类,
我既可以加兜底释放,也可以不加,还可以有限度的加,下游代码修改极少.
你可以说是前期设计缺失,但是后期遇到BUG,
内部更改不至于推翻重建,正所谓人无完人,先加中间层,再考虑未来.

5,由于存在事务崩溃,那么如何避免呢?
方案a:控制对象数量: 1w个以内就提交多次事务.
方案b:自己实现事务

自己实现事务(不完美)

    // 缓存弄成并发容器?
    // obj.IsNewlyObject 表示新创建图元
    // obj.IsWriteEnable 提权标记,
    // 一旦提权会自动克隆对象创建副本,
    // 然后修改的是副本,否则直接修改原生对象,
    // 然后就无法用Abort()撤回事务,
    // 提交事务就是通过替换句柄的方式进行,表示设置到数据库.

public class DBTrans {
    // 构造函数要加编组开始标记
    DBTrans() {
        CadSystem.Undo(true);
    }

    // 1,读取数据库的对象,兜底释放容器<id,旧对象>
    Dictionary<ObjectId, DBObject> _readObjectMap = new();
    // 要不要区分呢?例如刷新肯定是图元
    // Dictionary<ObjectId, Entity> _readEntityMap = new();
    // 2,新建对象
    HashSet<DBObject> _createSet = new();

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public DBObject GetObject(ObjectId id, OpenMode openMode = OpenMode.ForRead,
        bool openErased = false, bool openLockedLayer = false) {
        // 此方式打开自己持有对象
        if (!_readObjectMap.TryGetValue(id, out var obj)) {
            obj = id.Open(openMode, openErased, openLockedLayer);
            _readObjectMap.Add(id, obj);
        }
        // 要求提权
        if (openMode == OpenMode.ForWrite && !obj.IsWriteEnable)
            obj.UpgradeOpen();
        return obj;
    }

    /// <summary>
    /// 将新创建对象加入或移出事务
    /// <para>不是新创建对象移出会异常:eNotNewlyCreated</para>
    /// </summary>
    /// <param name="obj">新对象(旧对象被加入是拒绝)</param>
    /// <param name="add">true是加入事务,false是移出事务</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void AddNewlyCreatedDBObject(DBObject obj, bool add = true) {
        if (!obj.IsNewlyObject)
           throw new("eNotNewlyCreated");
        if (add) _createSet.Add(obj);
        else _createSet.Remove(obj);
    }

    /// <summary>
    /// 提交事务
    /// </summary>
    public void Commit() {
        // 替换句柄,也就是id指向克隆对象,
        // 旧对象被替换了,是否应该释放呢?非托管内存不知道是不是缓冲区.
        // 缓冲区就释放,不是缓冲区就表示数据库会原地固定.
        // obj.Dispose()内部根据是否存在database来释放非托管资源.
        // 直接id.Open的是不是不需要StartOpenCloseTransaction打开就能替换呢?
        using var tr = _database.TransactionManager.StartOpenCloseTransaction();
        foreach(var obj in _readObjectMap.Values) {
            if (obj.IsWriteEnable) {
                var cloneObj = obj.Clone();
                obj.HandOverTo(cloneObj, true, true);
                tr.AddNewlyCreatedDBObject(cloneObj, true);
            }
            obj.Dispose();
        }
        foreach(var obj in _createSet) {
            tr.AddNewlyCreatedDBObject(obj, true);
        }
        tr.Commit();
        _readObjectMap.Clear();
        _createSet.Clear();
        CadSystem.Undo(false);
    }

    // ...撤回视图信息,系统变量,回滚标记undo等等.
    /// <summary>
    /// 撤回事务
    /// </summary>
    public void Abort() {
        foreach(var obj in _readObjectMap.Values)
            obj.Dispose();
        foreach(var obj in _createSet)
            obj.Dispose();
        _readObjectMap.Clear();
        _createSet.Clear();

        // 发送命令进行回滚,那么后台又如何回滚呢?
        CadSystem.Undo(false);
        Document?.SendStringToExecute("_u\n", false, false, false);
    }

    // 刷新是根据缓存进行的,可能是触动刷新消息循环之类的...
    public void QueueForGraphicsFlush() {
        var move0 = Matrix3d.Displacement(new Vector3d(0,0,0));

        var allEntities = _readObjectMap.Values
            .Concat(_createSet)
            .OfType<Entity>();

        foreach(var ent in allEntities) {
            // 图元重绘,删除的绘制就是清理界面
            ent.Draw();
            if (ent.IsDisposed || ent.IsErased) continue;
            // move0要提权才有效果,不然也不报错
            if (!ent.IsWriteEnable) 
                ent.UpgradeOpen();
            ent.TransformBy(move0);
            // 图块刷新
            ent.RecordGraphicsModified(true);
            if (ent is BlockReference brf) {
                // 更新动态块
                var btr = (BlockTableRecord)tr.GetObject(brf.BlockTableRecord, OpenMode.ForWrite);
                btr.UpdateAnonymousBlocks();
            }
            else if (ent is Dimension dim) dim.RecomputeDimensionBlock(true);
            else if (ent is Viewport vp) vp.UpdateDisplay();
        }

        // 底层差不多ed.UpdateScreen();
        Acap.UpdateScreen(); 
        // acad2014及以上要加,立即处理队列上面的消息
        System.Windows.Forms.Application.DoEvents();
    }
}

这样提交的时候就知道谁改过了,不过由于实在复杂了,
本次事务栈还是利用Acad自己的事务机制,避免出错.

未解决:后台又如何回滚呢?
https://www.cnblogs.com/JJBox/p/10214582.html

和现有IFox0.9和1.0区别:

1,文档和命令行字段改为属性,不缓存了,二次寻址方式获取.
2,_fileName字段没有存在必要了,释放后台打开的可以用doc is null判断.
3,将提交和释放标记合并.
4,事务栈改为map,一个数据库一个事务,避免同栈切换数据库弹出出错.
5,提供两个静态函数用于检索数据库是否已经加入事务栈.
6,拆开后台和前台两个打开方式,具体见代码内部注释.
7,Task函数的参数改为回声提示.
8,说明工作数据库和当前文档数据库的区别.
9,加入1.0的获取句柄
10,提交事务前检查工作数据库报错,避免直接致命错误后不知道意义.
11,超多的积极内联标记.
12,Top直接指向GetTop()函数.
13,修正了抛错误加ex和不加,两种模式是不一样的.
14,构造函数添加对OCT的支持,增加嵌套OTC事务函数,打开文件增加OCT的支持.
15,强迫症式函数名.
16,和第6点关联,发现引发致命错误的逻辑
a:文件打开错误. b:Session标记缺失设置.
那么把这些作为静态函数打开文件,而不是构造函数,
那么Dispose释放时候就不再需要判断db和tr为空了,
IDE也不会绿色提示了.
17,想了一下还是制造一个字典缓存事务栈自己打开表记录,
使得取值之后能够本类回收,起码出现释放出错的时候会提示自己,
而不是由原生事务弹出不知道意义的提示.

代码

namespace IFoxCAD.Cad;
using System.Diagnostics;
using System.IO;
using Exception = System.Exception;
using Acaop = Application;

#if NET45_OR_GREATER
using System.Runtime.CompilerServices;
#else
// 以下就做一个假的内联标记,避免每个特性都要预处理.
// 让我们偷点编译器代码.
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/MethodImplAttribute.cs

[Flags]
public enum MethodImplOptions {
    Unmanaged = 0x0004, // 指定方法使用非托管调用约定
    NoInlining = 0x0008, // 阻止方法内联
    ForwardRef = 0x0010, // 表示方法是一个正向引用
    Synchronized = 0x0020, // 指示方法是同步的
    NoOptimization = 0x0040, // 禁用方法的优化
    PreserveSig = 0x0080, // 指示方法签名在跨语言调用时应保持不变
    AggressiveInlining = 0x0100, // 强制方法尽可能内联
    AggressiveOptimization = 0x0200, // 绕过分层编译的动态PGO优化
    InternalCall = 0x1000 // 表示方法是一个内部调用
}

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor, Inherited = false)]
public sealed class MethodImplAttribute : System.Attribute {
    // public MethodCodeType MethodCodeType;
    public MethodImplOptions Value { get; }
    public MethodImplAttribute() { }
    public MethodImplAttribute(MethodImplOptions methodImplOptions) {
        Value = methodImplOptions;
    }
    public MethodImplAttribute(short value) {
        Value = (MethodImplOptions)value;
    }
}
#endif

/// <summary>
/// 事务栈
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerTypeProxy(typeof(DBTrans))]
public sealed class DBTrans : IDisposable {
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string DebuggerDisplay => ToString();

    #region 公共静态函数

    /// <summary>
    /// 获取原生事务栈顶
    /// </summary>
    /// <param name="database">数据库</param>
    /// <returns>原生事务</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Transaction? GetTopTransaction(Database db)
    {
        var tr = db?.TransactionManager.TopTransaction;
        if (tr is null) throw new ArgumentNullException(nameof(DBTrans), $"此数据库{db}没有原生事务");
        return tr;
    }

    /// <summary>
    /// 获取栈顶事务
    /// </summary>
    /// <param name="db">数据库,默认是工作数据库</param>
    /// <returns>事务对象</returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static DBTrans GetTop(Database? db = null)
    {
        // 工作数据库和当前文档数据库是不一样的.
        // 例如布局管理器是经过工作数据库取值的,我们事务栈也是如此.
        // 而默认构造函数则以当前文档创建.
        // 工作文档是空的场景: 全部文档关闭,后台打开图纸,
        // 要WorkingDatabase=后台db,并且提交事务前恢复原本.
        db ??= HostApplicationServices.WorkingDatabase;
        if (db is null)
            throw new ArgumentNullException(nameof(DBTrans), $"工作数据库为空,后台调用需先设置或调用Task函数");
        if (_dBTrans.Count == 0)
            throw new ArgumentNullException(nameof(DBTrans), $"调用前必须创建事务栈");
        if (!_dBTrans.TryGetValue(db, out var trStack))
            throw new ArgumentNullException(nameof(DBTrans), $"此数据库{db}没有加入事务栈");
        return trStack.Peek();
    }

    /// <summary>
    /// 获取栈顶事务
    /// </summary>
    public static DBTrans Top => GetTop();

    // 提供一个查询不报异常的,用于后台常开的dbMap检索用
    /// <summary>
    /// 尝试获取栈顶事务
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool TryGetTop(Database db, out DBTrans dBTrans)
    {
        if (_dBTrans.TryGetValue(db, out var trStack)) {
            dBTrans = trStack.Peek();
            return true;
        }
        dBTrans = null!;
        return false;
    }

    /// <summary>
    /// 事务栈是否已经包含此数据库
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Has(Database db)
    {
        return _dBTrans.ContainsKey(db);
    }

    /// <summary>
    /// 设置工作数据库
    /// </summary>
    public static DBTrans SetWorking(Database db)
    {
        if (db is null)
            throw new ArgumentNullException(nameof(db));
        HostApplicationServices.WorkingDatabase = db;
    }

    /// <summary>
    /// 隐式转换为原生事务
    /// </summary>
    /// <param name="tr">事务栈</param>
    /// <returns>原生事务</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static implicit operator Transaction(DBTrans tr) => tr._transaction;

    #endregion

    #region 静态资源

    // 静态资源是不算到类大小的.
    /// <summary>
    /// 事务栈
    /// </summary>
    private static readonly Dictionary<Database, Stack<DBTrans>> _dBTrans = new();

    #endregion

    #region 本类字段
   
    /// <summary>
    /// 文档锁
    /// </summary>
    private readonly DocumentLock? _documentLock;

    // 既然可以通过隐式转换,那么就私有它
    // 例如: Transaction tr = DBTrans.Top;
    /// <summary>
    /// 原生事务
    /// </summary>
    private readonly Transaction _transaction;

    /// <summary>
    /// 数据库
    /// </summary>
    private readonly Database _database;

    /// <summary>
    /// 提交事务和释放标记
    /// </summary>
    private TransStatus _transStatus = new();

    #endregion

    #region 公开属性

    /// <summary>
    /// 文档
    /// </summary>
    public Document? Document => Acaop.DocumentManager.GetDocument(_database);

    /// <summary>
    /// 命令行
    /// </summary>
    public Editor? Editor => Document?.Editor;

    /// <summary>
    /// 数据库
    /// </summary>
    public Database Database => _database;

    #endregion

    #region 构造函数

    /// <summary>
    /// 事务栈
    /// <para>默认构造函数,默认为打开当前文档,默认提交事务</para>
    /// </summary>
    /// <param name="doc">要打开的文档</param>
    /// <param name="commit">事务是否提交</param>
    /// <param name="docLock">是否锁文档</param>
    /// <param name="openCloseTrans">无撤事务</param>
    public DBTrans(Document? doc = null, bool commit = true,
     bool docLock = false, bool openCloseTrans = false)
    {
        doc ??= Acaop.DocumentManager.MdiActiveDocument;
        if (docLock && doc.LockMode(false) == DocumentLockMode.NotLocked)
            _documentLock = doc.LockDocument();
        _database = doc.Database;
        CheckDatabaseError();
        var tm = _database.TransactionManager;
        if (openCloseTrans) _transaction = tm.StartOpenCloseTransaction();
        else _transaction = tm.StartTransaction();
        if (commit) _transStatus.Commit();
        if (!_dBTrans.TryGetValue(_database, out var trStack)) {
            trStack = new();
            _dBTrans.Add(_database, trStack);
        }
        trStack.Push(this);
    }

    /// <summary>
    /// 事务栈
    /// <para>打开数据库,默认提交事务</para>
    /// </summary>
    /// <param name="db">要打开的数据库</param>
    /// <param name="commit">事务是否提交</param>
    /// <param name="openCloseTrans">无撤事务</param>
    public DBTrans(Database db, bool commit = true, bool openCloseTrans = false)
    {
        _database = db;
        CheckDatabaseError();
        var tm = _database.TransactionManager;
        if (openCloseTrans) _transaction = tm.StartOpenCloseTransaction();
        else _transaction = tm.StartTransaction();
        if (commit) _transStatus.Commit();
        if (!_dBTrans.TryGetValue(_database, out var trStack)) {
            trStack = new();
            _dBTrans.Add(_database, trStack);
        }
        trStack.Push(this);
    }

    // 嵌套此事务是可以替换句柄的
    // https://www.cnblogs.com/JJBox/p/12489648.html
    /// <summary>
    /// 无撤事务
    /// <para>此处提交事务,外层可以进行回滚</para>
    /// </summary>
    /// <param name="action">无撤事务</param>
    /// <param name="commit">是否提交</param>
    public void StartTransaction(Action<OpenCloseTransaction> action, bool commit = true) {
        if (action is null) throw new ArgumentNullException(nameof(action));
        using var tr = _database.TransactionManager.StartOpenCloseTransaction();
        action.Invoke(tr);
        if (commit) tr.Commit();
    }

/*
0x01,前台开图创建文档,记录doc的.
前台打开会被文档持有,是无法释放db的,并且需要发送命令保存和关闭.

后台开图或者后台创建数据库,不记录doc的,
所以doc is null也能保证是后台开的,用于释放后台打开的db,
所以我删掉file字段了,
并且移除了保存方式到外部,它就更加没有存在价值了.

0x02,原本参数有file的构造函数存在问题

21,有同名file已经加入事务栈,表示已经开过,
但是在此无论如何都会再次开图,因为构造函数必然是构造有效对象.
所以构造函数无法避免此类情况,
调用者想要避免此场景,要自己去构造 fdbMap<file, db>,

22,文件已经在前台打开(文档集合已经持有),但是通过file参数要求后台打开.
出现后台只读场景,功能上是可行的,但是从业务上来说是矛盾的,
你可以直接用前台文档进行呀,因此我删掉了此构造函数,
分解成 后台打开 和 前台打开 两个静态函数,
让调用者自行规避或明知而为.

0x03,当前进程前台已经打开,通过文档集合判断.

0x04,当前进程后台已经打开呢?
你无法通过遍历文档集合得到,它压根不加入文档集合,
因此和21规避同名file一样,自行构造后台 fdbMap<file, db>,
再通过DBTrans.GetTop(db)得到事务,两个O(1)检索就得到了.

41,通常后台处理完就关闭了,
所以改为静态读取文件后加入事务栈,完成就提交释放,
为了便利性,期间会一直持有事务直到提交后自动关闭数据库.

42,后台常开去拷贝数据呢?多次提交事务呢?是可能的.
所以提供静态创建数据库函数,不加入事务栈.
由调用者自行持有.

0x05,前台开图必须设置: CommandFlags.Session 标记
前台开图如果用命令,不设置标记的话就会卡死.

如何判断调用的函数位置是Session呢?
目前是让调用者自己肉眼保证,没有报错机制.
感觉不可行的方案:
拦截运行时的命令,获取输入中的命令,然后反射全部命令,
因为事件也是Session环境,也可以调用事务栈,但是此时没有命令.
并且还不一定用桌子的命令特性定义命令.
*/

    /// <summary>
    /// 后台打开文件并加入事务栈
    /// <para>默认提交事务</para>
    /// <para>后台打开后执行任务需要用Task函数保证工作数据库正确</para>
    /// </summary>
    /// <param name="file">要打开的文件</param>
    /// <param name="commit">事务是否提交</param>
    /// <param name="fileOpenMode">开图模式</param>
    /// <param name="password">密码</param>
    /// <param name="openCloseTrans">无撤事务</param>
    /// <exception cref="FileNotFoundException"></exception>
    public static DBTrans OpenPushToBackend(string file, bool commit = true,
        FileOpenMode fileOpenMode = FileOpenMode.OpenForReadAndWriteNoShare,
        string? password = null, bool openCloseTrans = false)
    {
        if (string.IsNullOrWhiteSpace(file))
            throw new FileNotFoundException(nameof(OpenPushToBackend), "文件后缀不是dxf/dwg");
        Database? db = null;

        // 排除只读,因为磁盘文件可能已经释放可以可写打开.
        // doc.Name: "D:\\JX.dwg" 比较时候需要一致
        file = file.Replace("/", "\\");
        var fdocMap = FileDocumentMap();
        if (fdocMap.TryGetValue(file, out var doc)) {
            db = doc.Database;
        }
        // 此处创建的数据库是肯定获取不到文档的.
        db ??= OpenFileForBackend(file, fileOpenMode, password);
        return new DBTrans(db, commit, openCloseTrans : openCloseTrans);
    }

    /// <summary>
    /// 前台打开文件并加入事务栈
    /// <para>默认提交事务,需要设置CommandFlags.Session</para>
    /// </summary>
    /// <param name="file">要打开的文件</param>
    /// <param name="commit">事务是否提交</param>
    /// <param name="fileOpenMode">开图模式</param>
    /// <param name="password">密码</param>
    /// <param name="openCloseTrans">无撤事务</param>
    /// <exception cref="FileNotFoundException"></exception>
    public static DBTrans OpenPushToFrontend(string file, bool commit = true,
        FileOpenMode fileOpenMode = FileOpenMode.OpenForReadAndWriteNoShare,
        string? password = null, bool openCloseTrans = false) {
        if (string.IsNullOrWhiteSpace(file))
            throw new FileNotFoundException(nameof(OpenPushToFrontend), "文件后缀不是dxf/dwg");
        if (!File.Exists(file)) 
            throw new FileNotFoundException(nameof(OpenPushToFrontend), "文件必须存在");

        // 排除只读,因为磁盘文件可能已经释放可以可写打开.
        // doc.Name: "D:\\JX.dwg" 比较时候需要一致
        file = file.Replace("/", "\\");
        var fdocMap = FileDocumentMap();
        if (!fdocMap.TryGetValue(file, out var doc)) {
            doc ??= OpenFileForFrontend(file, fileOpenMode, password);
        }

        // 前台进行同步激活文档和工作数据库
        // 后台为什么不设置呢?因为后台需要还原,
        // 因此不在打开时候修改工作数据库,而是制作Task函数进行.
        // 而前台由文档持有,并通过它自己的机制保证关闭时候切换.
        var dm = Acaop.DocumentManager;
        if (!doc.IsActive) {
            dm.MdiActiveDocument = doc;
            HostApplicationServices.WorkingDatabase = doc.Database;
        }

        // file是当前文档就不用加锁,但是命令有Session就要加文档锁.
        // 非当前文档要激活切换,此时必然是跨文档,也要加文档锁.
        // 因此需要统一命令加上Session并且加文档锁.
        // 否则 Editor?.Redraw() tm.QueueForGraphicsFlush() 将报错提示文档锁
        return new DBTrans(doc, commit, true);
    }


    // 若命令没有设置 CommandFlags.Session
    // Open会卡死进入中断状态不会执行打开,
    // 直到切换文档ctrl+tab或者关闭文档,
    // 并且doc.IsActive会异常
    // 若是构造函数出错,导致Dispose函数出现需要判断本类资源为空场景.
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Document OpenFileForFrontend(string file, FileOpenMode fileOpenMode, string password) {
        try {
            var dm = Acaop.DocumentManager;
            return dm.Open(file,
                fileOpenMode == FileOpenMode.OpenForReadAndReadShare,
                password);
        } catch(Exception ex)
             throw;
        } 
    }

    // 文件不存在,创建数据库,允许之后保存.
    // 文件存在,前台文档集合没找到,后台开图.
    // 文件存在,前台文档集合找到,后台只读打开,诡异的业务场景,会致命错误吗?
    // Acad08测试: new Database()第2个参数使用false时,
    // 将导致关闭cad的时候出现致命错误:
    // Unhandled Access Violation Reading Ox113697a0 Exception at 4b4154h
    // 报错: ePermanentlyErased 就是new Database()参数写错了.
    // 报错: eFileSharingViolation 他人在打开此图.
    // 报错: eWrongObjectType 表示你没有用Task函数包裹.
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Database OpenFileForBackend(string file, FileOpenMode fileOpenMode, string password)
    {
        if (!File.Exists(file))
            return new Database(true, true);

        var db = new Database(false, true);
        var ext = Path.GetExtension(file);
        if (string.Equals(ext, ".dwg", StringComparison.OrdinalIgnoreCase))
            db.ReadDwgFile(file, fileOpenMode, true, password);
        else if (string.Equals(ext, ".dxf", StringComparison.OrdinalIgnoreCase)) 
            db.DxfIn(file, null); 
        else throw new FileNotFoundException(nameof(OpenFileForBackend), $"文件后缀不是dxf/dwg: {ext}");
        // 读取文件就断开连接,释放磁盘占用,避免其他不能读取和删除.
        db.CloseInput(true);
        return db;
    }

    // 为了方便内联,这个函数还是写轻松点,大不了其他人重写一份
    /// <summary>
    /// 前台文件路径和文档映射表
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Dictionary<string, Document> FileDocumentMap() {
        var dm = Acaop.DocumentManager;
        var map = new Dictionary<string, Document>();
        foreach (Document doc in dm) {
            if (doc.IsDisposed || doc.IsReadOnly) continue;
            if (!map.ContainsKey(doc.Name))
                map.Add(doc.Name, doc);
        }
        return map;
    }

    // 后台可以之后加入
    /// <summary>
    /// 前台文件路径和数据库映射表
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Dictionary<string, Database> FileDatabaseMap() {
        var dm = Acaop.DocumentManager;
        var map = new Dictionary<string, Database>();
        foreach (Document doc in dm) {
            if (doc.IsDisposed || doc.IsReadOnly) continue;
            if (!map.ContainsKey(doc.Name))
                map.Add(doc.Name, doc.Database);
        }
        return map;
    }

    #endregion

    #region 符号表
    /// <summary>
    /// 块表
    /// </summary>
    public SymbolTable<BlockTable, BlockTableRecord> BlockTable =>
        _blockTable ??= new(this, _database.BlockTableId);
    private SymbolTable<BlockTable, BlockTableRecord>? _blockTable;

    /// <summary>
    /// 层表
    /// </summary>
    public SymbolTable<LayerTable, LayerTableRecord> LayerTable =>
        _layerTable ??= new(this, _database.LayerTableId);
    private SymbolTable<LayerTable, LayerTableRecord>? _layerTable;

    /// <summary>
    /// 文字样式表
    /// </summary>
    public SymbolTable<TextStyleTable, TextStyleTableRecord> TextStyleTable =>
        _textStyleTable ??= new(this, _database.TextStyleTableId);
    private SymbolTable<TextStyleTable, TextStyleTableRecord>? _textStyleTable;

    /// <summary>
    /// 注册应用程序表
    /// </summary>
    public SymbolTable<RegAppTable, RegAppTableRecord> RegAppTable =>
        _regAppTable ??= new(this, _database.RegAppTableId);
    private SymbolTable<RegAppTable, RegAppTableRecord>? _regAppTable;

    /// <summary>
    /// 标注样式表
    /// </summary>
    public SymbolTable<DimStyleTable, DimStyleTableRecord> DimStyleTable =>
        _dimStyleTable ??= new(this, _database.DimStyleTableId);
    private SymbolTable<DimStyleTable, DimStyleTableRecord>? _dimStyleTable;

    /// <summary>
    /// 线型表
    /// </summary>
    public SymbolTable<LinetypeTable, LinetypeTableRecord> LinetypeTable =>
        _linetypeTable ??= new(this, _database.LinetypeTableId);
    private SymbolTable<LinetypeTable, LinetypeTableRecord>? _linetypeTable;

    /// <summary>
    /// 用户坐标系表
    /// </summary>
    public SymbolTable<UcsTable, UcsTableRecord> UcsTable =>
        _ucsTable ??= new(this, _database.UcsTableId);
    private SymbolTable<UcsTable, UcsTableRecord>? _ucsTable;

    /// <summary>
    /// 视图表
    /// </summary>
    public SymbolTable<ViewTable, ViewTableRecord> ViewTable =>
        _viewTable ??= new(this, _database.ViewTableId);
    private SymbolTable<ViewTable, ViewTableRecord>? _viewTable;

    /// <summary>
    /// 视口表
    /// </summary>
    public SymbolTable<ViewportTable, ViewportTableRecord> ViewportTable =>
        _viewportTable ??= new(this, _database.ViewportTableId);
    private SymbolTable<ViewportTable, ViewportTableRecord>? _viewportTable;

    #endregion

    #region 表记录

    private readonly Dictionary<ObjectId, DBObject> _objectCache = new();

    private T GetCache<T>(ObjectId objectId) where T : DBObject {
        if (_objectCache.TryGetValue(objectId, out var obj) 
            && obj is T result) {
            return result;
        }
        result = (T)GetObject(objectId);
        _objectCache.Add(objectId, result);
        return result;
    }

    /// <summary>
    /// 当前绘图空间(可能是不同布局的)
    /// </summary>
    public BlockTableRecord CurrentSpace =>
        GetCache<BlockTableRecord>(_database.CurrentSpaceId);

    /// <summary>
    /// 模型空间
    /// </summary>
    public BlockTableRecord ModelSpace =>
        GetCache<BlockTableRecord>(this.BlockTable[BlockTableRecord.ModelSpace]);

    /// <summary>
    /// 图纸空间
    /// </summary>
    public BlockTableRecord PaperSpace =>
        GetCache<BlockTableRecord>(this.BlockTable[BlockTableRecord.PaperSpace]);

    /// <summary>
    /// 命名对象字典
    /// </summary>
    public DBDictionary NamedObjectsDict =>
        GetCache<DBDictionary>(_database.NamedObjectsDictionaryId);

    /// <summary>
    /// 组字典
    /// </summary>
    public DBDictionary GroupDict =>
        GetCache<DBDictionary>(_database.GroupDictionaryId);

    /// <summary>
    /// 多重引线样式字典
    /// </summary>
    public DBDictionary MLeaderStyleDict =>
        GetCache<DBDictionary>(_database.MLeaderStyleDictionaryId);

    /// <summary>
    /// 多线样式字典
    /// </summary>
    // ReSharper disable once InconsistentNaming
    public DBDictionary MLStyleDict =>
        GetCache<DBDictionary>(_database.MLStyleDictionaryId);

    /// <summary>
    /// 材质字典
    /// </summary>
    public DBDictionary MaterialDict =>
        GetCache<DBDictionary>(_database.MaterialDictionaryId);

    /// <summary>
    /// 表格样式字典
    /// </summary>
    public DBDictionary TableStyleDict =>
        GetCache<DBDictionary>(_database.TableStyleDictionaryId);

    /// <summary>
    /// 视觉样式字典
    /// </summary>
    public DBDictionary VisualStyleDict =>
        GetCache<DBDictionary>(_database.VisualStyleDictionaryId);

    /// <summary>
    /// 颜色字典
    /// </summary>
    public DBDictionary ColorDict =>
        GetCache<DBDictionary>(_database.ColorDictionaryId);

    /// <summary>
    /// 打印设置字典
    /// </summary>
    public DBDictionary PlotSettingsDict =>
        GetCache<DBDictionary>(_database.PlotSettingsDictionaryId);

    /// <summary>
    /// 打印样式表名字典
    /// </summary>
    public DBDictionary PlotStyleNameDict =>
        GetCache<DBDictionary>(_database.PlotStyleNameDictionaryId);

    /// <summary>
    /// 布局字典
    /// </summary>
    public DBDictionary LayoutDict =>
        GetCache<DBDictionary>(_database.LayoutDictionaryId);

#if !zcad
    /// <summary>
    /// 数据链接字典
    /// </summary>
    public DBDictionary DataLinkDict =>
        GetCache<DBDictionary>(_database.DataLinkDictionaryId);

    /// <summary>
    /// 详细视图样式字典
    /// </summary>
    public DBDictionary DetailViewStyleDict =>
        GetCache<DBDictionary>(_database.DetailViewStyleDictionaryId);

    /// <summary>
    /// 剖面视图样式字典
    /// </summary>
    public DBDictionary SectionViewStyleDict =>
        GetCache<DBDictionary>(_database.SectionViewStyleDictionaryId);
#else
    public DBDictionary DataLinkDict => 
        throw new ArgumentNullException(nameof(DBTrans), "中望CAD遗忘设计");
    public DBDictionary DetailViewStyleDict =>
        throw new ArgumentNullException(nameof(DBTrans), "中望CAD遗忘设计");
    public DBDictionary SectionViewStyleDict =>
        throw new ArgumentNullException(nameof(DBTrans), "中望CAD遗忘设计");
#endif

    #endregion

    #region 通用方法

    /// <summary>
    /// 根据对象id获取对象
    /// </summary>
    /// <param name="id">对象id</param>
    /// <param name="openMode">打开模式,默认为只读</param>
    /// <param name="openErased">是否打开已删除对象,默认为不打开</param>
    /// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
    /// <returns>数据库DBObject对象</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public DBObject GetObject(ObjectId id, OpenMode openMode = OpenMode.ForRead,
        bool openErased = false, bool openLockedLayer = false)
    {
        return _transaction.GetObject(id, openMode, openErased, openLockedLayer);
    }

/*
返回值T还是T?
若返回值不可空,但是这函数又强转类,所以强转失败会为null.
若返回值可空,你就必须每次要调用之后判断空,
但是很多时候是明确类型的,会多了判断语句.
还记得那句话吗:可空类型标记不是让你写if,而是让你尽可能不写if.
是is还是as呢?这就是我们得放弃这个API.
*/
    /// <summary>
    /// 根据对象id获取图元对象
    /// </summary>
    /// <typeparam name="T">要获取的图元对象的类型</typeparam>
    /// <param name="id">对象id</param>
    /// <param name="openMode">打开模式,默认为只读</param>
    /// <param name="openErased">是否打开已删除对象,默认为不打开</param>
    /// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
    /// <returns>图元对象</returns>
    [Obsolete("可空类型标记出现后,建议使用非泛型标记的", false)]
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public T? GetObject<T>(ObjectId id, OpenMode openMode = OpenMode.ForRead,
        bool openErased = false, bool openLockedLayer = false) where T : DBObject
    {
        return _transaction.GetObject(id, openMode, openErased, openLockedLayer) as T;
    }

    /// <summary>
    /// id有效,未被删除
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsOk(ObjectId id)
        => !id.IsNull && id.IsValid && !id.IsErased && !id.IsEffectivelyErased && id.IsResident;


    /// <summary>
    /// 根据句柄获取对象Id
    /// </summary>
    /// <param name="handleString">句柄字符串</param>
    /// <returns>对象Id</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ObjectId GetObjectId(string handleString)
    {
        Handle handle;
        if (IntPtr.Size == 4)
            handle = new Handle(Convert.ToInt32(handleString, 16));
        else
            handle = new Handle(Convert.ToInt64(handleString, 16));
        return _database.TryGetObjectId(handle, out ObjectId id) ? id : ObjectId.Null;
    }

    /// <summary>
    /// 根据句柄获取对象Id
    /// </summary>
    /// <param name="handle">句柄</param>
    /// <returns>对象Id</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ObjectId GetObjectId(Handle handle)
    {
        return _database.TryGetObjectId(handle, out ObjectId id) ? id : ObjectId.Null;
    }

    /// <summary>
    /// 将新创建对象加入或移出事务
    /// <para>不是新创建对象移出会异常:eNotNewlyCreated</para>
    /// </summary>
    /// <param name="obj">对象</param>
    /// <param name="add">true是加入事务,false是移出事务</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void AddNewlyCreatedDBObject(DBObject obj, bool add = true) {
        _transaction.AddNewlyCreatedDBObject(obj, add);
    }

    [Flags]
    public enum Echo {
        None = 0, // 不应用任何回声
        ActiveDocument = 1 << 0, // 应用前台文档的回声
        Errors = 1 << 1, // 应用报错的回声
        FatalErrors = 1 << 2, // 应用致命错误的回声
        All = ActiveDocument | Errors | FatalErrors // 启用所有回声功能
    }

    /// <summary>
    /// 事务栈任务自动处理前台后台
    /// </summary>
    /// <param name="action">委托</param>
    /// <param name="echo">回声,可以选择就是可以排除</param>
    public DBTrans Task(Action action, Echo echo = Echo.All)
    {
        if (action is null) throw new ArgumentNullException(nameof(action));
        if (CheckDatabaseError(echo.HasFlag(Echo.FatalErrors))) {
            return this;
        }

        Database? dbBak = HostApplicationServices.WorkingDatabase;
        Document? docBak = Acaop.DocumentManager.MdiActiveDocument;
        var doc = Document;

        // 直接执行: 当前文档前台开图 或 当前就是工作数据库 
        if ((docBak is not null && docBak == doc && docBak.Database == dbBak)
           || (dbBak is not null && dbBak == _database)) {
            try {
                action.Invoke(); 
                return this;
            }
            catch (Exception ex) {
                if (!echo.HasFlag(Echo.Errors))
                    throw; // 不,加ex表示由我的栈帧抛出.
                System.Diagnostics.Trace.WriteLine(
                    $"捕获到异常:\n" +
                    $"错误时间: {DateTime.Now}\n" +
                    $"错误信息: {ex.Message}\n" +
                    $"堆栈跟踪: {ex.StackTrace}");
                return this;
            }
        }

        // GetTop()允许
        // 1,当前文档就是工作数据库.
        // 2,工作数据库不是当前文档,例如克隆后台数据到前台.
        // GetTop()阻止
        // 3,工作数据库为空表示:关闭全部文档并且后台开图.
        // 设置 工作数据库=后台db,之后进入任务,完成任务后还原.

        // 设置工作数据库还顺便处理了深度克隆导致单行文字偏移.
        // 布局管理器取值和设置是经过工作数据库.

        // 前台绑定参照用此方法会抛出异常:eWasErased
        // 是不是之前测试有问题呢?
        // 可能是原本只有一个栈的错误弹栈导致的.
        // 可能是绑定要调用其他引擎,此时需要Session呢?
        // 可能是用完工作数据库之后,提交事务前忘记切换.(现在才知道有这回事)
        // 可能是已经位于前台文档集合中,但是没有激活为当前文档,
        // 激活岂不是要设置Session?用事件处理?发送异步命令?
        // 发送异步命令激活文档,但是委托怎么传入进去呢?
        // 又或许这种切换是可以的,只是要文档锁?
        // 切换会不会刷新工作数据库呢?影响了这两行位置
        // 让用户自己去保证吧.
        if (doc is not null && !doc.IsDisposed && docBak != doc) {
            if (echo.HasFlag(Echo.ActiveDocument)) {
                System.Diagnostics.Trace.WriteLine(
                     $"前台文档,非激活状态执行任务\n" +
                     $"规避错误需要设置CommandFlags.Session+切换文档+文档锁");
            }
            // 调用者需要外部设置和恢复
            // Acaop.DocumentManager.MdiActiveDocument = doc;
        }

        HostApplicationServices.WorkingDatabase = _database;
        try {
            action.Invoke(); 
            return this;
        }
        catch (Exception ex) {
            if (!echo.HasFlag(Echo.Errors))
                throw; // 不,加ex表示由我的栈帧抛出.
            System.Diagnostics.Trace.WriteLine(
                $"捕获到异常:\n" +
                $"错误时间: {DateTime.Now}\n" +
                $"错误信息: {ex.Message}\n" +
                $"堆栈跟踪: {ex.StackTrace}");
            return this;
        }
        finally {
             // 工作数据库=后台db,使用完后还原应该允许设置null的.
             HostApplicationServices.WorkingDatabase = dbBak;

             // if (docBak is not null && !docBak.IsDisposed)
             //    Acaop.DocumentManager.MdiActiveDocument = docBak;
        }
    }
    #endregion

    #region IDisposable接口相关函数

    /// <summary>
    /// 取消事务
    /// </summary>
    public void Abort()
    {
        _transStatus.Abort();
        Dispose();
    }

    /// <summary>
    /// 提交事务
    /// </summary>
    public void Commit()
    {
        _transStatus.Commit();
        Dispose();
    }

    /// <summary>
    /// 释放标记
    /// </summary>
    public bool IsDisposed => _transStatus.IsDisposed;

    /// <summary>
    /// 手动调用释放
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// 析构函数调用释放
    /// </summary>
    ~DBTrans() => Dispose(false);

/* 
事务释放流程:
1,根据传入的_transStatus参数确定是否提交,
2,不管是否提交,既然进入dispose,就将当前事务弹出.
3,将文档锁释放
4,静态字段也就是全局变量,生命周期和进程一样,由GC释放.
5,通过事务栈打开读取的文件,需要释放数据库,否则遗忘释放了,
并且在此之后需要保证 工作数据库 是其他数据库,
工作数据库如果存放 释放了的数据库 ,这会产生错误.
为了简化这个流程,本类写了一个Task函数,使得后台用完工作数据库就切换.
*/
    /// <summary>
    /// 释放函数
    /// </summary>
    /// <param name="disposing"></param>
    private void Dispose(bool disposing)
    {
        if (_transStatus.IsDisposed) return;
        _transStatus.Dispose();

        // 释放本类资源
        if (disposing)
        {
            // 构造函数引发致命错误时,
            // 本类资源会被清空,此处会连锁报错,
            // 因此去掉打开文件的构造函数,避免构造出错,
            // 构造都是有效对象,此处也就不需要判断为空了
            if (!_transaction.IsDisposed) // 防止隐式转换提交绕过
            {
                if (_transStatus.IsCommit)
                {
                    CheckWorkingDatabaseSync();
                    _transaction.Commit();
                }
                else
                {
                    // 如果是前台文档集合的,但是不是当前文档呢?
                    // 和Task一样,由用户自己保证.
                    // 防止事务回滚造成的视图回滚
                    using var vtr = Editor?.GetCurrentView();
                    _transaction.Abort();
                    if (vtr is not null) Editor?.SetCurrentView(vtr);
                }
                _transaction.Dispose();
            }
            _documentLock?.Dispose();

            // 表记录释放
            foreach(var pair in _objectCache) {
                pair.Value.Dispose();
            }
            _objectCache.Clear();

            // 符号表释放
            _blockTable?.Dispose();
            _layerTable?.Dispose();
            _textStyleTable?.Dispose();
            _regAppTable?.Dispose();
            _dimStyleTable?.Dispose();
            _linetypeTable?.Dispose();
            _ucsTable?.Dispose();
            _viewTable?.Dispose();
            _viewportTable?.Dispose();
        }

        // 释放全局资源,将当前事务栈弹栈
        if (_dBTrans.TryGetValue(_database, out var trStack)) {
            trStack.Pop();
            if (trStack.Count == 0) {
                _dBTrans.Remove(_database);
                // 释放读取文件创建的数据库
                if (Document is null)
                    _database.Dispose(); 
            }
        }

        _blockTable = null!;
        _layerTable = null!;
        _textStyleTable = null!;
        _regAppTable = null!;
        _dimStyleTable = null!;
        _linetypeTable = null!;
        _ucsTable = null!;
        _viewTable = null!;
        _viewportTable = null!;
    }

    // 提交事务前如果工作数据库=后台图纸(还没有释放)
    // 不报错也会直接致命错误,并且没有任何信息,不知其意义.
    [Conditional("DEBUG")]
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void CheckWorkingDatabaseSync() {
        Database? wdb = HostApplicationServices.WorkingDatabase;
        if (wdb is null) return;
        Document? doc = Acaop.DocumentManager.GetDocument(wdb);
        if (doc is not null) return;
        throw new Exception($"致命错误,工作数据库是后台数据库");
    }

    // _database没有可空标记,
    // 被用户意外释放.
    [MethodImpl(MethodImplOptions.NoInlining)]
    private bool CheckDatabaseError(bool echo = true) {
        if (_database.IsDisposed) {
            if (!echo) return true;
            throw new Exception("致命错误,数据库被错误释放");
        }
        return false;
    }

    #endregion

    #region ToString

    public override string ToString()
    {
        StringBuilder sb = new();
        sb.AppendLine("事务栈信息:");
        int i = 0;
        foreach (var pair in _dBTrans) {
            sb.AppendLine($"序号{++i}: 数据库路径: \"{pair.Key.Filename}\" - 事务栈数: {pair.Value.Count}");
        }
        sb.AppendLine("当前事务信息:");
        sb.AppendLine($"_database = \"{_database.Filename}\"");
        sb.AppendLine($"Document = {Document != null}");
        sb.AppendLine($"Editor = {Editor != null}");
        sb.AppendLine($"_transStatus = {_transStatus.ToString()}");
        sb.AppendLine($"_documentLock = {_documentLock != null}");
        sb.AppendLine($"_transaction = {_transaction.UnmanagedObject}");
        return sb.ToString();
    }

    #endregion
}

保存文件

1,定点数判断dwg版本.
https://www.cnblogs.com/JJBox/p/18511807
2,判断是否已经被其他进程占用,并提示.
https://www.cnblogs.com/JJBox/p/18697838

namespace IFoxCAD.Cad;

public static class DatabaseHelper {

/// <summary>
/// 保存文件
/// </summary>
/// <param name="db">数据库</param>
/// <param name="saveAsFile">另存为文件,原位保存要填db.Filename</param>
/// <param name="version">取反:先提取原本DWG版本,失败时用你要求的</param>
public static void SaveFile(this Database db, string saveAsFile, int version = ~(int)DwgVersion.Current)
{
    if (db is null) throw new ArgumentNullException(nameof(db));
    if (saveAsFile is null) throw new ArgumentNullException(nameof(saveAsFile));

    // 当前数据库的文件路径,新创建的数据库为"".
    var file = db.Filename;

    // 检查前台文档是否打开
    var dm = Acaop.DocumentManager;
    // var doc = dm.Cast<Document>()
    //     .FirstOrDefault(doc => !doc.IsDisposed && doc.Database == db);
    Document? doc = dm.GetDocument(db);

    if (doc is not null) {
        bool qs = !doc.IsReadOnly && string.Equals(file, saveAsFile, StringComparison.OrdinalIgnoreCase);
        doc.SendStringToExecute(qs ? "_qsave\n" : "_saveAs\n", false, true, true);
        return;
    }

    // 后台另存为目标.
    // 若是新创建数据库,则肯定于目标不同.
    // 1,被自己占用就直接保存咯,数据库可能是只读. 
    // 2,被其他进程占用目标文件,报错.
    if (!string.Equals(file, saveAsFile, StringComparison.OrdinalIgnoreCase)
        && IsFileOpen(saveAsFile))
        throw new FileNotFoundException(nameof(saveAsFile), "另存目标文件被其他进程占用");

    // DXF用任何版本号都会异常
    var ext = Path.GetExtension(saveAsFile);
    if (string.Equals(ext, ".dxf", StringComparison.OrdinalIgnoreCase)) {
        db.DxfOut(saveAsFile, 7, true);
        return;
    }

    // DWG保存需要版本号,
    // 取反:先提取原本DWG版本,失败时用你要求的.
    // 已经打开的数据库怎么会失败,无法识别参数呢?
    // 1,没有维护VersionTool
    // 2,兼容性加载,低加载高,
    // DwgVersion.Current被编译固定,此时判断就会高于当前. 
    if (File.Exists(file) && version < 0) {
        try {
            int acNum = -1;
            // 异常只会存在读取文件出错.
            ReadWriteDWG(file, (index, fileStream) => {
                acNum = VersionTool.DwgVers[index];
            });

            // 虽然编译时参数转为常量.
            // 但是运行时候遍历是引用的dll版本,是在exe旁边的.
            // 高于当前CAD是找不到的.
            Dictionary<string, int> verMap = new();
            foreach (DwgVersion dv in Enum.GetValues(typeof(DwgVersion)))
                verMap.Add(dv.ToString(), (int)dv);
            if (verMap.TryGetValue("AC" + acNum, out var v)) {
                version = v;
            }
        } catch {}
    }

    if (version < 0) version = ~version;

    // 此时是运行时
    if (version > (int)DwgVersion.Current) {
        version = (int)DwgVersion.Current;
    }
    db.SaveAs(saveAsFile, true, (DwgVersion)version, db.SecurityParameters);
}

}

状态结构体

只有1字节,嘿嘿,但是类和结构对齐就算一个指针长度了.

namespace IFoxCAD.Cad;

using System;
using System.Runtime.InteropServices;

public struct TransStatus
{
   [System.Flags]
    private enum TrStatus : byte {
        IsCommit = 1 << 0,    // 提交标记 (00000001)
        IsDisposed = 1 << 1   // 释放标记 (00000010)
    }

    // 私有状态字段
    private TrStatus _status;

    // 公有属性
    public bool IsCommit => _status.HasFlag(TrStatus.IsCommit);
    public bool IsAbort => !_status.HasFlag(TrStatus.IsCommit); // 未提交即为取消
    public bool IsDisposed => _status.HasFlag(TrStatus.IsDisposed);

    // 提交事务
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Commit() => _status |= TrStatus.IsCommit;

    // 取消事务
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Abort() => _status &= ~TrStatus.IsCommit;

    // 释放事务
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Dispose() => _status |= TrStatus.IsDisposed;

    // 隐式类型转换
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static implicit operator byte(TransStatus status)
        => (byte)status._status;

    public override string ToString()
        => $"提交标记: {IsCommit}, 释放标记: {IsDisposed}";
}

测试

public class Program
{
    public static void Main()
    {
        // 测试结构体大小
        int size = Marshal.SizeOf(typeof(TransStatus));
        Console.WriteLine($"TransStatus 结构体的大小为:{size} 字节");
        
        // 测试事务操作
        TransStatus status = new TransStatus();
        byte statusAsByte = status;
        Console.WriteLine($"状态值(byte): {statusAsByte}");
        Console.WriteLine(status.ToString());

        // 测试提交
        status.Commit();
        statusAsByte = status;
        Console.WriteLine($"状态值(byte): {statusAsByte}");
        Console.WriteLine(status.ToString());

        // 测试取消
        status.Abort();
        statusAsByte = status;
        Console.WriteLine($"状态值(byte): {statusAsByte}");
        Console.WriteLine(status.ToString());

        // 测试释放
        status.Dispose();
        statusAsByte = status;
        Console.WriteLine($"状态值(byte): {statusAsByte}");
        Console.WriteLine(status.ToString());

        // 再次提交
        status.Commit();
        statusAsByte = status;
        Console.WriteLine($"状态值(byte): {statusAsByte}");
        Console.WriteLine(status.ToString());
    }
}

符号表

// ReSharper disable RedundantNameQualifier
namespace IFoxCAD.Cad;

/// <summary>
/// 符号表模板类
/// </summary>
/// <typeparam name="TTable">符号表</typeparam>
/// <typeparam name="TRecord">表记录</typeparam>
public class SymbolTable<TTable, TRecord> : IEnumerable<ObjectId>, IDisposable
    where TTable : SymbolTable
    where TRecord : SymbolTableRecord, new() 
{
    #region 字段和属性

    /// <summary>
    /// 事务栈
    /// </summary>
    public DBTrans DBTrans => _dBTrans;
    private DBTrans _dBTrans;

    /// <summary>
    /// 当前符号表,是九个符号表之一
    /// </summary>
    public TTable CurrentSymbolTable => _curSym;
    private TTable _curSym;

    #endregion

    #region 构造函数

    /// <summary>
    /// 符号表模板类
    /// </summary>
    /// <param name="tr">事务栈</param>
    /// <param name="tableId">符号表id</param>
    /// <param name="defaultBehavior">默认行为:例如打开隐藏图层</param>
    internal SymbolTable(DBTrans tr, ObjectId tableId, bool defaultBehavior = true)
    {
        _dBTrans = tr;
        _curSym = (TTable)tr.GetObject(tableId);
        if (!defaultBehavior) return;
        if (_curSym is LayerTable layerTable) {
            // 打开层表隐藏
            layerTable = layerTable.IncludingHidden;
            if (layerTable is TTable table)
                _curSym = table;
        }
    }

    #endregion

    #region

    /// <summary>
    /// 添加表记录
    /// </summary>
    /// <param name="record">表记录</param>
    /// <returns>对象id</returns>
    public ObjectId Add(TRecord record)
    {
        ObjectId id;
        if (!_curSym.IsWriteEnable)
            _curSym.UpgradeOpen();
        id = _curSym.Add(record);
        _dBTrans.AddNewlyCreatedDBObject(record, true);
        return id;
    }

    /// <summary>
    /// 添加表记录
    /// </summary>
    /// <param name="recordName">表记录名</param>
    /// <param name="action">表记录处理函数</param>
    /// <returns>对象id</returns>
    public ObjectId Add(string recordName, Action<TRecord>? action = null)
    {
        var id = this[recordName];
        if (!id.IsNull) return id;
        var record = new TRecord() { Name = recordName };
        id = _curSym.Add(record);
        _dBTrans.AddNewlyCreatedDBObject(record, true);
        action?.Invoke(record);
        return id;
    }

    /// <summary>
    /// 有则修改无则添加表记录
    /// </summary>
    /// <param name="recordName">表记录名</param>
    /// <param name="action">表记录处理函数</param>
    /// <returns>对象id</returns>
    public ObjectId AddOrUpdate(string recordName, Action<TRecord> action)
    {
        var id = this[recordName];
        if (id.IsNull) id = Add(recordName, action);
        else Change(recordName, action);
        return id;
    }

    #endregion

    #region

    /// <summary>
    /// 删除表记录
    /// </summary>
    /// <param name="record">表记录</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void Remove(TRecord record)
    {
        if (!_curSym.IsWriteEnable)
            record.UpgradeOpen();
        record.Erase();
    }

    /// <summary>
    /// 删除表记录
    /// </summary>
    /// <param name="recordName">表记录名</param>
    public void Remove(string recordName)
    {
        // 热路径是非图层,在前面
        if (_curSym is not LayerTable lt) {
            var record = GetRecord(recordName);
            if (record is not null)
                Remove(record);
            return;
        }

        if (SymbolUtilityServices.IsLayerZeroName(recordName) ||
            SymbolUtilityServices.IsLayerDefpointsName(recordName))
            return;
        lt.GenerateUsageData();
        var recordLayer = GetRecord(recordName);
        if (recordLayer is not LayerTableRecord { IsUsed: false } ltr)
            return;
        if (!_curSym.IsWriteEnable)
            ltr.UpgradeOpen();
        ltr.Erase();
    }

    /// <summary>
    /// 删除表记录
    /// </summary>
    /// <param name="recordId">表记录Id</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Remove(ObjectId recordId)
    {
        var record = GetRecord(recordId);
        if (record is not null)
            Remove(record);
    }

    #endregion

    #region

    /// <summary>
    /// 修改表记录
    /// </summary>
    /// <param name="recordName">表记录名</param>
    /// <param name="action">修改委托</param>
    [DebuggerNonUserCode]
    public void Change(string recordName, Action<TRecord> action)
    {
        var record = GetRecord(recordName);
        if (record is null) return;
        if (!_curSym.IsWriteEnable)
            record.UpgradeOpen();
        action(record);
    }

    /// <summary>
    /// 修改表记录
    /// </summary>
    /// <param name="recordId">表记录Id</param>
    /// <param name="action">修改的函数</param>
    [DebuggerNonUserCode]
    public void Change(ObjectId recordId, Action<TRecord> action)
    {
        var record = GetRecord(recordId);
        if (record is null) return;
        if (!_curSym.IsWriteEnable)
            record.UpgradeOpen();
        action(record);
    }

    #endregion

    #region

    /// <summary>
    /// 获取表记录
    /// </summary>
    /// <param name="recordId">表记录Id</param>
    /// <param name="openMode">打开模式</param>
    /// <param name="openErased">是否打开已删除对象,默认为不打开</param>
    /// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
    /// <returns>表记录</returns>
    [DebuggerNonUserCode]
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public TRecord? GetRecord(ObjectId recordId, 
        OpenMode openMode = OpenMode.ForRead, 
        bool openErased = false,
        bool openLockedLayer = false)
    {
        if (!_map.TryGetValue(recordId, out var obj)) {
            var tr = _dBTrans;
            obj = (TRecord)tr.GetObject(recordId, openMode, openErased, openLockedLayer);
            if (obj is not null)
                _map.Add(recordId, obj);
        }
        return obj;
    }

    /// <summary>
    /// 获取表记录
    /// </summary>
    /// <param name="recordName">表记录名</param>
    /// <param name="openMode">打开模式</param>
    /// <param name="openErased">是否打开已删除对象,默认为不打开</param>
    /// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
    /// <returns>表记录</returns>
    [DebuggerNonUserCode]
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public TRecord? GetRecord(string recordName,
        OpenMode openMode = OpenMode.ForRead,
        bool openErased = false,
        bool openLockedLayer = false)
    {
        var recordId = this[recordName];
        if (!_map.TryGetValue(recordId, out var obj)) {
            var tr = _dBTrans;
            obj = (TRecord)tr.GetObject(recordId, openMode, openErased, openLockedLayer);
            if (obj is not null)
                _map.Add(recordId, obj);
        }
        return obj;
    }

#region
    // 这些函数挑战了using释放原则,要记录并释放.
    Dictionary<ObjectId, TRecord> _map = new();

    /// <summary>
    /// 获取表记录
    /// </summary>
    /// <returns>表记录集合</returns>
    public IEnumerable<TRecord> GetRecords()
    {
        foreach (var item in _curSym) {
            var record = GetRecord(item);
            if (record is null) continue;
            yield return record;
        }
    }

    /// <summary>
    /// 获取表记录名字
    /// </summary>
    /// <returns>惰性返回名字集合</returns>
    public IEnumerable<string> GetRecordNames() {
        GetRecords().Select(record => record.Name);
    }

    /// <summary>
    /// 获取表记录名字
    /// </summary>
    /// <param name="filter">过滤器</param>
    /// <returns>惰性返回名字集合</returns>
    public IEnumerable<string> GetRecordNames(Func<TRecord, bool> filter)
    {
        foreach (var item in _curSym) {
            var record = GetRecord(item);
            if (record is not null && filter.Invoke(record))
                yield return record.Name;
        }
    }
#endregion

    /// <summary>
    /// 从源数据库拷贝符号表记录
    /// </summary>
    /// <param name="table">符号表</param>
    /// <param name="recordName">表记录名</param>
    /// <param name="over">是否覆盖,<see langword="true"/>为覆盖,<see langword="false"/>为不覆盖</param>
    /// <returns>对象id</returns>
    private ObjectId GetRecordFrom(SymbolTable<TTable, TRecord> table,
        string recordName, bool over)
    {
        if (table is null)
            throw new ArgumentNullException(nameof(table));

        var rid = this[recordName];
        var has = rid != ObjectId.Null;
        if (has && (!has || !over)) return rid;

        using IdMapping map = new();
        using ObjectIdCollection ids = new();
        var id = table[recordName];
        ids.Add(id);
        table._dBTrans.Database.WblockCloneObjects(ids,
            _curSym.Id,
            map,
            DuplicateRecordCloning.Replace,
            false);
        return map[id].Value;
    }

    /// <summary>
    /// 从文件拷贝符号表记录
    /// </summary>
    /// <param name="tableSelector">符号表过滤器</param>
    /// <param name="file">文件</param>
    /// <param name="recordName">表记录名</param>
    /// <param name="over">是否覆盖,<see langword="true"/>为覆盖,<see langword="false"/>为不覆盖</param>
    /// <returns>对象id</returns>
    internal ObjectId GetRecordFrom(Func<DBTrans, SymbolTable<TTable, TRecord>> tableSelector,
        string file, string recordName, bool over)
    {
        using var tr = DBTrans.OpenPushToBackend(file);
        return GetRecordFrom(tableSelector(tr), recordName, over);
    }

    /// <summary>
    /// 索引器
    /// </summary>
    /// <param name="key">对象名称</param>
    /// <returns>对象的id</returns>
    public ObjectId this[string key] => 
        _curSym.Has(key) ? _curSym[key] : ObjectId.Null;

    /// <summary>
    /// 判断是否含有表记录
    /// </summary>
    /// <param name="key">记录名</param>
    /// <returns>存在返回 <see langword="true"/>, 不存在返回 <see langword="false"/></returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool Has(string key) => _curSym.Has(key);

    /// <summary>
    /// 判断是否含有表记录
    /// </summary>
    /// <param name="objectId">记录id</param>
    /// <returns>存在返回 <see langword="true"/>, 不存在返回 <see langword="false"/></returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool Has(ObjectId objectId) => _curSym.Has(objectId);

    /// <summary>
    /// 遍历符号表,执行委托
    /// </summary>
    /// <param name="task">要运行的委托</param>
    /// <param name="openMode">打开模式,默认为只读</param>
    /// <param name="checkIdOk">检查id是否删除,默认true</param>
    /// <param name="openErased">是否打开已删除对象,默认为不打开</param>
    /// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
    [DebuggerNonUserCode]
    public void ForEach(Action<TRecord> task, 
        OpenMode openMode = OpenMode.ForRead,
        bool checkIdOk = true,
        bool openErased = false, 
        bool openLockedLayer = false) {
        // 此处有委托函数,用[DebuggerStepThrough]特性会进入,
        // 预处理 #line hidden 肯定可以避免
        // AI提供了一个 [DebuggerNonUserCode] 特性,或者不用Invoke方式.
        ForEach((a, _, _) => {
            task(a);
        }, openMode, checkIdOk, openErased, openLockedLayer);
    }

    /// <summary>
    /// 遍历符号表,执行委托(允许循环中断)
    /// </summary>
    /// <param name="task">要执行的委托</param>
    /// <param name="openMode">打开模式,默认为只读</param>
    /// <param name="checkIdOk">检查id是否删除,默认true</param>
    /// <param name="openErased">是否打开已删除对象,默认为不打开</param>
    /// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
    [DebuggerNonUserCode]
    public void ForEach(Action<TRecord, LoopState> task,
        OpenMode openMode = OpenMode.ForRead,
        bool checkIdOk = true,
        bool openErased = false,
        bool openLockedLayer = false)
    {
        ForEach((a, b, _) => { task(a, b); }, openMode, checkIdOk, openErased, openLockedLayer);
    }

    /// <summary>
    /// 遍历符号表,执行委托(允许循环中断,输出索引值)
    /// </summary>
    /// <param name="task">要执行的委托</param>
    /// <param name="openMode">打开模式,默认为只读</param>
    /// <param name="checkIdOk">检查id是否删除,默认true</param>
    /// <param name="openErased">是否打开已删除对象,默认为不打开</param>
    /// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
    [DebuggerNonUserCode]
    public void ForEach(Action<TRecord, LoopState, int> task,
        OpenMode openMode = OpenMode.ForRead,
        bool checkIdOk = true,
        bool openErased = false, 
        bool openLockedLayer = false)
    {
        if (task is null) throw new ArgumentNullException(nameof(task));
        LoopState state = new(); /*这种方式比Action改Func更友好*/
        var i = 0;
        foreach (var id in _curSym) {
            if (checkIdOk && !DBTrans.IsOk(id)) continue;
            var record = GetRecord(id, openMode, openErased, openLockedLayer);
            if (record is not null) task(record, state, i);
            if (!state.IsRun) break;
            i++;
        }
    }

/*
    // 直接强转少了一层yield,这个不行,嘻嘻
    [DebuggerNonUserCode]
    public IEnumerator<ObjectId> GetEnumerator()
        => (IEnumerator<ObjectId>)_curSym.GetEnumerator();
*/

    [DebuggerNonUserCode]
    public IEnumerator<ObjectId> GetEnumerator() {
        foreach(ObjectId id in _curSym)
            yield return id;
    }

    [DebuggerNonUserCode]
    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }

    #endregion

    #region 释放

    private bool _disposed = false;
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        _disposed = true;
        if (disposing) {
            _curSym.Dispose();
            foreach(var record in _map.Values) {
                record.Dispose();
            }
            _map.Clear();
        }
    }
    ~SymbolTable() => Dispose(false);

    #endregion

}

LoopState

namespace IFoxCAD.Basal;
#line hidden
/// <summary>
/// 控制程序流程状态
/// </summary>
public class LoopState {
    // 状态枚举
    [System.Flags]
    public enum StateFlags : int
    {
        None = 0,           // 初始化状态
        Run = 1 << 0,            // 运行状态
        Broken = 1 << 1,         // 中断状态
        Stopped = 1 << 2,        // 停止状态
        Canceled = 1 << 3,       // 取消状态
        Exceptional = 1 << 4    // 异常状态
    }

    private volatile StateFlags _state = StateFlags.None;

    // 状态属性
    public bool IsNone => _state == StateFlags.None;
    public bool IsRun => _state.HasFlag(StateFlags.Run);
    public bool IsBroken => _state.HasFlag(StateFlags.Broken);
    public bool IsStopped => _state.HasFlag(StateFlags.Stopped);
    public bool IsCanceled => _state.HasFlag(StateFlags.Canceled);
    public bool IsExceptional => _state.HasFlag(StateFlags.Exceptional);

    // 状态控制方法
    public void Start() => _state = StateFlags.Run; // 设置为运行状态
    public void Break() => _state = StateFlags.Broken; // 设置为中断状态
    public void Stop() => _state = StateFlags.Stopped; // 设置为停止状态
    public void Cancel() => _state = StateFlags.Canceled; // 设置为取消状态
    public void Exceptional() => _state |= StateFlags.Exceptional; // 设置异常状态
    public void Reset() => _state = StateFlags.None; // 重置为初始状态
}
#line default

测试类大小

using System;
using System.Runtime.CompilerServices;
namespace IFoxCAD.Cad;

public class MyClass {
    public int IntField;
    public double DoubleField;
    public string StringField;
}

public class Program {
    public static void Main() {
        MyClass obj = new MyClass();
        obj.IntField = 123;
        obj.DoubleField = 456.789;
        obj.StringField = "Hello";
        long size = Unsafe.SizeOf<MyClass>();
        Console.WriteLine("Size of MyClass: " + size + " bytes");
    }
}

无事务图元过滤器

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Collections.Generic;

namespace IFoxCAD.Cad;

public class TestEntityFilterCommands {
    [CommandMethod(nameof(TestEntityFilter), CommandFlags.UsePickSet)]
    public void TestEntityFilter()
    {
        Document doc = Application.DocumentManager.MdiActiveDocument;
        Database db = doc.Database;
        Editor ed = doc.Editor;

        var ss = ed.GetSelection();
        if (ss.Status != PromptStatus.OK) return;

        // 获取选中的ObjectId集合
        var ids = new HashSet<ObjectId>(ss.Value.GetObjectIds());

        // 创建要过滤的类型集合
        var filterTypes = new HashSet<Type> {
            typeof(Line),
            typeof(Circle)
        };
        // 过滤惰性返回id
        var filteredIds = ids.Filter(filterTypes)
            .ToList();
        // 过滤惰性返回pair,转为字典
        var filteredMap = ids.FilterToPair(filterTypes)
            .ToDictionary(pair => pair.Key, pair => pair.Value);
        ed.WriteMessage($"\n匹配的实体数量:{filteredIds.Count}");

        // 高亮显示匹配的实体
        using var tr = db.TransactionManager.StartTransaction();
        foreach (ObjectId id in filteredIds) {
            using var ent = (Entity)tr.GetObject(id, OpenMode.ForRead);
            ent.Highlight();
        }
        tr.Commit();
    }
}

public static class ObjectIdHelper {
    // 过滤得到直接匹配类型和派生关系
    // 自行保证两个参数不重复,当然,重复和无所谓
    public static IEnumerable<ObjectId> Filter(this ObjectId[] ids, HashSet<Type> typeSet) {
        if (ids is null) 
            throw new ArgumentNullException(nameof(ids));
        if (types is null) 
            throw new ArgumentNullException(nameof(types));
        // 获取顶层父类用于遍历,ClassVersion是0.0.0.0
        var baseTypes = typeSet.Select(type => 
             RXObject.GetClass(type))
                 .ToHashSet().ToList();

        // 储存排除类型,查找过的类型就不再需要找了
        var excludedTypes = new HashSet<Type>();

        foreach (var id in ids) {
            var myType = id.ObjectClass;
            // 已经排除类型
            if (excludedTypes.Contains(myType)) continue;
            // 同类直接加入
            if (typeSet.Contains(myType)) {
                yield return id;
                continue;
            }
            // 查询是否来自于派生,成功就加入同类容器,否则加入排除类型.
            // 调用者其后打印typeSet内容,
            // 就能直接加入参数,天正类型可能无法直接加入.
            var matchType = baseTypes.Find(baseType =>
                myType.IsDerivedFrom(baseType));
            if (matchType is not null) {
                typeSet.Add(matchType);
                yield return id;
                continue;
            }
            excludedTypes.Add(myType);
        }
    }

    public static IEnumerable<KeyValuePair<Type, ObjectId>> FilterToPair(
        this ObjectId[] ids, HashSet<Type> typeSet) {
        if (ids is null) 
            throw new ArgumentNullException(nameof(ids));
        if (typeSet is null) 
            throw new ArgumentNullException(nameof(typeSet));

        // 获取顶层父类用于遍历
        var baseTypes = typeSet.Select(type => 
            RXObject.GetClass(type))
                .ToHashSet().ToList();

        // 储存排除类型,查找过的类型就不再需要找了
        var excludedTypes = new HashSet<Type>();

        foreach (var id in ids) {
            var myType = id.ObjectClass;
            // 已经排除类型
            if (excludedTypes.Contains(myType)) continue;
            // 同类直接加入
            if (typeSet.Contains(myType)) {
                yield return new KeyValuePair<Type, ObjectId>(myType, id);
                continue;
            }
            // 查询是否来自于派生,成功就加入同类容器,否则加入排除类型.
            var matchType = baseTypes.Find(baseType => 
                myType.IsDerivedFrom(baseType));
            if (matchType is not null) {
                typeSet.Add(matchType);
                yield return new KeyValuePair<Type, ObjectId>(matchType, id);
                continue;
            }
            excludedTypes.Add(myType);
        }
    }

}
posted @   惊惊  阅读(140)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示