今天主要讲如何编写Rust事件
为每个接收器编写的自定义转换。 最终声明为:
声明并使用发射器和接收器的变量,分别为emitter1,r1和r2。可以使用想要的任何名称作为标识符。通过使用重复捕获,将它们与唯一的闭包配对,可以使多个接收器具有可变的功能。
另请注意,当声明MySignal
时,即使这些标识符都不存在,也将通过宏扩展使用提供的名称来创建它们。
本文包含了我认为是出色的解决方案。
从emitter
或signal
(发送事件数据)和receiver
(对信号发送的数据进行处理)方面来考虑事件。 我认为实现信号和接收器的最佳方法是使用Rust trait。 Receiver` trait:
我决定利用trait类型声明。 类型Data和类型Transformer意味着在实现Receiver的结构中,必须同时定义Data和Transformer类型。
Receiver也使用典型的函数定义—在这种情况下,on_emit是触发事件时将调用的函数。 它利用了Self :: Data定义—我们定义的类型之一。 实际上不需要Receiver的Transformer类型,但是稍后需要它告诉Signal期望什么类型:
使用类型声明,这一次添加了绑定到RecType的特征,该特征告诉编译器仅接受实现Receiver的项。 在emit中利用Self :: Data类型声明。
为了以后能够声明receiver,需要指定闭包类型。 最后,disconnect使用一个usize代表disconnect的receiver的ID。
当我第一次决定如何使事件在Rust中工作时,我意识到实现这些trait需要一次又一次地使用相同的样板代码。 因此,我决定最好的方法就是采用宏的方式。
Rust中的宏很棒。 他们将类型安全的,用户定义的令牌模式转换为Rust代码。 作为输入,宏将获取令牌流,该令牌流在宏调用中提供。 定义宏很简单:
更复杂的宏:
为了更加清楚起见,请回顾第一张图片(关于最终声明的外观),并将其与该图片进行比较。 通过此处定义的模式,我们可以使用在原始示例中定义的语法来声明signal。 分解一下:
首先,查看模式中以$开头的任何部分。 可以使用这些变量来表示部分扩展。 查看$ this变量。 这是一个ident类型捕获。 捕获可以是任何有效的标识符(类型,结构,变量或特征名称)。
接下来看一下符号。很难辨别,但要在遇到第一个问题后会看到一个冒号:。使用:来分隔产生的信号实例的名称及其类型,以保留某种期望的声明语法(sans let mut)。
:之后的下一部分是另一个捕获-$ sig。这是另一个实体。用来命名生成的Signal结构的捕获。在$ sig之后是一个尖括号。接下来的两个标识$ rectype和$ data,它们是Signal类型的类型参数。定义信号时,可以使用语法MySignal
。数据类型必须存在于宏之外。确保使用现有的数据类型,因为在扩展中不会为我们创建该参数。也可以使用大多数内置类型-i32,元组或Copy + Clone可以使用的任何结构。
宏的下一部分是使用=封装在方括号中的内容。这只是意味着我们在调用宏时会在类型声明之后出现方括号。如果没有在项目前加上$,则该项目将被解释为模式中应包含的文字字符。括号内的是$(),+中的一些捕获。围绕$(),+表示希望重复捕获封闭的模式,而$ +表示希望以逗号分隔的设置一次或多次。
在重复的捕获中,我们正在寻找$ rec,ident和$ cls expr,以=字符分隔。这样看起来我们正在分配(并且将在宏声明的末尾)。重复捕获中有一种新的捕获类型-expr。它将捕获任何有效的Rust表达式。我们将在扩展的函数指针中封装该闭包。
Rust宏捕获模式很难看懂。 但是不要担心-一旦了解语法和捕获类型,就可以立即编写自己的宏。 因为您可以使用任何喜欢的模式,所以可以轻松地扩展Rust语法或在宏中全部编写特定于域的脚本语言。
现在看看如何使用一些捕获的令牌来做到表示Signal 和Receiver这一点:
将捕获的$ sig标识符用作Signal-implementing结构的名称,将$ rectype标识符用作Receiver-implementing结构的名称。
我们的信号始终需要一种跟踪监听器的方法,因此使用程序生成的$ rectype的Vec。还需要为连接到信号的接收器生成ID,因此给Signal结构指定了一个称为计数器的usize。
在\$ rectype结构中,我们有一个对应的id(使用),用于在信号上使用计数器值。 $ rectype还有一个用于保存函数指针的字段。我们可以再次使用捕获的变量来一般性地设置函数的签名。
可以将适当签名的闭包分配给匹配的函数指针。做到这一点最好的部分是函数指针的大小始终相同,并且已经实现了Copy + Clone,从而使我们在过程中定义的结构也可以实现Copy + Clone。
现在开始实现信号和接收器。首先,在生成的结构上定义一些辅助方法:
我们可以在这里大量使用捕获来使实现通用。 当扩展时,可以在编译时显式键入诸如函数指针cls之类的东西。 为两种类型都提供了一个惯用的新构造函数,并且信号类型得到了一个用于分配接收者ID的增量器。
现在为过程定义的结构实现Signal和Receiver。 从实现接收器开始:
大量使用捕获来定义所需的特征类型。 稍后将需要该Transformer类型使用省略的类型声明来声明闭包。 在on_emit中,调用存储在函数指针末尾的函数。
实现Signal:
通过使用$ data和$ rectype捕获,可以完成我们的类型声明任务。
emit的实现简单地对内部结构的receiver集合进行迭代,并调用每个receiver的on_emit函数。
connect的实现必须使用相当冗长的类型转换(就像我们定义Signal trait时必须做的那样),以澄清编译器可能存在的类型歧义。增加信号计数器并将其返回到变量i中。然后,我们使用提供的i和trns值创建一个新的接收器,并将其推入信号的Vec中。返回接收器r,因此可以将其用作宏扩展的结果。
disconnect使用迭代器的position函数查找相关项目的索引。在这种情况下,通过ID字段来识别匹配项。它将返回一个值,因此在知道ID存在的情况下将其拆开,因为在构造receiver时会分配ID。现在,我们可以在信号接收器的Vec的remove方法中使用此变量idx。
定义宏的格式时,在一些方括号内放置了一个重复的捕获。 这提供了两个功能:
可以定义receiver,就好像它们是在未定义大小的数组中声明的一样(即JavaScript样式)。
可以解开重复捕获的内容,以实际定义传递给宏的重复部分的标识符,因此,我们的标识符不仅仅是占位符-它们是扩展结束时定义的真实标识符。
要将捕获用于重复扩展,使用与定义重复捕获基本相同的语法。
首先,使用$ this标识符作为信号实例的名称来声明信号。 然后使用$()*语法打开重复的扩展。 告诉编译器为每个捕获提供这一行代码,该行定义了一个变量,该变量的名称与当前的$ rec值关联,作为信号的连接接收器。
通过在此处提供$ rectype的类型,借助Transformer关联的类型,可以在闭包中取消类型声明。
最后,展示一下如何通过现在定义一个信号来将任何Copy + Clone结构用作信号的数据类型:
仅使用i32即可完成。 这是connect,emit和disconnect的功能测试的简单证明。 它还显示了如何用原语替换结构,以实现更简单的Signal定义。
现在,可以使用信号轻松定义信号和接收器! 宏和一些即兴的语法。 可以以各种方式关联事件触发。 因为我们的实现使用简单的结构和闭包,所以可以通过将闭包传递给带有签名的Signal :: connect函数来添加新的接收器。
Smart
现在,可以使用信号轻松定义信号和接收器! 宏和一些即兴的语法。 可以以各种方式关联事件触发。 因为我们的实现使用简单的结构和闭包,所以可以通过将闭包传递给带有签名的Signal :: connect函数来添加新的接收器。