元对象与信号槽通信契约
1. 一条连接要回答六问
- sender 和 receiver/context 谁拥有?
- 槽最终在哪个线程执行?
- 参数按什么语义复制/共享?
- receiver 销毁时连接是否自动断开?
- 高频信号积压时如何背压/合并?
- 关闭时队列中的旧事件是否仍有效?
信号槽解决解耦和调度,不自动解决所有权、线程安全与流量控制。
2. MOC 协议
Q_OBJECT 让类拥有 metaObject、信号分派、属性、qobject_cast 等支持。生成代码布局是 Qt 实现细节,不应手工依赖 moc 内部索引。
函数指针 connect 提供编译期签名检查;字符串语法主要用于旧代码/动态场景,重构安全较弱。
3. Connection type
| 类型 | 槽执行位置 |
|---|---|
| Direct | 发射信号的当前线程,调用栈内同步执行 |
| Queued | receiver 线程事件循环稍后执行 |
| Auto | 发射时按当前线程与 receiver affinity 决定 direct/queued |
| BlockingQueued | 排队并阻塞发射线程直到槽完成 |
Auto 不是简单比较 sender/receiver 对象 affinity,而是发射时当前执行线程与 receiver 所在线程。Direct 跨线程会在发射线程直接调用 receiver 代码,必须自行证明安全。
BlockingQueued 在同线程使用会死锁;跨线程也容易与反向锁/关闭形成死锁,只用于严格受控 RPC 式边界。
4. Context object
connect(reply, &QNetworkReply::finished,
this, [this, reply] { handle(reply); });2
给 Lambda 提供 this context,使 context 销毁后连接自动断开,并决定 queued 执行线程。无 context 的长期 Lambda 连接容易捕获悬空指针。
自动断开不撤销已经启动的外部工作;reply/worker 的所有权和取消仍需处理。
5. Queued 参数
Queued 调用需把参数保存到事件中。自定义类型通常需要 Q_DECLARE_METATYPE,运行时场景可能还需 qRegisterMetaType;以当前 Qt 版本和连接语法为准。
传裸指针只复制地址,不保活对象。大型容器按值可能产生复制/共享分离成本;可使用不可变 shared data、ID + 数据仓库或批量快照,但明确线程安全。
6. 积压与合并
进度每个迭代发信号可能淹没 GUI event queue。策略:
- 限频到例如每帧/几十毫秒;
- 只保存最新进度,单个 queued 更新读取最新值;
- 批量发送结果;
- receiver 落后时丢弃过期可替代事件;
- 完成/错误事件不可被进度覆盖。
7. 连接身份与断开
保存 QMetaObject::Connection 可在状态切换时显式断开。Qt::UniqueConnection 对某些 Lambda/函数对象形式有限制,不能把它当所有重复连接的万能检测。设计上让 connect 位置只执行一次更可靠。
8. 面试速答
AutoConnection 何时决定类型? 发射信号时。
QueuedConnection 是否同步阻塞 sender? 不会,事件入队后返回;BlockingQueued 才阻塞。
receiver 销毁后连接怎样? QObject 连接会自动移除,但外部任务和捕获的其他裸指针不自动安全。
9. 自测答案
GUI receiver 被 moveToThread 是否可行?QWidget 不可移动到工作线程;一般 QObject 也要求无 parent 才能 move。传 const BigData& 的 queued signal 是否零拷贝?签名写引用不代表排队事件不保存参数,需按元类型复制语义评估。
元对象代码从哪里来
header with Q_OBJECT
-> moc scans declarations
-> generated meta-object C++
-> compile/link with target
-> runtime QMetaObject tables + qt_metacall2
3
4
5
出现 undefined vtable/metaObject 常要检查 AUTOMOC、源文件是否加入 target、Q_OBJECT 所在头是否被扫描,而不是只补一个虚析构。
新式 connect 在编译期检查什么
connect(sender, &Sender::valueChanged,
receiver, &Receiver::updateValue);2
编译器检查成员指针与参数兼容;运行时仍决定对象生命、连接类型和线程投递。lambda connect 应提供 context 对象,使 context 析构时自动断开并决定 queued 执行线程。
Connection 是一份路由契约
sender signal
-> connection record [receiver/context, method/thunk, type]
-> Direct: current call stack
-> Queued: copy args into event -> receiver event loop2
3
4
AutoConnection 在 emit 时根据当前执行线程与 receiver affinity 决定,不是 connect 创建时永久决定。
queued 参数与元类型
参数要能安全跨事件队列保存。自定义类型应具有明确复制/移动语义,并按 Qt 版本/API 注册元类型;不要传裸指针指向短命对象。大对象可用共享不可变值或 ID,但所有权要明确。
自动断开与排队事件
sender/receiver/context 销毁会使连接失效,防止未来派发;已经入队的元调用如何处理依赖接收对象事件投递与销毁状态。关闭不能只调用 disconnect 就假定所有业务工作停止,还需取消在途任务和等待线程。
DirectConnection 的重入
Direct slot 在 emit 调用栈立即执行,可能回调 sender、修改正在迭代的容器或触发嵌套事件:
emit -> slot -> changes model -> emits another signal -> nested slot信号发射点要维持可重入不变量,或通过 queued connection 明确延后;后者又引入异步顺序和生命问题。
BlockingQueuedConnection 的死锁图
T1 emits blocking to T2 and waits
T2 waits lock held by T1 / emits blocking back to T1
-> deadlock2
3
同线程使用 blocking queued 也会自锁。它只适合严格控制的跨线程同步边界,通常更好用异步结果/状态机。
属性与反射边界
Q_PROPERTY 提供名字、读写方法、NOTIFY 等元数据,适合 QML、编辑器和序列化适配;它不自动保存字段或验证线程。属性 setter 仍需维护不变量,NOTIFY 应在值真正改变后发出并避免循环绑定。
连接诊断清单
- sender/receiver/context 谁活得更久?
- emit 时在哪个线程,receiver affinity 在哪?
- queued 参数可否安全复制并注册?
- 是否可能重入或反馈环?
- 是否重复 connect,需要 UniqueConnection/令牌?
- 关闭时已排队/在途工作怎样处理?
连续追问与自测
问:signal 是否一定异步? 答:不一定,Direct/同线程 Auto 常同步;跨线程 Auto 通常 queued。
问:context lambda connect 的价值? 答:自动断开与确定接收线程/生命周期边界。
- moc 是编译器吗? 答:它生成补充 C++ 元对象代码,之后仍由 C++ 编译器编译。
- AutoConnection 何时决定 direct/queued? 答:信号发射时根据线程关系。
- disconnect 能否取消业务线程任务? 答:不能,只影响信号路由,任务需独立取消协议。
工程深化:把本篇知识落到项目里
这一节不是为了凑篇幅,而是把《元对象与信号槽通信契约》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。
concept -> boundary -> failure signal -> minimal proof -> project rule工程切片 1:最小可复现样例
**场景。**围绕 moc 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 moc、信号槽 还是 连接类型 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> moc -> 信号槽 -> 连接类型 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 2:接口边界
**场景。**围绕 信号槽 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。
**边界。**先判断这里讨论的是 信号槽、连接类型 还是 元对象 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 信号槽 -> 连接类型 -> 元对象 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 3:失败注入
**场景。**围绕 连接类型 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。
**边界。**先判断这里讨论的是 连接类型、元对象 还是 线程亲和性 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 连接类型 -> 元对象 -> 线程亲和性 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 4:跨平台差异
**场景。**围绕 元对象 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。
**边界。**先判断这里讨论的是 元对象、线程亲和性 还是 断开连接 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 元对象 -> 线程亲和性 -> 断开连接 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 5:性能观察
**场景。**围绕 线程亲和性 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。
**边界。**先判断这里讨论的是 线程亲和性、断开连接 还是 moc 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程亲和性 -> 断开连接 -> moc -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 6:生命周期
**场景。**围绕 断开连接 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。
**边界。**先判断这里讨论的是 断开连接、moc 还是 信号槽 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 断开连接 -> moc -> 信号槽 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 7:诊断证据
**场景。**围绕 moc 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。
**边界。**先判断这里讨论的是 moc、信号槽 还是 连接类型 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> moc -> 信号槽 -> 连接类型 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 8:维护策略
**场景。**围绕 信号槽 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。
**边界。**先判断这里讨论的是 信号槽、连接类型 还是 元对象 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 信号槽 -> 连接类型 -> 元对象 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 9:版本演进
**场景。**围绕 连接类型 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。
**边界。**先判断这里讨论的是 连接类型、元对象 还是 线程亲和性 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 连接类型 -> 元对象 -> 线程亲和性 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 10:最小消费者
**场景。**围绕 元对象 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。
**边界。**先判断这里讨论的是 元对象、线程亲和性 还是 断开连接 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 元对象 -> 线程亲和性 -> 断开连接 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 11:边界复盘
**场景。**围绕 线程亲和性 建一个很小的案例,不要一开始就放进完整业务系统。用一张表说明问题发生在源码、编译、链接、加载、运行、关闭或发布中的哪一层。
**边界。**先判断这里讨论的是 线程亲和性、断开连接 还是 moc 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程亲和性 -> 断开连接 -> moc -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 12:反例训练
**场景。**围绕 断开连接 建一个很小的案例,不要一开始就放进完整业务系统。写出一个看似能工作但不可维护的方案,再说明它在规模、团队协作或发布时会怎样失败。
**边界。**先判断这里讨论的是 断开连接、moc 还是 信号槽 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 断开连接 -> moc -> 信号槽 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 13:最小可复现样例
**场景。**围绕 moc 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 moc、信号槽 还是 连接类型 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> moc -> 信号槽 -> 连接类型 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 14:接口边界
**场景。**围绕 信号槽 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。
**边界。**先判断这里讨论的是 信号槽、连接类型 还是 元对象 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 信号槽 -> 连接类型 -> 元对象 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 15:失败注入
**场景。**围绕 连接类型 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。
**边界。**先判断这里讨论的是 连接类型、元对象 还是 线程亲和性 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 连接类型 -> 元对象 -> 线程亲和性 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 16:跨平台差异
**场景。**围绕 元对象 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。
**边界。**先判断这里讨论的是 元对象、线程亲和性 还是 断开连接 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 元对象 -> 线程亲和性 -> 断开连接 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 17:性能观察
**场景。**围绕 线程亲和性 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。
**边界。**先判断这里讨论的是 线程亲和性、断开连接 还是 moc 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程亲和性 -> 断开连接 -> moc -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 18:生命周期
**场景。**围绕 断开连接 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。
**边界。**先判断这里讨论的是 断开连接、moc 还是 信号槽 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 断开连接 -> moc -> 信号槽 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 19:诊断证据
**场景。**围绕 moc 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。
**边界。**先判断这里讨论的是 moc、信号槽 还是 连接类型 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> moc -> 信号槽 -> 连接类型 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 20:维护策略
**场景。**围绕 信号槽 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。
**边界。**先判断这里讨论的是 信号槽、连接类型 还是 元对象 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 信号槽 -> 连接类型 -> 元对象 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 21:版本演进
**场景。**围绕 连接类型 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。
**边界。**先判断这里讨论的是 连接类型、元对象 还是 线程亲和性 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 连接类型 -> 元对象 -> 线程亲和性 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 22:最小消费者
**场景。**围绕 元对象 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。
**边界。**先判断这里讨论的是 元对象、线程亲和性 还是 断开连接 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 元对象 -> 线程亲和性 -> 断开连接 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 23:边界复盘
**场景。**围绕 线程亲和性 建一个很小的案例,不要一开始就放进完整业务系统。用一张表说明问题发生在源码、编译、链接、加载、运行、关闭或发布中的哪一层。
**边界。**先判断这里讨论的是 线程亲和性、断开连接 还是 moc 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程亲和性 -> 断开连接 -> moc -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
本篇收束
掌握《元对象与信号槽通信契约》的标志,不是记住所有名词,而是能把 moc、信号槽、连接类型、元对象、线程亲和性、断开连接 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。