A. [from js to rust 系列][宏-01][官网文档 19.5]高级特性:宏[译文]
原文链接:The Rust Programming Language
作者:rust 团队
译文首发链接:zhuanlan.hu.com/p/516660154
着作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
中间加了一些对于 javaScript 开发者有帮助的注解。在学习 Rust 的代码时,尤其是有一些经验的开发者,一般都会去看一些相对简单的库,或者项目去学习。Rust 的原生语法本身并不复杂,有一定的 TypeScript 经验的开发者,应该通过看一些教程,都能开始熟悉基本的语法。反而是宏相关内容,虽然就像本文写的,大部分开发者不需要自己去开发宏,但是大家使用宏,和看懂别人代码的主体逻辑,几乎是绕不开宏的。虽然宏在 rust 里属于“高级”内容,但是因为其和 rust 本身语法的正交性,Hugo 认为,反而应该早一些学习。并且,如果一个 JavaScript 开发者之前接触过代码生成器、babel,对这一章的内容反而会比较亲切。
Derive 宏、attribute 宏特别像 rust 里的装饰器。从作用上,和一般库提供的接口来看,也特别像。所以如果之前有装饰器的经验的开发者,对这一章节应该也会比较亲切。
我们一个个来讨论这些内容,但是首先,我们看既然我们已经有了函数,我们为什么需要这些特性。
基本上,宏是指一些代码可以生成另一些代码,这一块的技术一般称为元编程(Hugo 注:代码生成器也属于这一类技术)。在附贺颤录 C,我们讨论了 derive 属性,可以帮助你生成一系列的 trait。整本书我们也在用 println! 和 vec! 宏。这些宏在编译时,都会展开成为代码,这样你雹拍洞就不需要手写这些代码。
元编程可以帮助你减少手写和维护的代码量,当然,函数也能帮助你实现类似的功能。但是,宏有函数没有的威力。
宏不好的地方在于,宏很复杂,因为你要用 rust 代码写 rust 代码(Hugo 注:任何元编程都不是简单的事儿,包括 JS 里的。)。因为这种间接性,宏的代码要更难读、难理解、难维护。(Hugo 注:个人学 rust,感觉最难不是生命周期,因为生命周期的问题,可以通过用一些库绕过去,或者无脑 clone,如果是应用程序,则可以通过使用 orm 和数据库来绕过很多生命周期的问题。反而是宏,因为稍微有点规模的代码的,都有一大堆宏。宏最难的不是语法,而是作者的意图,因为本质是他造了一套 DSL)
另一个和函数不一样的地方是,宏需要先定义或者引入作用域,而函数可以在任何地方定义和使用。
我们可以通过 vec! 宏来创建任意类型的 vector,例如 2 个 integer,或者五个 string slice.
一个简化的 vec! 宏:
#[macro_export] 标注指明了这个宏在 crate 的作用域里可用。没有这个标注,宏不会被带入到作用域里。
macro_rules! 后面就是宏的名字。这里只有一种模式匹配的边(arm):( (( (x:expr ),* ) ,=> 后面是这个模式对应要生成的代码。如果这个模式匹配成功,对应的代码和输入的参数组成的代码就会生成在最终的代码中。因为这里只有一种边,所以只有这一种可以匹配的条件。不符合这个条件的输入,都会报错。一般复杂的宏,都会有多个边。
这里匹配的规则和 match 是不一样的,因为这里的语法匹配的是 rust 的语法,而不是 rust 的类型,或者值。更全的宏匹配语法,见文档。
对于 宏的输入条件 ( (( (x:expr ),* ),()内部是匹配的语法,expr表示所有Rust的表达式。() 内部是匹配的语法,expr 表示所有 Rust 的表达式。()内部是匹配的语法,expr表示所有Rust的表达式。() 后面的都喊表示这个变量后面有可能有逗号,* 表示前面的模式会出现一次或者多次。(Hugo 注:像不像正则?源枯宏语法其实挺简单的,不要被高级唬住了。当然,宏还是难的,宏要考虑的问题本身是一个复杂的问题。)
当我们调用:vec![1, 2, 3]; 时,$x 模式会匹配 3 个表达式 1 , 2 和 3。
现在我们看一下和这个边匹配的生成代码的部分:
在 ()里的tempvec.push(() 里的 temp_vec.push(()里的tempvec.push(x); 就是生成的代码的部分。* 号仍然表示生成零个和多个,这个匹配的具体个数,要看匹配条件命中的个数。
你传任意参数,最后就生成符合上面条件的代码。
虽然过程宏有三种:custom derive、attribute-like 和 function-like,但是原理都是一样的。
如果要创建过程宏,定义的部分需要在自己的 crate 里,并且要定义特殊的 crate 类型。(Hugo 注:相当于定义了一个 babel 插件,只不过有一套 rust 自己的体系。这些宏会在编译的时候,按照书写的规则,转成对应的代码。所有的宏,都是代码生成的手段,输入是代码,输入是代码。)这种设计,我们有可能会在未来消除。
下面是一个过程宏的例子:
过程宏接收一个 TokenStream,输出一个 TokenStream。TokenStream 类型定义在 proc_macro 里,表示一系列的 tokens。这个就是这种宏的核心机制,输入的代码(会被 rust) 转成 TokenStream,然后做一些按照业务逻辑的操作,最后生成 TokenStream。这个函数也可以叠加其他的属性宏(#[some_attribute], 看起来像装饰器的逻辑,也可以理解为一种链式调用),可以在一个 crate 里定义多个过程。(Hugo 注:搞过 babel 的同学肯定很熟悉,一样的味道。没搞过的同学,强烈建议先学学 babel。)
下面我们来看看不同类型的过程宏。首先从自定义 derive 宏开始,然后我们介绍这种宏和其他几种的区别。
我们创建一个 crate 名字叫 hello_macro,定义一个 HelloMacro 的 trait,关联的函数名字叫 hello_macro。通过使用这个宏,用户的结构可以直接获得默认定义的 hello_macro 函数,而不需要实现这个 trait。默认的 hello_macro 可以打印 Hello, Macro! My name is TypeName!,其中 TypeName 是实现这个 derive 宏的结构的类型名称。
创建这个宏的过程如下,首先
然后定义 HelloMacro trait
这样我们就有了一个 trait,和这个triat 的函数。用户可以通过这个 trait 直接实现对应的函数。
但是,用户需要每次都实现一遍 hello_macro。如果 hello_macro 的实现都差不多,就可以通过 derive 宏来是实现。
因为 Rust 没有反射机制,我们不可以在执行时知道对应类型的名字。我们需要在编译时生成对应的代码。
下一步,定义过程宏。在这个文章编写时,过程宏需要在自己的 crates 里。最终,这个设计可能改变。关于 宏 crate 的约定是:对于一个名为 foo 的 crate,自定义 drive 宏的crate 名字为 foo_derive。我们在 hello_macro 项目中创建 hello_macro_derive crate。
我们的两个的 crate 关联紧密,所以我们在 hello_macro crate 里创建这个 crate。如果我们要改变 hello_macro 的定义,我们同样也要更改 hello_macro_derive 的定义。这两个 crates 要隔离发布。当用户使用时,要同时添加这两个依赖。为了简化依赖,我们可以让 hello_macro 使用 hello_macro_derive 作为依赖,然后导出这个依赖。但是,这样,如果用户不想使用 hello_macro_derive,也会自动添加上这个依赖。
下面开始创建 hello_macro_derive,作为一个过程宏 crate。需要添加依赖 syn 和 quote。下面是这个 crate 的 Cargo.toml。
在 lib.rs 里添加下述代码。注意,这个代码如果不增加 impl_hello_macro 的实现是通不过编译的。
注意,这里把代码分散成两部分,一部分在 hello_macro_derive 函数里,这个函数主要负责处理 TokenStream,另一部分在 impl_hello_macro,这里负责转换语法树:这样编写过程宏可以简单一些。在绝大部分过程宏立,对于前者的过程一般都是一样的。一般来说,真正的区别在 impl_hello_macro,这里的逻辑一般是一个过程宏的业务决定的。
我们引入了三个 crates: proc_macro, syn 和 quote。proc_macro 内置在 rust 立,不需要在 Cargo.toml 中引入。proc_macro 实际是 rust 编译器的一个接口,用来读取和操作 Rust 代码。
syn crate 把 Rust 代码从字符串转换为可以操作的结构体。quote crate 把 syn 数据在转回 Rust 代码。这些 Crate 可以极大简化过程宏的编写:写一个 Rust 代码的 full parser 可不是容易的事儿!
当在一个类型上标注 [derive(HelloMacro)] 时,会调用 hello_macro_derive 函数。之所以会有这样的行为,是因为在定义 hello_macro_derive 时,标注了 #[proc_macro_derive(HelloMacro)] 在函数前面。
hello_macro_derive 会把输入从 TokenStream 转换为一个我们可以操作的数据结构。这就是为什么需要引入 syn 。sync 的 parse 函数会把 TokenStream 转换为 DeriveInput。
上述这个结构的意思是:正在处理的是 ident(identifier, 意味着名字)为 Pancakes 的 unit struct。其他的字段表示其余的 Rust 代码。如果想了解更详细的内容,请参考。
接下来,我们就要开始定义 impl_hello_macro。这个函数实现了添加到 Rust 代码上的函数。在我们做之前,注意 derive macro 的输出也是 TokenStream。返回的 TokenStream 就是添加完代码以后的代码。当编译 crate 时,最终的代码,就是处理完成的代码了。
你也许也会发现,这里调用 syn::parse 时使用了 unwrap,如果报错就中断。这里必须这么做,因为最终返回的是 TokenStream,而不是 Result。这里是为了简化代码说明这个问题。在生产代码,你应该处理好报错,提供更详细的报错信息,例如使用 panic! 或者 expect。
下面是代码:
通过上面的代码,cargo build 就可以正常工作了。如果要使用这个代码,需要把两个依赖都加上。
现在执行下面的代码,就可以看到 Hello, Macro! My name is Pancakes!
下一步,我们来 探索 其他类型的过程宏。
属性宏和 derive 宏类似,但是可以创造除了 derive 意外的属性。derive 只能作用于 structs 和 enums,属性宏可以作用于其他的东西,比如函数。下面是一个属性宏的例子:例如你制作了一个名为 route 的属性宏来在一个web 框架中标注函数。
#[route] 是框架定义的过程宏。定义这个宏的函数类似:
这里,有两个参数,类型都是 TokenStream。第一个是属性的内容,GET, "/" 部分,第二个是标注属性宏传入的语法部分,在这个例子里,就是剩下的 fn index() {}。
工作原理和 derive 宏是一样的。
函数宏的使用比较像调用一个 rust 函数。函数宏有点像 macro_rules! ,能提供比函数更高的灵活性。例如,可以接受未知个数的参数。但是,macro_rules! 只能使用在上述章节的匹配型的语法。而函数宏接受 TokenStream 参数作为入参,和其他过程宏一样,可以做任何变换,然后返回 TokenStream。下面是一个函数宏 sql!
这个宏接受 SQL 语句,可以检查这个 SQL 的语法是否正确,这种功能比 macro_rules! 提供的要复杂的多。这个 sql! 的宏可以定义为:
这个定义和自定义 derive 宏类似:接受括号内的 tokens,返回生成的代码。
好了,现在你有了一些可能不常用的 Rust 新工具,但是你要知道的是,在需要的场合,他们的运行原理是什么。我们介绍了一些复杂的话题,当你在错误处理或者别人的代码里看到这些宏时,可以认出这些概念和语法。可以使用这一章的内容作为解决这些问题的索引。
B. rust可以代替javascript吗
不会取代的。javascript是一种页面脚本,通过执行程序脚本片段,我们可以对页面及页面上的元素进行操作,实现特定的功能与效果。
而Rust是Mozilla开发的注重安全、性能和并发性的编程语言。
创建这个新语言的目的是为了解决一个顽疾:软件的演进速度大大低于硬件的演进,软件在语言级别上无法真正利用多核计算带来的性能提升。Rust是针对多核体系提出的语言,并且吸收一些其他动态语言的重要特性,比如不需要管理内存,比如不会出现Null指针等等。
rust将来有可能取代c或者c++,但是无法撼动javascript在业界的地位。
C. 深入浅出Rust(第三部分-1)
传送门:
深数芹入浅出Rust(第一部分-1)
深入浅出Rust(第一部分-2)
深入浅出Rust(第二部分-1)
深入浅出Rust(第二部分-2)
深入浅出Rust(第三部分-1)
深入浅出Rust(第三部分-2)
深入浅出Rust(第四部分)
深入浅出Rust(第五部分)
看了引入泛型,就要考虑的方方面面,怪不得饥咐Go迟迟拿不出方案了...
Rust的泛型和java的不同,java只是在编译器进行检查,运行是进行类型擦除.而Rust是在编译器时进行检查和类型绑定.
阅读下来,语义上闭包和js的闭包区别不大,语法小薯肢毕有区别
语法:
简写:
省略类型,{},return,这些和java,js倒是一样的.
D. 减少rust编译后程序体积
第一步:
编敬祥译release版本
第二步:
strip 命令
扩展
整优化等级
通过修改默认优化亮液搏等级方式减少体积,以cpu换空间,如果不是必要,建议不要改
在Cargo.toml中新增下面配置
开启 LTO
减少体积,增加链接时埋孙间也是一个取舍问题
在Cargo.toml中新增下面配置
E. Electron替代方案,rust跨平台GUI框架TAURI之hello world
tauri 是一个新兴的配明判跨平台GUI框架。与electron的基本思想相似,tauri的前端实现也是基于html系列语言。tauri的后端使用rust。官方形容,tauri可以创建体积更小、运行更快、更加安全的跨平台桌面应用。
详细的介绍可以自行去官网查看:
官网
Github
本人使用windows10系统。本hello world,实现了以tauri搭建桌面程序,在html页面点击按键后,槐陵由后台rust反馈信息。
效果如下:
tauri 需要用到rust、nodejs,编译器可使用vscode
官方文档有比较详培改细的环境搭建步骤,可参阅:
https://tauri.studio/docs/getting-started/intro
其中,当搭建完环境,使用命令
yarn add tauri
安装tauri包时,可能会出现报错:
pngquant failed to build, make sure that libpng-dev is installed
此错误并不影响使用,可忽略。
初始化完成的tauri程序结构如上图所示。默认情况下dist菜单用于存放实际的页面文件。具体可在tauri.conf.json文件中进行设置。
具体实现步骤如下:
F. rust与js的语法
上有较大的不同,瞎搜rust遵循静态类型,代码执行过程磨散历中间没有变量的掘圆类型改变,并且rust的语法机制更加严格,更注重于类型的正确推断,而javascript则更加灵活,运行时根据赋值情况会更改变量类型。
G. 对比 Go 语言,Rust 有什么优势和劣势
我并没有什么编程的经验,觉得编程实在是太复杂了,不喜欢去研究太多,对这个也不怎么懂,只能说自己是个半吊子,就是所掌握的知识,也是东拼西凑的,朋友和我说点儿,自己去书上看一点儿,只能说根据自己的体验给出一些体会吧。
其实我觉得什么代码啊编程啊这些东西还是比较适合理工的学生去研究,我一看脑袋就大,完全不明白在讲什么。我大概了解的就是这些,语言的话大家可以多方面的去了解,也不是说有缺点就是不好,看配置看个人吧,每个人习惯不一样,也许有的人用不稳定的还觉得挺好呢,有的人就喜欢比较完美的,在我看来编程这个东西真的是很复杂,会有很多的代码,这些代码弄得我自己头都大了,有的时候还得去恶补一下。
H. 对比 Go 语言,Rust 有什么优势和劣势
Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对陵肆物多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。
Rust是Mozilla开发的注重安全、性能和并发性雹悉的编程语言。"Rust",由web语言的领军人物Brendan Eich(js之父),Dave Herman以及Mozilla公司的Graydon Hoare 合力开发。Rust是针对多核体系提出的语言,并且吸收一些其他动态语言的重要特性,比如不需要管理内存,比如不会出现Null指针等等。
不管是GO语言还是ruts都是各有各的长处,各有各的缺点的,每个都有自己存在的意义和用处,可以互不打扰的,选择适合自己的语言去使用,让他发挥到自己的用处才是他所存在的意义,也不能太过于可以的去比较他们之间互相的好与坏。
I. rust条件编译
比如
自定义条件的属性标注方式如下:
target_os等rustc已经支持的条件可以隐式判断进行编译,而自定义陆指条件,需要在编译需要早判配显式指定编译条件。
以上编译方式会报错,因为直接执行冲指编译找不到条件下的conditional_function方法
需要使用以下方法编译:
直接执行cargo build会报错找不到方法
正确方法应该使用以下方式编译:
J. rust析构函数没调用
首先,我们设计一个Node类型,它里面包含一个指针,可以指向其它的Node实例:
struct Node { next : Box<Node> }
下面我们尝试一下创建两个实例,将它们首尾相连:
fn main() { let node1 = Node { next : Box::new(...) } }
额,这里写不下去了,Rust中要求,Box指针必须被合理初始化,而初始化Box的时候又必须先传入一个Node的实例,这个Node的实例又要求创闹肢租建一个Box指针。成液兆了“鸡生蛋蛋生鸡”的无限循环。
要打破这个循环,我们需要使用“可空的指针”。在初始化Node的时候,指针应该是“空”状态,后面再把它们连接起来。我们把代码改进成以下这样,为了能修改node的值,还需要使用mut:
struct Node { next : Option<Box<Node>> } fn main() { let mut node1 = Box::new (Node { next : None }); let mut node2 = Box::new (Node { next : None }); node1.next = Some(node2); node2.next = Some(node1); }
编译,发生错误:“error: use of moved value: node2”。
从编译信息中可以看到,在node1.next = Some(node2);这条语句中,发生了move语义,从此句往后,node2变量的生命周期已经结束了。因此后面一句中使用node2的时候发生了错误。那我们需要继续改进,不使用node2,换而使用node1.next,代码改成这样:
fn main() { let mut node1 = Box::new (Node { next : None }); let mut node2 = Box::new (Node { next : None }); node1.next = Some(node2); match node1.next { Some(mut n) => n.next = Some(node1), None => {} } }
编译,又发生了错误,错误信息为:“error: use of partially moved value: node1”。
这是因为在match语句中,我们把node1.next的所有权转移到了局部变量n中,这个n实际上就是node2的实例,在执行赋值操作n.next = Some(node1)的过程中,编译器认为此时node1的一部分已经被转移出去了,它不能再被用于赋值号的右边。
看来,这是因为我们选择使用的指针类型不对,Box类型的指针对所管理的内存拥有所有权,只使用Box指针没有办法构造一个循环引用的结构出来。于是,我们想到,使用Rc指针。同时,我们还用了Drop trait,来验证这个对象是否真正被释放了。
use std::rc::Rc; struct Node { next : Option<Rc<Node>> } impl Drop for Node { fn drop(&mut self) { println!("drop"); } } fn main() { let mut node1 = Node { next : None }; let mut node2 = Node { next : None }; let mut node3 = Node { next : None }; node1.next = Some(Rc::new(node2)); node2.next = Some(Rc::new(node3)); node3.next = Some(Rc::new(node1)); }
编饥信译依然没有通过,错误信息为:“error: partial reinitialization of uninitialized structure node2”。还是没有达到我们的目的,继续改进,我们将原先“栈”上分配内存改为在“堆”上分配内存:
use std::rc::Rc; struct Node { next : Option<Rc<Node>> } impl Drop for Node { fn drop(&mut self) { println!("drop"); } } fn main() { let mut node1 = Rc::new(Node { next : None }); let mut node2 = Rc::new(Node { next : None }); let mut node3 = Rc::new(Node { next : None }); node1.next = Some(node2); node2.next = Some(node3); node3.next = Some(node1); }
编译,再次不通过,错误信息为:“error: cannot assign to immutable field”。通过这个错误信息,我们现在应该能想到,Rc类型包含的数据是不可变的,通过Rc指针访问内部数据并做修改是不行的,必须用上RefCell把它们包裹起来才可以。继续修改:
use std::rc::Rc; use std::cell::RefCell; struct Node { next : Option<Rc<RefCell<Node>>> } impl Node { fn new() -> Node { Node { next : None} } } impl Drop for Node { fn drop(&mut self) { println!("drop"); } } fn alloc_objects() { let node1 = Rc::new(RefCell::new(Node::new())); let node2 = Rc::new(RefCell::new(Node::new())); let node3 = Rc::new(RefCell::new(Node::new())); node1.borrow_mut().next = Some(node2.clone()); node2.borrow_mut().next = Some(node3.clone()); node3.borrow_mut().next = Some(node1.clone()); } fn main() { alloc_objects(); println!("program finished."); }