槽的定义只有一句:能被调用的东西。 普通函数、成员方法、lambda、绑定了参数的函数,全都行。
上一节讲了发出方,这一节讲接收方。 「槽」这个词听着像专有名词,其实是「C++ 里能被取地址调用的东西」, 和 Python 的 callable 是同一件事。 四种形式各有各的场景,尤其第四种(绑定参数)在工程里最常用。
一、四种槽,一张表
D35 把这四种都连上同一个信号,全都能调。
| 形式 | 写法 | 典型用途 |
|---|---|---|---|
| 普通函数 | connect(fn) | 工具函数、日志 |
| 成员方法 | connect(obj.method) | 最常见 |
| lambda | connect(lambda x: …) | 临时处理、捕获上下文 |
| 绑定参数 | partial(fn, 固定值) | 一个槽服务多个控件 |
第四行是工程里最值钱的一招:十个按钮共用一个处理函数,靠 partial 把「我是几号」粘进去。
Qt 老版本用 QSignalMapper(已废弃),新版本一律用 lambda 捕获或者 sender()。
二、lambda 捕获的隐患
lambda 能捕获外部变量,这是它最大的好处,也是最大的坑。
| 捕获什么 | 风险 |
|---|---|
| 按值捕获(Python 默认) | 安全,但拿到的是旧值 |
| 捕获 this / self | 对象销毁后就是野指针 |
| 捕获局部引用(C++ 的 &) | 函数返回后悬空 |
第二行是 Qt 里最典型的崩法:connect(btn, &Button::clicked, [this]{ this->doSomething(); }),
窗口关了、this 没了,按钮还在发信号 → 崩溃。
Qt5 的解法是 connect 传第三个参数(context):context 对象销毁,连接自动断开。
Python 里也有对应问题但表现不同:捕获的是对象引用,所以对象不会被回收(引用计数不减), 结果是「内存泄漏」而不是「崩溃」。两种都不好。
三、参数匹配规则
这条规则必须记准,它是「连不上」的最常见原因:
| 信号 | 槽 | 结果 |
|---|---|---|
| (int, int) | (int, int) | 完全匹配 |
| (int, int) | (int) | 允许,多的丢掉 |
| (int) | (int, int) | 接不上 |
第二行在 Qt 里是允许的:槽可以只关心前几个参数, 这一点在「我只想知道进度,不关心总量」时很有用。 第三行反过来了就不行,因为槽要的参数信号给不出来。
新式写法(&Class::slot)下这是编译错误,看得见;
旧式字符串写法下是运行期静默失败——connect 返回 false,而很多人不看返回值。
所以「连不上」第一件事:看 connect 的返回值。
四、槽里能做什么,不能做什么
槽就是普通函数,技术上什么都能做。但工程上有三条纪律:
| 纪律 | 原因 |
|---|---|
| 别做耗时操作 | 直连时会把发送方拖住 |
| 别改界面以外的共享状态 | 跨线程时没有锁 |
| 别在槽里 delete 发送者 | 信号还在调用栈上 |
第一条在 3.4.4 会量:槽里跑 20 毫秒,发送方就要等 20 毫秒,界面直接卡住。
第三条更阴:在槽里删掉发信号的对象,信号返回后还会继续访问它——这是典型的 use-after-free。
正确做法是投递一个「稍后删除」的消息(Qt 的 deleteLater)。
五、动手 Demo:四种槽连同一个信号,看顺序和绑定效果
普通函数、成员方法、lambda、partial 绑定参数,四种都连到同一个信号上,发一次看谁被调、
按什么顺序。
重点是第四种:它把「固定参数」和「信号参数」拼在一起。
import functools
def plain(x):
out.append("普通函数(%s)" % x)
class Recv(object):
def method(self, x):
out.append("成员方法(%s)" % x)
sig.connect(plain)
sig.connect(Recv().method)
sig.connect(lambda x: out.append("lambda(%s)" % x))
sig.connect(functools.partial(
lambda tag, x: out.append("绑定(%s,%s)" % (tag, x)), "K"))
实跑输出:
--------------------------------------------------------------------
槽可以是任何能被调用的东西
--------------------------------------------------------------------
**Qt 文档说槽是「普通成员函数」,其实更宽:只要能被调用,就能当槽。**
槽的形式 写法 典型用途
------------------------------------------------------------
普通函数 `connect(fn)` 工具函数
成员方法 `connect(obj.method)` 最常见
lambda `connect(lambda x: …)` 临时处理
绑定参数 `partial(fn, 固定值)` 一个槽处理多按钮
实际调用顺序:
普通函数(v)
成员方法(v)
lambda(v)
绑定参数(K,v)
**第四行是工程里最有用的一招**:十个按钮共用一个槽,靠 `partial` 把「我是几号」绑进去。
**Qt 里对应的写法是 `QSignalMapper`(已废弃)或 lambda 捕获**,**新代码一律用 lambda。**
**但 lambda 有一个坑,必须现在说清:**如果 lambda 捕获了局部变量,而对象的生命周期比这个变量长,
**槽被调用时读到的是野指针。**
**Qt 的解法是 `connect` 时传第三个参数(context 对象)**,那个对象销毁,连接自动断。
**这一条在 3.4.4 会专门踩一遍。**
四种槽都按登记顺序被调用,partial 那个把 K 和信号给的 v 拼成了两个参数。
这就是「一个槽服务多个按钮」的实现原理:把按钮编号绑进去,槽里按编号分支。
注意 Recv().method 这一句:我直接用了临时对象的方法。
在 Python 里这样是安全的(绑定方法抓着对象引用),
但 C++ 里就是立即悬空——临时对象出了语句就析构了。
这个差别必须清楚:Python 的坑是「忘不掉」,C++ 的坑是「忘太早」。
functools.partial 是个冷门但好用的工具:它把参数「冻」在前面,剩下的留给调用者。
Qt 里对应的是 lambda 捕获,效果一样。
30 秒实验
- 把
partial的"K"换成循环变量, 看十个按钮是不是各自记住自己的编号——注意 Python 的闭包晚绑定问题: 直接在 lambda 里用循环变量会全是最后一个。 - 把某个槽换成会抛异常的函数,看后面的槽还执行不执行。
- 用
partial给槽多绑两个参数,看信号传进来的参数落在哪个位置。
常见误区
- 以为槽必须是成员函数。 任何可调用的都行,包括 lambda、partial、甚至一个列表的 append。
- 以为 lambda 捕获对象是安全的。 捕获 this/self 后对象被销毁,就是野指针。 Qt 的解法是 connect 时传 context 对象。
- 以为 C++ 里
Obj().method也能用。 临时对象当场析构,连接立刻悬空。 Python 里安全是因为引用计数。
自测三问(点开对答案)
1. 槽的参数比信号少,能连上吗? —— 能。多的参数会被丢掉。反过来(槽要的参数比信号多)就连不上。 2. 为什么 lambda 捕获 this 会崩? —— 对象销毁后,捕获的指针还在,槽被调用时访问的是已释放的内存。 Qt 的解法是 connect 传 context 对象,自动断开。 3. `partial` 在槽里解决什么问题? —— 把固定参数绑在前面,剩下的留给信号传。常用于「十个按钮共用一个槽,靠编号区分」。下一节讲怎么把信号和槽接上:connect 的五种方式,其中线程那两种是重点。
- ☐ (本节完,下一节 03.04.03 信号与槽的关联)