范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

fromjstorust系列宏01官网文档19。5高级特性宏译文

  原文链接:The Rust Programming Language
  作者:rust 团队
  译文首发链接:zhuanlan.zhihu.com/p/516660154
  著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。前言
  中间加了一些对于 JavaScript 开发者有帮助的注解。在学习 Rust 的代码时,尤其是有一些经验的开发者,一般都会去看一些相对简单的库,或者项目去学习。Rust 的原生语法本身并不复杂,有一定的 TypeScript 经验的开发者,应该通过看一些教程,都能开始熟悉基本的语法。反而是宏相关内容,虽然就像本文写的,大部分开发者不需要自己去开发宏,但是大家使用宏,和看懂别人代码的主体逻辑,几乎是绕不开宏的。虽然宏在 rust 里属于"高级"内容,但是因为其和 rust 本身语法的正交性,Hugo 认为,反而应该早一些学习。并且,如果一个 JavaScript 开发者之前接触过代码生成器、babel,对这一章的内容反而会比较亲切。
  Derive 宏、attribute 宏特别像 rust 里的装饰器。从作用上,和一般库提供的接口来看,也特别像。所以如果之前有装饰器的经验的开发者,对这一章节应该也会比较亲切。正文
  整本书经常使用 println! 宏,但是还没介绍宏这个机制。宏实际上是 rust 的一系列特性的合集:声明式宏(declarative macro):使用 marco_rules!声明的代码和三种过程式宏(procedural macros):自定义 #[derive] 宏,可以把制定的代码作用在 struct 和 enum 里属性类似的宏,可以把任何属性定义在任何东西上函数类似的宏,看起来像是函数调用,但是是作用在它参数的 tokens 上
  我们一个个来讨论这些内容,但是首先,我们看既然我们已经有了函数,我们为什么需要这些特性。函数和宏的区别
  基本上,宏是指一些代码可以生成另一些代码,这一块的技术一般称为元编程(Hugo 注:代码生成器也属于这一类技术)。在附录 C,我们讨论了 derive 属性,可以帮助你生成一系列的 trait。整本书我们也在用 println! 和 vec! 宏。这些宏在编译时,都会展开成为代码,这样你就不需要手写这些代码。
  元编程可以帮助你减少手写和维护的代码量,当然,函数也能帮助你实现类似的功能。但是,宏有函数没有的威力。
  函数必须声明如惨的数量和种类。而宏,在另一方面,可以接收任意数量的参数:我们可以调用 println!("hello"),也可以调用 println!("hello {}", name)。并且,宏是在编译阶段展开了代码,所以一个宏可以在编译时为一个类型实现一个 trait。一个函数就不可以。因为函数是在运行时调用,而 trait 实现只可以发生在编译时。(Hugo 注:JS 可以实现运行时生成 trait(这里只是套用 rust 的概念),当然,如果你需要的话。动态语言在某些场景能很简单实现非常强大的功能。)
  宏不好的地方在于,宏很复杂,因为你要用 rust 代码写 rust 代码(Hugo 注:任何元编程都不是简单的事儿,包括 JS 里的。)。因为这种间接性,宏的代码要更难读、难理解、难维护。(Hugo 注:个人学 rust,感觉最难不是生命周期,因为生命周期的问题,可以通过用一些库绕过去,或者无脑 clone,如果是应用程序,则可以通过使用 orm 和数据库来绕过很多生命周期的问题。反而是宏,因为稍微有点规模的代码的,都有一大堆宏。宏最难的不是语法,而是作者的意图,因为本质是他造了一套 DSL)
  另一个和函数不一样的地方是,宏需要先定义或者引入作用域,而函数可以在任何地方定义和使用。使用声明式宏 macro_rules! 进行通用元编程
  在 Rust 中使用最广泛的宏是声明式宏。它们有时也被称为 "macros by example"、"macro_rules!宏" 或者就是 "macros"。声明式宏写起来和 Rust 的 match 语法比较像。在第六章里讲到,match 语法是一种流程控制语法,接收一个表达式,然后和结果进行模式匹配,然后执行匹配到结果的代码。宏也会做类似的比较:在这种情况下,传入的参数是 rust 的合法的语法代码,然后通过宏的规则,和书写好的模版,在编译时转换成代码。
  定义声明式宏的语法是 macro_rule!。下面我来用 vec! 来介绍这一机制。第八章有关于 vec! 的内容。例如,创建一个新的 vector,包含 3 个 integer:#![allow(unused)] fn main() { let v: Vec = vec![1, 2, 3]; }
  我们可以通过 vec! 宏来创建任意类型的 vector,例如 2 个 integer,或者五个 string slice.
  我们不能用函数去实现这个功能,因为我们不知道输入的参数个数。(Hugo 注:这一点和 JS 非常不一样,我们写 JS,已经习惯了可以传任意变量。当然如果你熟悉 TS,和 TS 有一些类似。Rust 虽然也有范型,但是和 TS 的范型非常不一样。这里不一样,我主要指关注点,因为 Rust 比较大一部分都是函数式的代码,每个函数一般都承载非常细粒度的功能,一般每个函数都处理好了自己的输入、输出、报错,所有可能性都写好了,写 rust 有一种在填状态机的错觉…TS 有的代码也有这种感觉。)
  一个简化的 vec! 宏:#[macro_export] macro_rules! vec {     ( $( $x:expr ),* ) => {         {             let mut temp_vec = Vec::new();             $(                 temp_vec.push($x);             )*             temp_vec         }     }; }
  注意:实际的 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。
  现在我们看一下和这个边匹配的生成代码的部分: {         {             let mut temp_vec = Vec::new();             $(                 temp_vec.push($x);             )*             temp_vec         }     };
  在 ()里的tempvec.push(() 里的 temp_vec.push(()里的tempvec.push(x); 就是生成的代码的部分。* 号仍然表示生成零个和多个,这个匹配的具体个数,要看匹配条件命中的个数。
  当我们调用:vec![1, 2, 3]; 时,生成了这个代码。(Hugo 注:Cargo 有 expand 插件,对于声明宏,多看看展开基本就能学会了。){     let mut temp_vec = Vec::new();     temp_vec.push(1);     temp_vec.push(2);     temp_vec.push(3);     temp_vec }
  你传任意参数,最后就生成符合上面条件的代码。
  有一些 macro_rules! 的奇怪的边界例子。在未来,Rust 会有第二种声明式宏,和现在的机制类似,但是会解决这些边界问题。在那一次升级后,macro_rules! 会被弃用。(Hugo 注:Rust 仍然是非常年轻的语言,做好随时接受改变的准备)记住这些,当然另一个事实是,大部分 Rust 程序员更多是宏的使用者,而不是开发者,我们不会在深入讨论 macro_rules!。如果你对这块特别感兴趣。请阅读《"The Little Book of Rust Macros"》。(Hugo 注:站在入门的角度,能知道机制去使用就 ok 了。在绝大部分入门的情况下,函数以及使用 crates.io 上的宏都能满足你的需求。)从属性生成代码的过程宏
  第二种宏是过程宏,表现形式更像函数(过程的一种类型)。过程宏的入参是一些代码,你可以操作这些代码,然后衬衫一些代码。(Hugo:从结果看和声明宏没区别,其实站在 JS 的角度,更像是 babel,你可以根据输入的 token 做变换)
  虽然过程宏有三种:custom derive、attribute-like 和 function-like,但是原理都是一样的。
  如果要创建过程宏,定义的部分需要在自己的 crate 里,并且要定义特殊的 crate 类型。(Hugo 注:相当于定义了一个 babel 插件,只不过有一套 rust 自己的体系。这些宏会在编译的时候,按照书写的规则,转成对应的代码。所有的宏,都是代码生成的手段,输入是代码,输入是代码。)这种设计,我们有可能会在未来消除。
  下面是一个过程宏的例子:use proc_macro;  #[some_attribute] pub fn some_name(input: TokenStream) -> TokenStream { }
  过程宏接收一个 TokenStream,输出一个 TokenStream。TokenStream 类型定义在 proc_macro 里,表示一系列的 tokens。这个就是这种宏的核心机制,输入的代码(会被 rust) 转成 TokenStream,然后做一些按照业务逻辑的操作,最后生成 TokenStream。这个函数也可以叠加其他的属性宏(#[some_attribute], 看起来像装饰器的逻辑,也可以理解为一种链式调用),可以在一个 crate 里定义多个过程。(Hugo 注:搞过 babel 的同学肯定很熟悉,一样的味道。没搞过的同学,强烈建议先学学 babel。)
  下面我们来看看不同类型的过程宏。首先从自定义 derive 宏开始,然后我们介绍这种宏和其他几种的区别。如何编写自定义 derive 宏
  我们创建一个 crate 名字叫 hello_macro,定义一个 HelloMacro 的 trait,关联的函数名字叫 hello_macro。通过使用这个宏,用户的结构可以直接获得默认定义的 hello_macro 函数,而不需要实现这个 trait。默认的 hello_macro 可以打印 Hello, Macro! My name is TypeName!,其中 TypeName 是实现这个 derive 宏的结构的类型名称。use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro;  #[derive(HelloMacro)] struct Pancakes;  fn main() {     Pancakes::hello_macro(); }
  创建这个宏的过程如下,首先$ cargo new hello_macro --lib
  然后定义 HelloMacro traitpub trait HelloMacro {     fn hello_macro(); }
  这样我们就有了一个 trait,和这个triat 的函数。用户可以通过这个 trait 直接实现对应的函数。use hello_macro::HelloMacro;  struct Pancakes;  impl HelloMacro for Pancakes {     fn hello_macro() {         println!("Hello, Macro! My name is Pancakes!");     } }  fn main() {     Pancakes::hello_macro(); }
  但是,用户需要每次都实现一遍 hello_macro。如果 hello_macro 的实现都差不多,就可以通过 derive 宏来是实现。
  因为 Rust 没有反射机制,我们不可以在执行时知道对应类型的名字。我们需要在编译时生成对应的代码。
  下一步,定义过程宏。在这个文章编写时,过程宏需要在自己的 crates 里。最终,这个设计可能改变。关于 宏 crate 的约定是:对于一个名为 foo 的 crate,自定义 drive 宏的crate 名字为 foo_derive。我们在 hello_macro 项目中创建 hello_macro_derive crate。$ cargo new hello_macro_derive --lib
  我们的两个的 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] proc-macro = true  [dependencies] syn = "1.0" quote = "1.0"
  在 lib.rs 里添加下述代码。注意,这个代码如果不增加 impl_hello_macro 的实现是通不过编译的。use proc_macro::TokenStream; use quote::quote; use syn;  #[proc_macro_derive(HelloMacro)] pub fn hello_macro_derive(input: TokenStream) -> TokenStream {     // Construct a representation of Rust code as a syntax tree     // that we can manipulate     let ast = syn::parse(input).unwrap();      // Build the trait implementation     impl_hello_macro(&ast) }
  注意,这里把代码分散成两部分,一部分在 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。DeriveInput {     // --snip--      ident: Ident {         ident: "Pancakes",         span: #0 bytes(95..103)     },     data: Struct(         DataStruct {             struct_token: Struct,             fields: Unit,             semi_token: Some(                 Semi             )         }     ) }
  上述这个结构的意思是:正在处理的是 ident(identifier, 意味着名字)为 Pancakes 的 unit struct。其他的字段表示其余的 Rust 代码。如果想了解更详细的内容,请参考。
  接下来,我们就要开始定义 impl_hello_macro。这个函数实现了添加到 Rust 代码上的函数。在我们做之前,注意 derive macro 的输出也是 TokenStream。返回的 TokenStream 就是添加完代码以后的代码。当编译 crate 时,最终的代码,就是处理完成的代码了。
  你也许也会发现,这里调用 syn::parse 时使用了 unwrap,如果报错就中断。这里必须这么做,因为最终返回的是 TokenStream,而不是 Result。这里是为了简化代码说明这个问题。在生产代码,你应该处理好报错,提供更详细的报错信息,例如使用 panic! 或者 expect。
  下面是代码:fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {     // 通过&ast.ident 获取类型的名字     let name = &ast.ident;     // 使用 quote 宏,可以使用 rust 语法来定义要实现的 trait(Hugo 注:JS 要有这个就好了)     let gen = quote! {         // #name 是 quote! 的模版语法,会自动替换为这个变量里的值         impl HelloMacro for #name {             fn hello_macro() {                 // 这里把对应的 struct 名字转换好了,stringfy!把值转换为字符串                 println!("Hello, Macro! My name is {}!", stringify!(#name));             }         }     };    // 转换为最终的 TokenStream     gen.into() }
  通过上面的代码,cargo build 就可以正常工作了。如果要使用这个代码,需要把两个依赖都加上。hello_macro = { path = "../hello_macro" } hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
  现在执行下面的代码,就可以看到 Hello, Macro! My name is Pancakes!use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro;  #[derive(HelloMacro)] struct Pancakes;  fn main() {     Pancakes::hello_macro(); }
  下一步,我们来探索其他类型的过程宏。属性宏(Attribute-like)
  属性宏和 derive 宏类似,但是可以创造除了 derive 意外的属性。derive 只能作用于 structs 和 enums,属性宏可以作用于其他的东西,比如函数。下面是一个属性宏的例子:例如你制作了一个名为 route 的属性宏来在一个web 框架中标注函数。#[route(GET, "/")] fn index() {
  #[route] 是框架定义的过程宏。定义这个宏的函数类似:#[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
  这里,有两个参数,类型都是 TokenStream。第一个是属性的内容,GET, "/" 部分,第二个是标注属性宏传入的语法部分,在这个例子里,就是剩下的 fn index() {}。
  工作原理和 derive 宏是一样的。函数宏(Function-like)
  函数宏的使用比较像调用一个 rust 函数。函数宏有点像 macro_rules! ,能提供比函数更高的灵活性。例如,可以接受未知个数的参数。但是,macro_rules! 只能使用在上述章节的匹配型的语法。而函数宏接受 TokenStream 参数作为入参,和其他过程宏一样,可以做任何变换,然后返回 TokenStream。下面是一个函数宏 sql!let sql = sql!(SELECT * FROM posts WHERE id=1);
  这个宏接受 SQL 语句,可以检查这个 SQL 的语法是否正确,这种功能比 macro_rules! 提供的要复杂的多。这个 sql! 的宏可以定义为:#[proc_macro] pub fn sql(input: TokenStream) -> TokenStream {
  这个定义和自定义 derive 宏类似:接受括号内的 tokens,返回生成的代码。总结
  好了,现在你有了一些可能不常用的 Rust 新工具,但是你要知道的是,在需要的场合,他们的运行原理是什么。我们介绍了一些复杂的话题,当你在错误处理或者别人的代码里看到这些宏时,可以认出这些概念和语法。可以使用这一章的内容作为解决这些问题的索引。

湖北5亿!智慧物流科技产业园备案招标备案号22044206508905494432项目名称智慧物流科技产业园项目所在地高新工业园项目总投资50000。0万元项目规模及内容计划将智慧物流融入智慧城市,以信息化智能化美盛文化,新华联,东华科技,天保基建,贵研铂业,高新兴立昂技术是大数据概念。招商银行是银行概念。高新兴是软件开发概念。泰禾集团是住宅开发概念。天保基建是住宅开发概念。北汽蓝谷是新能源车概念。贵研铂业是小金属概念。美盛文化是虚拟现实概念新能源车产业链暗藏问题寻求突破编者按新能源汽车产业的大规模超速发展,对产业链整体发展提出了更高要求。由于汽车产业链条长产值大,产业链上的每一个环节出现问题都会影响快速崛起的新能源汽车产业。安全体系建设原材料涨价数字化时代惊叹的文化内容创造在数字产业化和产业数字化的宏观政策推动下,移动互联网云计算物联网云计算人工智能等一系列信息技术从多个角度重塑着我们生活的方方面面,引发各行各业的蝶变与重生,我们正处于数字经济大时代山东人脸识别系统告诉您该如何选择人脸识别门禁系统1抗光线干扰能力一款好的人脸识别门禁系统除了系统稳定性外,还保证在强逆光弱光黑夜雨雾天能正常使用,若是室外应用,那么产品就要具备在逆光光线不足的情况依旧能够准确识别的能力,这就要求利用车辆识别技术建设可视化智能化停车场综合管理系统传统停车场存在进出场效率低找车位难找车难管理难管理成本高等诸多问题,严重制约了城市交通的现代化发展。便捷安全高效管理科学的停车场综合管理系统成为当下停车建设的新需求,可视化智能化的锤子系统宣布回归或将推出一款智慧屏新品Tech星球4月15日消息,SmartisanOS团队今日在社交平台发文称,朋友们,好久不见,宣布即将推出一款新品。官方表示,虽然暂时告别手机江湖,但并没有停止对Smartisan欧拉操作系统已在电信金融等行业规模应用中新社北京4月15日电(记者刘育英)记者15日从欧拉开发者大会获悉,目前,欧拉在政府运营商金融能源交通互联网等行业已规模应用,累计超过130万套。2019年,华为把自己在服务器操作苹果公司MagSafe可能变成无线数据传输系统4月15日上午消息(李文朋)苹果公司的MagSafe目前只是无线磁力充电装置,但在未来,它可能才是无线数据传输的关键。苹果公司一直在研究如何让MagSafe像现在的Lightnin小米新MIUIGO系统曝光,支持4GB以下内存手机,这款机型率先搭载智能手机能够流畅运行的关键因素有两个,一是强悍的硬件性能,二是体验良好的手机操作系统。目前国内的智能手机在核心硬件上大多都是采用外购的模式进行组装,然后再搭配自己深度定制开发的操作神操作我的支付宝终于清静了不知道大家的支付宝主页,是不是都是这样的咧?讲真,作为极简主义的技巧酱我呢,是不容许自己的App里,有这么花里胡哨的内容的。只是我一直都误以为,这些功能都是强制加上,不能关闭的。终
为什么i3的cpu基础频率最高,达到4。0了?i39350kf是英特尔新出的不带核显的型号,从频率和性能上来看是之前i39100的加强版,9350kf的基础频率提升了400mhz,最大加速频率提高了400mhz,最高单核频率达请问,我用OCAM录制游戏,为什么录制不了,只有声音无画面?OCAM体积小巧而且下载了直接可以用不需要安装,一般的界面操作和视频录制还是可以的,但OCAM并不适合录制游戏。因而经常会出现只有声音没有画面的情况。实际上视频录制的软件还有很多,你手机中最厉害的软件是什么?网易云音乐我觉得我手机里最好的软件就是网易云音乐了。用过酷狗,qq音乐,酷我音乐,虾米音乐,没用多久都卸载了。直到网易云音乐,从此我都没有换过播放器了。一次又一次,从私人fm听到自为什么网上没有人敢说喜欢苹果手机而苹果的销量却疯涨?说喜欢华为是工作,用苹果手机是生活!骂美国是工作,移民美国是生活!品质说话,到底哪个好自己骗不了自己,销量上还是苹果排名靠前啊,群众的眼睛是雪亮的谁说的不敢说喜欢苹果手机?我敢说喜手机省电模式真的有用吗?问这个问题可能是因为题主不明白自己手机省电模式的运行原理,我这里详细解释一下。观点手机省电模式肯定有用手机中是什么在耗电硬件是的,你没有看错,手机软件并不会直接耗电,所有耗电行为都如何评价AMD推出的3900X处理器?3900X是AMD锐龙系列第三代的产品,是2019年AMD发布的一款主流旗舰处理器。虽然今年已经是2020年,但作为一款高性能CPU依然并没落后。3900XCPU采用7nm工艺打造有没有6000左右剪视频的笔记本推荐呀?近期入手?如果是剪辑视频的话就不建议轻薄本了,毕竟显卡方面还是要略差一些,这个或多或少还是会影响到一些性能,而且现在游戏本也开始轻薄化了,内存也要最少16G才好。机械革命蛟龙Z3(6298。手机性能就维持三年吗?有多少用三年就不得不换手机的?您好,现在只能手机一年一换,两年一换都很正常。因为现在智能手机除了硬件性能,更多还是体现后续服务上面。简单举例,单说CPU这一块,高通835是2016年发布的芯片了,从性能上来说是为什么坐飞机安检时,笔记本电脑需要单独拿出来,而iPad却不需要(直接放在了书包里)?电子设备包括ipad是需要拿出来的。至于笔记本除了为了避免扫描的干扰,其次是确定电池是插在笔记本上而不是拿下来的。你这是哪个机场,管得这么松,我去年从首都国际机场出发来非洲,两台电为什么手机淘宝比手机游戏还占运行内存?手机淘宝并不是单纯的购物模块,里面包含了聊天模块,支付模块,资料处理模块,新闻!PGCVIDEOthumbheight720,vidv02016180000brhg3jr82vub有哪些让你看一眼就爱上的手机壁纸?太多,只能放9张,喜欢的朋友可以移步一人一图一世界,有很多好看的壁纸头像我喜欢绿色壁纸!因为绿色壁纸养眼的!绿色壁纸给人一种清新!充实向往的感觉!绿色属于大自然!发上两张请朋友们欣