https://blog.csdn.net/liuuze5/article/details/53523463html
深刻理解信号槽(一)ios
这篇文章来自于 A Deeper Look at Signals and Slots,Scott Collins 2005.12.19。须要说明的是,咱们这里所说的“信号槽”不只仅是指 Qt 库里面的信号槽,而是站在一个全局的高度,从系统的角度来理解信号槽。因此在这篇文章中,Qt 信号槽仅仅做为一种实现来介绍,咱们还将介绍另一种信号槽的实现——boost::signal。所以,当你在文章中看到一些信号的名字时,或许仅仅是为了描述方便而杜撰的,实际并无这个信号。程序员
什么是信号槽?编程
这个问题咱们能够从两个角度来回答,一个简短一些,另一个则长些。安全
让咱们先用最简洁的语言来回答这个问题——什么是信号槽?架构
信号和槽是多对多的关系。一个信号能够链接多个槽,而一个槽也能够监听多个信号。app
信号能够有附加信息。例如,窗口关闭的时候可能发出 windowClosing 信号,而这个信号就能够包含着窗口的句柄,用来代表到底是哪一个窗口发出这个信号;一个滑块在滑动时可能发出一个信号,而这个信号包含滑块的具体位置,或者新的值等等。咱们能够把信号槽理解成函数签名。信号只能同具备相同签名的槽链接起来。你能够把信号当作是底层事件的一个形象的名字。好比这个 windowClosing 信号,咱们就知道这是窗口关闭事件发生时会发出的。框架
信号槽实际是与语言无关的,有不少方法均可以实现信号槽,不一样的实现机制会致使信号槽差异很大。信号槽这一术语最初来自 Trolltech 公司的 Qt 库(如今已经被 Nokia 收购)。1994年,Qt 的第一个版本发布,为咱们带来了信号槽的概念。这一律念马上引发计算机科学界的注意,提出了多种不一样的实现。现在,信号槽依然是 Qt 库的核心之一,其余许多库也提供了相似的实现,甚至出现了一些专门提供这一机制的工具库。ide
简单了解信号槽以后,咱们再来从另一个角度回答这个问题:什么是信号槽?它们从何而来?函数
前面咱们已经了解了信号槽相关的概念。下面咱们将从更细致的角度来探讨,信号槽机制是怎样一步步发展的,以及怎样在你本身的代码中使用它们。
程序设计中很重要的一部分是组件交互:系统的一部分须要告诉另外一部分去完成一些操做。让咱们从一个简单的例子开始:
|
换句话说,Page 类知道如何从新载入页面(reload),Button 有一个动做是点击(click)。假设咱们有一个函数返回当前页面 currentPage(),那么,当 button 被点击的时候,当前页面应该被从新载入。
|
这看起来并不很好。由于 Button 这个类名彷佛暗示了这是一个可重用的类,可是这个类的点击操做却同 Page 牢牢地耦合在一块儿了。这使得只要 button 一被点击,一定调用 currentPage() 的 reload() 函数。这根本不能被重用,或许把它更名叫 PageReloadButton 更好一些。
实际上,不得不说,这确实是一种实现方式。若是 Button::click() 这个函数是 virtual 的,那么你彻底能够写一个新类去继承这个 Button:
|
好了,如今 Button 能够被重用了。可是这并非一个很好的解决方案。
引入回调
让咱们停下来,回想一下在只有 C 的时代,咱们该如何解决这个问题。若是只有 C,就不存在 virtual 这种东西。重用有不少种方式,可是因为没有了类的帮助,咱们采用另外的解决方案:函数指针。
|
这就是一般所说的“回调”。buttonClicked() 函数在编译期并不知道要调用哪个函数。被调用的函数是在运行期传进来的。这样,咱们的 Button 就能够被重用了,由于咱们能够在运行时将不一样的函数指针传递进来,从而得到不一样的点击操做。
增长类型安全
对于 C++ 或者 Java 程序员来讲,老是不喜欢这么作。由于这不是类型安全的(注意 url 有一步强制类型转换)。
咱们为何须要类型安全呢?一个对象的类型其实暗示了你将如何使用这个对象。有了明确的对象类型,你就可让编译器帮助你检查你的代码是否是被正确的使用了,如同你画了一个边界,告诉编译器说,若是有人越界,就要报错。然而,若是没有类型安全,你就丢失了这种优点,编译器也就不能帮助你完成这种维护。这就如同你开车同样。只要你的速度足够,你就可让你的汽车飞起来,可是,通常来讲,这种速度就会提醒你,这太不安全了。同时还会有一些装置,好比雷达之类,也会时时帮你检查这种状况。这就如同编译器帮咱们作的那样,是咱们出浴一种安全使用的范围内。
回过来再看看咱们的代码。使用 C 不是类型安全的,可是使用 C++,咱们能够把回调的函数指针和数据放在一个类里面,从而得到类型安全的优点。例如:
|
好了!咱们的 Button 已经能够很方便的重用了,而且也是类型安全的,再也没有了强制类型转换。这种实现已经能够解决系统中遇到的绝大部分问题了。彷佛如今的解决方案同前面的相似,都是继承了一个类。只不过如今咱们对动做进行了抽象,而以前是对 Button 进行的抽象。这很像前面 C 的实现,咱们将不一样的动做和 Button 关联起来。如今,咱们一步步找到一种比较使人满意的方法。
多对多
下一个问题是,咱们可以在点击一次从新载入按钮以后作多个操做吗?也就是让信号和槽实现多对多的关系?
实际上,咱们只须要利用一个普通的链表,就能够轻松实现这个功能了。好比,以下的实现:
|
这就是其中的一种实现。不要以为这种实现看上去没什么水平,实际上咱们发现这就是一种至关简洁的方法。同时,不要纠结于咱们代码中的 std:: 和 boost:: 这些命名空间,你彻底能够用另外的类,强调一下,这只是一种可能的实现。如今,咱们的一个动做能够链接多个 button 了,固然,也能够是别的 Action 的使用者。如今,咱们有了一个多对多的机制。经过将 AbstractAction* 替换成 boost::shared_ptr,能够解决 AbstractAction 的归属问题,同时保持原有的多对多的关系。
这会有不少的类!
若是你在实际项目中使用上面的机制,不少就会发现,咱们必须为每个 action 定义一个类,这将不可避免地引发类爆炸。至今为止,咱们前面所说的全部实现都存在这个问题。不过,咱们以后将着重讨论这个问题,如今先不要纠结在这里啦!
特化!特化!
当咱们开始工做的时候,咱们经过将每个 button 赋予不一样的 action,实现 Button 类的重用。这实际是一种特化。然而,咱们的问题是,action 的特化被放在了固定的类层次中,在这里就是这些 Button 类。这意味着,咱们的 action 很难被更大规模的重用,由于每个 action 实际都是与 Button 类绑定的。那么,咱们换个思路,能不能将这种特化放到信号与槽链接的时候进行呢?这样,action 和 button 这二者都没必要进行特化了。
函数对象
将一个类的函数进行必定曾度的封装,这个思想至关有用。实际上,咱们的 Action 类的存在,就是将 execute() 这个函数进行封装,其余别无用处。这在 C++ 里面仍是比较广泛的,不少时候咱们用 ++ 的特性从新封装函数,让类的行为看起来就像函数同样。例如,咱们重载 operator() 运算符,就可让类看起来很像一个函数:
|
这样,咱们的类看起来很像函数。前面代码中的 for_each 也得作相应的改变:
|
如今,咱们的 Button::clicked() 函数的实现有了更多的选择:
|
看起来很麻烦,值得这样作吗?
下面咱们来试着解释一下信号和槽的目的。看上去,重写 operator() 运算符有些过度,并不值得咱们去这么作。可是,要知道,在某些问题上,你提供的可用的解决方案越多,越有利于咱们编写更简洁的代码。经过对一些类进行规范,就像咱们要让函数对象看起来更像函数,咱们可让它们在某些环境下更适合重用。在使用模板编程,或者是 Boost.Function,bind 或者是模板元编程的情形下,这一点尤其重要。
这是对无需更多特化创建信号槽链接重要性的部分回答。模板就提供了这样一种机制,让添加了特化参数的代码并不那么难地被特化,正如咱们的函数对象那样。而模板的特化对于使用者而言是透明的。
松耦合
如今,让咱们回顾一下咱们以前的种种作法。
咱们执着地寻求一种可以在同一个地方调用不一样函数的方法,这其实是 C++ 内置的功能之一,经过 virtual 关键字,固然,咱们也可使用函数指针实现。当咱们须要调用的函数没有一个合适的签名,咱们将它包装成一个类。咱们已经演示了如何在同一地方调用多个函数,至少咱们知道有这么一种方法(但这并非在编译期完成的)。咱们实现了让“信号发送”可以被若干个不一样的“槽”监听。
不过,咱们的系统的确没有什么很是不同凡响的地方。咱们来仔细审核一下咱们的系统,它真正不一样的是:
可是,这样的系统还远达不到松耦合的关系。Button 类并不须要知道 Page 类。松耦合意味着更少的依赖;依赖越少,组件的可重用性也就越高。
固然,确定须要有组件同时知道 Button 和 Page,从而完成对它们的链接。如今,咱们的链接实际是用代码描述的,若是咱们不用代码,而用数据描述链接呢?这么一来,咱们就有了松耦合的类,从而提升两者的可重用性。
新的链接模式
什么样的链接模式才算是非代码描述呢?假如仅仅只有一种信号槽的签名,例如 void (*signature)(),这并不能实现。使用散列表,将信号的名字映射到匹配的链接函数,将槽的名字映射到匹配的函数指针,这样的一对字符串便可创建一个链接。
然而,这种实现其实包含一些“握手”协议。咱们的确但愿具备多种信号槽的签名。在信号槽的简短回答中咱们提到,信号能够携带附加信息。这要求信号具备参数。咱们并无处理成员函数与非成员函数的不一样,这又是一种潜在的函数签名的不一样。咱们尚未决定,咱们是直接将信号链接到槽函数上,仍是链接到一个包装器上。若是是包装器,这个包装器须要已经存在呢,仍是咱们在须要时自动建立呢?虽然底层思想很简单,可是,真正的实现还须要很好的努力才行。彷佛经过类名可以建立对象是一种不错的想法,这取决于你的实现方式,有时候甚至取决于你有没有能力作出这种实现。将信号和槽放入散列表须要一种注册机制。一旦有了这么一种系统,前面所说的“有太多类了”的问题就得以解决了。你所须要作的就是维护这个散列表的键值,而且在须要的时候实例化类。
给信号槽添加这样的能力将比咱们前面所作的全部工做都困可贵多。在由键值进行链接时,多数实现都会选择放弃编译期类型安全检查,以知足信号和槽的兼容。这样的系统代价更高,可是其应用也远远高于自动信号槽链接。这样的系统容许实例化外部的类,好比 Button 以及它的链接。因此,这样的系统有很强大的能力,它可以完成一个类的装配、链接,并最终完成实例化操做,好比直接从资源描述文件中导出的一个对话框。既然它可以凭借名字使函数可用,这就是一种脚本能力。若是你须要上面所说的种种特性,那么,完成这么一套系统绝对是值得的,你的信号槽系统也会从中受益,由数据去完成信号槽的链接。
对于不须要这种能力的实现则会忽略这部分特性。从这点看,这种实现就是“轻量级”的。对于一个须要这些特性的库而言,完整地实现出来就是一个轻量级实现。这也是区别这些实现的方法之一。
信号槽的实现实例—— Qt 和 Boost
Qt 的信号槽和 Boost.Signals 因为有着大相径庭的设计目标,所以两者的实现、强度也十分不一样。将两者混合在一块儿使用也不是不可能的,咱们将在本系统的最后一部分来讨论这个问题。
使用信号槽
信号槽是伟大的工具,可是如何能更好的使用它们?相比于直接函数调用,有三点值得咱们的注意。一个信号槽的调用:
使用信号槽进行解耦,咱们得到的最大的好处是,链接两端的对象不须要知道对方的任何信息。Button 同动做的链接是一个很典型的案例。例如以下信息:
|
Elevator 类,也就是电梯,不须要知道有多少显示器正在监听它的信号,也不须要知道这些显示器的任何信息。每一层可能有一个屏幕和一组灯,用于显示电梯的当前位置和方向,另一些远程操控的面板也会显示出一样的信息。电梯并不关心这些东西。当它穿过(或者停在)某一层的时候,它会发出一个 floorChanged(int) 信号。或许,交通讯号灯是更合适的一个例子。
你也能够实现一个应用程序,其中每个函数调用都是经过信号来触发的。这在技术上说是彻底没有问题的,然而倒是不大可行的,由于信号槽的使用无疑会丧失一部分代码可读性和系统性能。如何在这其中作出平衡,也是你须要考虑的很重要的一点。
Qt 方式
了解 Qt 信号槽最好的莫过于 Qt 的文档。不过,这里咱们从一个小例子来了解信号槽的 Qt 方式的使用。
|
Boost.Signals 方式
了解 Boost.Signals 的最好方式一样是 Boost 的文档。这里,咱们仍是先从代码的角度了解一下它的使用。
|
对比
或许你已经注意到上面的例子中,不管是 Qt 的实现方式仍是 Boost 的实现方式,除了必须的 Button 和 Page 两个类以外,都不须要额外的类。两种实现都解决了类爆炸的问题。下面让咱们对照着来看一下咱们前面的分析。如今咱们有:
Boost.Signals | Qt Signals 和 Slots |
一个信号就是一个对象 | 信号只能是成员函数 |
发出信号相似于函数调用 | 发出信号相似于函数调用,Qt 提供了一个 emit 关键字来完成这个操做 |
信号能够是全局的、局部的或者是成员对象 | 信号只能是成员函数 |
任何可以访问到信号对象的代码均可以发出信号 | 只有信号的拥有者才能发出信号 |
槽是任何可被调用的函数或者函数对象 | 槽是通过特别设计的成员函数 |
能够有返回值,返回值能够在多个槽中使用 | 没有返回值 |
同步的 | 同步的或者队列的 |
非线程安全 | 线程安全,能够跨线程使用 |
当且仅当槽是可追踪的时候,槽被销毁时,链接自动断开 | 槽被销毁时,链接都会自动断开(由于全部槽都是可追踪的) |
类型安全(编译器检查) | 类型安全(运行期检查) |
参数列表必须彻底一致 | 槽能够忽略信号中多余的参数 |
信号、槽能够是模板 | 信号、槽不能是模板 |
C++ 直接实现 | 经过由 moc 生成的元对象实现(moc 以及元对象系统都是 C++ 直接实现的) |
没有内省机制 | 能够经过内省发现 能够经过元对象调用 链接能够从资源文件中自动推断出 |
最重要的是,Qt 的信号槽机制已经深深地植入到框架之中,成为不可分割的一部分。它们可使用 Qt 专门的开发工具,例如 QtCreator,经过拖拽的方式很轻松的建立、删除、修改。它们甚至能够经过动态加载资源文件,由特定命名的对象自动动态生成。这些都是 boost 做为一个通用库所不可能提供的。
将 Qt 的信号槽系统与 Boost.Signals 结合使用
实际上,将 Qt 的信号槽系统与 Boost.Signals 结合在一块儿使用并不是不可能。经过前面的阐述,咱们都知道了两者的不一样,至于为何要将这两者结合使用,则是见仁见智的了。这里,咱们给出一种结合使用的解决方案,可是并非说咱们暗示应该将它们结合使用。这应该是具体问题具体分析的。
将 Qt 的信号槽系统与 Boost.Signals 结合使用,最大的障碍是,Qt 使用预处理器定义了关键字 signals,slots 以及 emit。这些能够看作是 Qt 对 C++ 语言的扩展。同时,Qt 也提供了另一种方式,即便用宏来实现这些关键字。为了屏蔽掉这些扩展的关键字,Qt 4.1 的 pro 文件引入了 no_keywords 选项,以便使用标准 C++ 的方式,方便 Qt 与其余 C++ 同时使用。你能够经过打开 no_keywords 选项,来屏蔽掉这些关键字。下面是一个简单的实现:
|
请注意,咱们已经在 pro 文件中打开了 no_keywords 选项,那么,相似 signals 这样的关键字已经不起做用了。因此,咱们必须将这些关键字修改为相应的宏的版本。例如,咱们须要将 signals 改成 Q_SIGNALS,将 slots 改成 Q_SLOTS 等等。请看下面的代码:
|
如今咱们有了一个发送者,下面来看看接收者:
|
下面,咱们来测试一下:
|
这段代码将会有相似下面的输出:
|
咱们能够看到,这两种实现的不一样之处在于,Boost.Signals 的信号,boostSignal,是 public 的,任何对象均可以直接发出这个信号。也就是说,咱们可使用以下的代码:
|
从而绕过咱们设置的 sendBoostSignal() 这个触发函数。另外,咱们能够看到,boostSignal 彻底能够是一个全局对象,这样,任何对象均可以使用这个信号。而对于 Qt 来讲,signal 必须是一个成员变量,在这里,只有 Sender 可使用咱们定义的信号。
这个例子虽然简单,然而已经很清楚地为咱们展现了,如何经过 Qt 发出信号来获取 Boost 的行为。在这里,咱们使用一个公共的 sendQtSignal() 函数发出 Qt 的信号。然而, 为了从 Boost 的信号获取 Qt 的行为,咱们须要多作一些工做:隐藏信号,可是须要提供获取链接的函数。这样看上去有些麻烦:
|
应该说,这样的实现至关丑陋。实际上,咱们将 Boost 的信号与链接分割开了。咱们但愿可以有以下的实现:
|
注意,这只是个人但愿,并无作出实现。若是你有兴趣,不妨尝试一下。
总结
前面啰嗦了这么多,如今总结一下。
信号和槽的机制其实是观察者模式的一种变形。它是面向组件编程的一种很强大的工具。如今,信号槽机制已经成为计算机科学的一种术语,也有不少种不一样的实现。
Qt 信号槽是 Qt 整个架构的基础之一,所以它同 Qt 提供的组件、线程、反射机制、脚本、元对象机制以及可视化 IDE 等等紧密地集成在一块儿。Qt 的信号是对象的成员函数,因此,只有拥有信号的对象才能发出信号。Qt 的组件和链接能够由非代码形式的资源文件给出,而且可以在运行时动态创建这种链接。Qt 的信号槽实现创建在 Qt 元对象机制之上。Qt 元对象机制由 Qt 提供的 moc 工具实现。moc 也就是元对象编译器,它可以将用户指定的具备 Q_OBJECT 宏的类进行必定程度的预处理,给这个增长元对象能力。
Boost.Signals 是具备静态的类型安全检查的,基于模板的信号槽系统的实现。全部的信号都是模板类 boost::signal 的一个特化;全部的槽函数都具备相匹配的可调用的签名。Boost.Signals 是独立的,不须要内省、元对象系统,或者其余外部工具的支持。然而,Boost.Signals 没有从资源文件动态创建链接的能力。
这两种实现都很是漂亮,而且都具备工业强度。将它们结合在一块儿使用也不是不可能的,Qt 4.1 即提供了这种可能性。
任何基于 Qt GUI 的系统都会天然而然的使用信号槽。你能够从中获取很大的好处。任何大型的系统,若是但愿可以下降组件之间的耦合程度,都应该借鉴这种思想。正如其余的机制和技术同样,最重要的是把握一个度。在正确的地方使用信号槽,可让你的系统更易于理解、更灵活、高度可重用,而且你的工做也会完成得更快。