我正在学习 Qt,并且对信号槽模式很感兴趣。我的问题是,信号和插槽只是事件侦听器和处理程序的语法糖,还是后台发生的事情具有不同的性质?如果是后者,根本区别是什么?
3 回答
这不仅仅是语法糖。在 Qt 信号/槽的背景下有一个真正的工作。这项工作由 MOC(元对象编译器)完成。这就是在所有 C++ 头文件中都有一个包含带有 Q_OBJECT 宏的类的过程的原因。
信号/插槽的“困难部分”是当您处于多线程上下文中时。事实上,connect() 函数的 Qt::ConnectionType 参数,它在单线程环境中是 Direct(例如,函数的直接调用),如果发送者和发射器不在同一个线程中,则在队列中排队。在这种情况下,信号必须由 Qt 事件循环处理。
信号和槽是语法糖还是更多?我的问题是,信号和插槽只是事件侦听器/处理程序的语法糖
不,它们存在的平均原因是排放和处理的脱钩。
或者背景中发生的事情具有不同的性质?
这个想法是将发射与某个“事件”的处理分开。如果您考虑将直接函数调用作为替代方案,我想指出,对于它们,负责发射的代码需要了解实际处理“信号”的代码。那就是这两个部分彼此太紧了。
如果不更改负责信号发射的代码,就不可能为信号动态添加另一个处理程序。想象一个这样的例子:
代码的某些部分发出信号“水果到了”
此代码将直接调用“洗水果”方法。
如果有人想添加一种方法来计算水果的数量怎么办?
- 需要修改前面的代码以包含对该方法的直接调用。
使用信号槽机制,您无需接触原始代码。您可以通过完全不同的代码将新插槽简单地连接到现有信号。这被称为良好的设计和自由。
当您拥有像 Qt 这样可以在不事先了解 Qt 应用程序的情况下发出信号的库时,这尤其有用。如何处理信号取决于 Qt 应用程序。
此外,这种模式还使应用程序响应更快,阻塞更少,方向函数调用就是这种情况。这是因为存在构建 Qt 信号槽机制的事件循环。当然,您也可以将线程与直接调用一起使用,但与理想世界中所需的相比,它的工作量更大且难以维护。
因此,正如部分已经触及的那样,后台有一个 QtEventLoop 将这些事件排队等待处理,尽管也可以执行“直接调用”。
真正的背景内部实现代码可以在那里找到,也可以在 moc(元对象编译器)中找到。Moc 基本上是为您没有为其定义主体的信号创建一个函数,因此您只需在需要时在 QObject 子类中声明它们。
您可以在此处阅读有关该主题的更多信息,但我认为我的解释可以让您继续前进:
QtDD13 - Olivier Goffart - Qt 5 中的信号和插槽
信号和槽是一种将方法调用与被调用方法解耦的方法。它们根本不是语法糖,因为它们没有向 C++ 语言添加新语法。信号发射是一种方法调用。插槽是一个普通的旧实例方法。链接两者的代码是普通的旧 C++。这里没有什么新鲜事——没有任何种类的糖。
大多数你称之为“语法糖”的东西类似于注释——那些是空定义(Q_SLOT
, Q_SIGNAL
, signals
, slots
),用于标记元对象编译器(moc)处理的方法。Moc 基于声明的正常 C++ 语法生成自省信息和信号实现(有一些限制)。
我声称这不是语法糖,因为 moc 理解常规 C++,并且不是基于任何语法糖,而是基于通常的实例方法声明生成自省数据。“糖”是为了避免让 moc 为类声明中的所有内容生成元数据而过早悲观。它还让 moc 忽略方法定义 - 否则它需要解析它们,并假设没有定义的方法是信号。
该emit
宏仅供人类使用,仅表明方法调用实际上是信号发射。moc 不使用它。它被定义为空。
Q_OBJECT
和Q_GADGET
宏声明了一些用于访问元数据的类成员。可以说,它们是唯一真正的“糖”,因为它使您不必输入几行声明。
使其工作可能涉及相当多的代码。
一个信号:
是一个实例方法,其实现由 moc 生成,
具有关于其名称和参数的完整自省信息。这可作为
QMetaMethod
.
一个插槽:
是您提供其实现的实例方法,
同样具有完整的内省信息。
元信息在运行时可用,并且可以被不知道信号或槽的签名的代码枚举和使用。
当您发出信号时,您只需调用 moc 生成的方法。此方法调用获取相关互斥锁的 Qt 库代码,迭代附加槽的列表,并执行调用,并在此过程中根据需要获取额外的互斥锁。正确执行此操作需要小心,因为发送者和接收者对象可以驻留在不同的线程中。必须避免将槽调用传递给不存在的对象。哦,你也不想要死锁。这需要一些先见之明。
由于信号和槽都只是方法,因此您当然可以将信号连接到其他信号 - 底层机制并不关心被调用的内容,它只是一个可调用的方法。不可调用的方法是那些没有元数据的方法。
发出相关信号时会调用插槽。信号发射只是对生成的信号主体的方法调用。这与事件侦听器模式不同,因为插槽调用可以是立即的(所谓的直接连接)或延迟到事件循环(所谓的排队连接)。通过复制参数并将它们捆绑在一个QMetaCallEvent
. 这个事件被“转换”回一个方法调用QObject::event
。这发生在事件循环将事件传递给目标对象时。
元数据不仅仅包含信号和槽签名。它还允许您默认和复制构造信号/槽参数类型 - 这是实现排队调用所必需的。它还包含用于枚举的键值对——这就是 Qt 相当容易编写脚本的原因。传递给 Qt 方法的所有枚举值都可以在运行时按名称查找!