aot数字货币是真是假,aot数字平台

  

     

  

  来自实践的真知,迅速获得澄清概念,解释错误的数据竞赛条件,建立树形结构,渲染树形结构,总结https://github.com/qianyan/tree/blob/master/tree.rsTL;DR   

  

  下面是我对内部分享这个话题的介绍。   

  

  Rust是系统编程语言,也是很多区块链的底层编程语言,不管是老奇偶还是新天秤;微软和Linux核心开发人员对此都很有眼光。   

  

  Rust有一些迷人的特性,如AOT、内存安全、空指针安全、丰富的类型系统、差异或巨大的社区生态。这些其他语言也有,但都没有这么彻底。   

  

  铁锈的特征也很突出。拥有和借用,引用和影子都让人眼花缭乱。最有意思的是,你写程序的时候,编译器会对你大加指责,你感觉就像在和编译器打架。   

  

  学Rust不仅仅是心血来潮(早在2015年就听说了),更是一种拥抱变化的态度。重要的是,当你阅读众多的区块链密码时,你不会如此慌张。   

  

  语言的趋势反映了未来主流开发群体的预测,Rust确实是后起之秀。   

  

  在这个话题中,我将阐述我学习Rust的过程,希望能指导你在学习一门新的编程语言时,如何更高效地去做。同时,我会用一个小程序树来展示它的不同之处。   

  

  缘起   

  

  几乎没有一个懂区块链基础知识的人不知道Rust,一种编程语言。它在区块链底层的开发商中很受欢迎。奇怪的是,Rust起源于Mazilla,它唯一的大规模应用是Firefox。作为一种小众语言,它在区块链圈子里流行起来。这应该和以太坊创始人戈文伍德(Govin Wood)创立的平价项目有关。奇偶校验是一个用Rust写的以太坊客户端。   

  

  最初接触Rust的时间大概是2015年,当时有同事发邮件“你对Rust编程语言感兴趣吗?”那时候我年轻热情,觉得这种语言因为很酷的小众,特别适合练习,就激情回复了邮件,结果之后就没有下文了。想必那位同事也是因为响应的人数太少而兴趣缺缺。   

  

  我第二次关注Rust是因为田琛在他的微信官方账号里提到了这个语言。我更欣赏田琛。我在学习仙丹的时候受到了他的影响,所以我跟着他的步伐去听张汉东的Know Live,然后加入了他的读者群(Charm Rust)。我在这个团潜水半年了,一直惊叹于这个团的活跃度。   

  

  2019年,区块链圈的一件大事是,脸书要发行非主权货币Libra,随后是基于Rust的Move编程语言。说白了,这一招就是Move的DSL。用比较学术的术语来说,就是指称的语义。Move的语法通过简单的编译器翻译成Rust的语法,然后Rust的编译器生成二进制代码。这个过程中没有什么惊喜,但是Move语言明显借鉴了Rust中所有权的概念,代表了数字资产只能有一个所有者的事实。一旦它们被移动,主权将被转移,以前的主人将失去这一主权。这种想法与Rust中的主权管理非常契合,所以也就不难理解Libra的开发团队为什么要抄袭这个名字了。当然,天秤座的底层区块链也使用铁锈色。这个大事件,连同以太坊平价的珍珠玉,都在前面。对于程序员这种天生喜欢新事物的人类群体来说,学习Rust的热情必然高涨。   

  

  就是在这个机会,我开始学习铁锈。按照老规矩,我还是会从命令行程序树开始,通过试错逐渐学会语言Rust。包括其基本数据类型、组合数据类型、控制流、模块(函数)、文件和集合操作,以及所有权最关键的应用。   

  

  实践出真知   

  

  学Rust最深刻的体会就是和编译器竞争,这是我听过最多的抱怨。我想很多新手看到这么多警告或者错误,什么都不说,应该是心情不好吧。但这也是Rust引以为傲的设计理念。   

  

  每一种新语言都有其本质原因或设计哲学。比如Lisp家的Clojure就有优雅和家庭重要的神秘妙语。追溯到古代,Java的一次编写,随处运行的豪言壮语;Rust的基本设计理念是,如果它能编译,那么它就能工作。想想这个条件有多苛刻,就知道动态弱类型语言向静态强类型语言的逐渐趋同,基本宣告了类型系统的胜利。   

  

  即便如此,现代软件工程始终强调程序员要编写各种测试来保证代码运行的正确性——从单元测试到集成测试,从冒烟测试到回归测试,从概要分析到性能测试。   

。这些测试方法和工具已经深入到软件工程的方方面面,然而各类软件还是漏洞百出。Rust 发出这种高调宣言,不免有夜郎自大之嫌疑。不过程序届是个能造概念也能落地概念的神奇圈子,高调的牛吹着吹着也就实现了。况且,Rust 充分诠释了现代编程语言的核心思想――约束程序员,不是劝劝你的意思,是憋死你的意思。

  

我在《我是如何学习新的编程语言》中说过学习的最好方式是有目的地试错,我时常拿来练手的程序叫tree - list contents of directories in a tree-like format. 这段程序需要用到的 Rust 基本构件有:

  

基础概念1. 变量 - let2. ownership borrow - &3. 可变性 - mut4. 可变引用 - &mut复合数据类型1. String - String::from("") // 非基本类型2. Slice - "" or vec<..>2. struct - struct {}集合及其操作1. Vec<_> - Vec::new() // 考虑到集合需要自动扩展2. iter()3. .map()4. .enumerate()5. .flatten()6. .collect()7. .extend() //集合拼接控制语句1. if Expressions - if {} else {}2. recursions模块1. fn - fn x(s: String) -> Vec<String>功能组件1. Path2. fs3. env当尝试寻找这些元素时,我发现 Rust 或者诸如此类的编译型语言都有一个让人不舒服的地方――验证的前置步骤耗时太长。因为没有repl,所以想去了解一些概念的使用方法,就不得不另外创建一个项目(我可不想污染当前项目的代码),在它的 main 函数里编写试验程序,这比起具有快速反馈能力的repl,着实太慢了。不过这里的慢也是相对的,Rust 也有一个显著的优势,在出现编译错误时,编译器不仅能向你解释原因,还能推荐潜在的修改方式,这就比 Javascript 一类的动态语言要清晰和高明得多。再利用内置的 assert_eq! 等断言函数预判结果,又比单独写测试省事。所以,总体而言,学习的过程还是很愉悦的。

  

快速获取

  

这里举个例子,为了解如何拼接两个集合时,需要事先搞明白几个问题:

  

集合的构造?集合的拼接?结果的断言?在没有repl的条件下,唯一快速上手的工具就是文档,在 https://doc.rust-lang.org/std/ 的官方标准库中,可以搜到Struct std::vec::Vec的详细解释。

  

通过例子程序,可以很快知道集合的构造方式如下:

  

let mut v = vec!<1, 2, 3>;v.reverse();assert_eq!(v, <3, 2, 1>);vec! 宏可以快速构造出一个集合来,顺便试验下它的reverse方法。那么集合如何拼接呢?为了解答这个问题,我一般会用搜索引擎,或者深入文档,查找如 concat,append等关键字,每每总有收获。

  

在不考虑非功能需求的前提下,我们先用最直接的方式实现,例如:文档中给出的样例extend方法

  

let v = vec!<1, 2, 3>;v.extend(<1, 2, 3>.iter().cloned()); // 编译错误注意,这里编译失败。Rust 编译器会直截了当地给出错误信息。

  

error: cannot borrow `v` as mutable, as it is not declared as mutable --> src/main.rs:13:5 |12 | let v = vec!<1, 2, 3>; | - help: consider changing this to be mutable: `mut v`13 | v.extend(<1, 2, 3>.iter().cloned()); | ^ cannot borrow as mutable错误信息中透露出我们的程序在尝试借用(borrow)一个不可变的变量。borrow和 mutable都是新的概念。对于新的概念,我们会习惯地用熟知的知识去类比。如果套用函数式编程中不可变的特性,大体可以猜到 Rust 中的变量默认是不可变的。但是 cannot borrow as mutable 中 borrow 确实是有点超出认知范围。那么此时弄清定义是非常有必要的。

  

澄清概念

  

学习语言的过程中最需要注意的事项就是澄清概念。当遇到崭新的概念时,我们得停下先去补充这部分的知识,然后再回过头来理解和解决实际遇到的问题。因为每一门编程语言都有本门派的哲学原理,它本身就萃取了多种理论和实践的成果,所以必须学习这些概念。学习的过程其实就是逐步澄清概念的过程。

  

在学习(尝试定义)borrow 的过程中,我又先后接触到了 ownership, move, reference, mutable reference 等概念。所以我定义了这些概念:

  

Ownership

  

变量拥有它指称的值的所有权。

  

在 Rust 当中,变量拥有它指称的值,即变量(variable)是它指称值(value)的主人(owner),值一次只能有一个主人,一旦主人离开作用域它的值就会被销毁。

  

Move

  

把一个变量的值重新赋值给另一个变量的行为。

  

根据 Ownership 的定义,值一次只能有一个主人,所以此时该值的所有权会被转移给另一个变量,原来的变量就丧失了对这个值的所有权,导致的直接影响就是这个变量此后不再可用。

  

Reference

  

一个变量指向(refer to)值而非拥有该值的所有权的状态。

  

在很多赋值的场景,包括变量赋值或者函数参数赋值,我们并不希望之后原来的变量不再可用,此时可以通过&(ampersands创建一个指向值的引用,将引用进行赋值时不会发生 Move,所以原来的变量依旧可用。这种赋值行为被称为borrow(借用)。结合实际,我们拥有的物品可以出借给别人,别人享有该物品的使用权(Possession),而非所有权(Ownership)。

  

Mutable reference

  

标识该引用的值是可变的。

  

很多场景下,我们希望引用传递的值是可以改变的。此时我们就必须通过&mut标识该引用,否则不允许修改操作发生。值得注意的是,&mut标识要求原来的变量也必须是mut的,这很好理解,可变的变量的引用也得可变。而且为了防止数据竞态条件的发生,在同一个作用域下,&mut的引用只能有一个,因为一旦出现多个可变引用,就可能遭遇不可重复读风险(注意,Rust 保证这里没有并行修改的风险)。而且同一个值的&mut和&的引用不能共存,因为我们不希望一个只读&的值同时还能被写&mut,这样会导致歧义。

  

解释错误

  

澄清了必要概念以后,我们再来回顾上面的代码。先去看一下这个extend函数的定义:

  

fn extend<I>(&mut self, iter: I)where I: IntoIterator<Item = T>, Extends a collection with the contents of an iterator...原来v.extend只是一个语法糖,真正的方法调用会把self作为第一个参数传递到extend(&mut self, iter: I)当中。可变引用作为函数参数赋值,那么自然原来的变量也必须声明成可变的。

  

所以我们照着它的指示修正如下:

  

let mut v = vec!<1, 2, 3>; // 加上一个mut修饰符v.extend(<1, 2, 3>.iter().cloned());这回编译器消停了,利用assert_eq!,我们来验证extend操作的正确性。

  

assert_eq!(v, <1, 2, 3, 1, 2, 3>);另外,值得注意的是,Rust 和我们熟悉的函数式编程有些不同,集合的拼接不会产生一个新的集合,而是对原有的集合进行修改。一般情况下,我们都会警惕可能会出现数据的竞态条件――多个线程对该集合进行写入操作怎么办?带着这个问题,我们反思一下什么是数据的竞态条件。

  

数据竞态条件

  

数据竞态条件发生的必要条件有:

  

多个引用同时指向相同的数据;至少有一个引用在写数据;对于数据的访问没有同步机制。考察1和2:

  

假如此处有两个引用指向同一个集合,如下:

  

let mut v = vec!<1, 2, 3>;let r1 = &mut v;let r2 = &mut v;assert_eq!(r1, r2);编译器会立即给出编译错误

  

error: cannot borrow `v` as mutable more than once at a time--> src/main.rs:13:10|12 | let r1 = &mut v;| ------ first mutable borrow occurs here13 | let r2 = &mut v;| ^^^^^^ second mutable borrow occurs here14 | assert_eq!(r1, r2);| ------------------- first borrow later used here也就是说,在指定的作用域下只能有一个可变引用。为什么要如此设计呢?在单线程下,这好像并不会出现数据竞争的问题<1>(https://www.reddit.com/r/rust/comments/95ky6u/why_arent_multiple_mutable_references_allowed_in/)。不过考虑到下面这种场景的语义,我们思考一下。

  

let mut v = vec!<1, 2, 3>;let r1 = &mut v;let r2 = &mut v;assert_eq!(r2<1>, 2);*r1 = vec!<0>assert_eq!(r2<1>, 2); // 失效一旦允许r1改变数据,那对于r2而言,它先前持有的数据就已经发生改变甚至失效,再拿来使用就有问题了,在上面这个例子当中,*r1解除引用后被重新赋值,导致v的值随之改变,但是r2并不知情,依旧使用r2<1>导致此处越界。这个问题和数据库中事务的不可重复读(提交读)的隔离级别类似,但是在单线程下这并不能算作充分的理由,只是说在语义层面有细微的不自然,留待后续研究。

  

蹊跷的是,如果我将两个可变引用放到不同的函数中,同样的逻辑却可以绕过编译器错误。

  

fn main() { let mut v = vec!<1, 2, 3>; mut1(&mut v); mut2(&mut v);}fn mut1(v: &mut Vec<i32>) { *v = vec!<0>;}fn mut2(v: &mut Vec<i32>) { println!("{}", v<1>); // panicked at 'index out of bounds' 运行时错误}可见,上述的论述并没有解释清楚在单线程下同一个作用域下限制多个可变引用的根本原因。

  

对于&mut和&其实也可以做同样的解释。所以&mut和&在 Rust 同一个作用域中无法共存。

  

考察3:

  

至于在多线程的环境下,是否会出现数据竞态条件,我们得看 Rust 在线程使用方面的限制。在 Rust 的上下文里,使用Thread::spawn的线程时必须 Move 所有权<2>(http://squidarth.com/rc/rust/2018/06/04/rust-concurrency.html),因为在 Rust 看来,Thread 的 LifeTime(生命周期)会比调用它的函数的生命周期的长,如果不 Move 所有权,那么线程中数据就会在调用函数结束后释放掉变量的内存,导致线程中的数据无效。所以,这样的限制是很有必要的,但反过来想,一旦数据的所有权发生转移,那么多个线程并行修改同样数据的可能性也就不复存在。

  

构建树状结构

  

struct Entry { name: String, children: Vec<Entry> }fn tree(path: &Path) -> Entry { Entry{ name: path.file_name() .and_then(|name| name.to_str()) .map_or(String::from("."), |str| String::from(str)), children: if path.is_dir() { children(path) } else { Vec::new() } }}既然是树状结构,定义的结构体就是递归的。这里的struct Entry {}就是一种递归的结构。我想实现的树状结构大致如下:

  

entry :: {name, }child :: entryRust 中没有显式的return,最后一个表达式的结果会被当成返回值,所以此处整个Entry结构体会被返回。

  

path.file_name() .and_then(|name| name.to_str()) .map_or(String::from("."), |str| String::from(str)),这段代码看上去很复杂,但实现的功能其实很简单,目的是为了获取当前文件的文件名。那么逻辑为何如此绕呢?这是由于 Rust 中的多种字符串表示导致的问题,暂按不表。先去看看各个函数的定义。

  

Path.file_name 的定义

  

pub fn file_name(&self) -> Option<&OsStr>and_then是我们常见的flat_map操作在 Rust 中的命名,其目的是为了在两个Option之间实现转换。

  

OsStr.to_str 的定义

  

pub fn to_str(&self) -> Option<&str>上面的path.file_name().and_then(|name| name.to_str())最终转变成了Option<&str>,在其上调用Option.map_or方法并提供默认值:字符串"."。为什么要提供默认值呢?这和OsStr到Str的转换密切相关,当我们传入参数"."时,Path.file_name返回的其实是一个None。

  

构建了父级的树状结构,我们需要把子级的树状结构也一并完成,最终通过递归,构建出一棵内存中的目录树。

  

fn children(dir: &Path) -> Vec<Entry> { fs::read_dir(dir) .expect("unable to read dir") .into_iter() .map(|e| e.expect("unable to get entry")) .filter(|e| is_not_hidden(&e)) .map(|e| e.path()) .map(|e| tree(&e)) .collect()}fn is_not_hidden(entry: &DirEntry) -> bool { entry .file_name() .to_str() .map(|s| !s.starts_with(".")) .unwrap_or(false)}这里也存在挺多的转换操作,我们一一解释。

  

fs::read_dir(dir).expect("unable to read dir")使用expect是因为fs::read_dir返回的是一个Result<ReadDir>,在其上调用expect会尝试解开其中的值,如果有错则会抛出错误。解开的结果类型是ReadDir,它是io::Result<DirEntry>的迭代器,也就是一个目录下的所有类目,可以在上面调用into_iter()创建出可以被消费的迭代器。

  

.map(|e| e.expect("unable to get entry")).filter(|e| is_not_hidden(e)).map(|e| e.path()).map(|e| tree(&e))接着,解开Result<DirEntry>之后,我们把隐藏文件过滤掉,因为filter接收的一个闭包,这个闭包的类型声明是P: FnMut(&Self::Item) -> bool,所以filter接收的所有元素都是引用类型,故调用时无需需声明成is_not_hidden(&e)。

  

然后利用e.path()获取每个文件的全路径,并依次交给tree去递归构建。经过tree和children两个函数的交替递归,内存中的一棵目录树就被构建出来了。

  

有了内存中的树状结构,我们接下来就可以渲染这个结构了。具体的做法如下:

  

对于第一层目录名,如果它是最后一个目录,则前缀修饰为L_branch = "└── ";反之,装饰成 T_branch = "├── "。对于有子目录,如果是其父目录是父级最后一个目录,则前缀装饰为SPACER = " ";反之,前缀装饰成 I_branch = "│ "。逻辑如下:

  

fn decorate(is_last: bool, children: Vec<String>) -> Vec<String> { const I_BRANCH: &str = "│ "; const T_BRANCH: &str = "├── "; const L_BRANCH: &str = "└── "; const SPACER: &str = " "; let prefix_first = if is_last { L_BRANCH } else { T_BRANCH }; let prefix_rest = if is_last { SPACER } else { I_BRANCH }; let mut first = vec!)>; first.extend(children<1..>.iter().map(|child| format!("{}{}", prefix_rest, child)).collect::<Vec<_>>()); first}这里比较好用的字符串拼接操作是format!("{}{}", &str, &str)。

  

渲染树状结构

  

fn render_tree(tree: &Entry) -> Vec<String> { let mut names = vec!; // error let children = &tree.children; let children: Vec<_> = children .iter() .enumerate() .map(|(i, child)| decorate(children.len() - 1 == i, render_tree(child))) .flatten() .collect(); names.extend(children); names}这里会有编译错误,错误信息如下:

  

error: cannot move out of `tree.name` which is behind a shared reference --> src/main.rs:48:26 |48 | let mut names = vec!; | ^^^^^^^^^ move occurs because `tree.name` has type `std::string::string`, which does not implement the `copy` trait由于tree.name不是标量类型(Scalar Type),它没有实现copy trait(见提示),又因为tree本身是复合类型(Compound Type),tree.name如果发生 Move 的话,包含它的tree就有问题了。为了避免发生这种情况,我们不得不去引用&tree.name。但是一旦加上引用,又会出现类型不匹配的编译错误。

  

59 | names | ^^^^^ expected struct `std::string::String`, found reference | = note: expected type `std::vec::Vec<std::string::String>` found type `std::vec::Vec<&std::string::String>`我们期待的是Vec<String>而不是Vec<&String>,所以需要重新构建出一个String出来。可以使用String::from(&String)方法

  

let mut names = vec!;这样修改下来,才能保证编译完全通过。但事实上,Rust 给我们提供了一个更加便捷的写法

  

let mut names = vec!使用to_owned()表示重新拷贝了一份数据,和重新构建一个String出来别无二致。

  

组合调用

  

use std::env;use std::path::Path;use std::fs::{self, DirEntry};fn main() { let args: Vec<String> = env::args().collect(); println!("{}", render_tree(&tree(Path::new(&args<1>))).join("\n"));}render_tree 返回的是Vec<String>,所以为了打印出来,我们将所有元素用"\n" join到一起。

  

.├── Cargo.toml├── Cargo.lock...└── src └── main.rs总结

  

学习下来的一些主观感觉是 Rust 中的概念繁杂,有些地方的设计确实让人有些迷惑。再加上类型众多(如:OsStr, String),代码很难通过直觉判断写出,需要大量查阅文档才能让编译器消停。所以学习曲线相对陡峭。

  

不过,语言约束的越多,某种程度上讲,对于程序员而言却是福音。If it compiles, then it works. 的哲学理念在前,学习道阻且长,努力加餐饭。

  

提示

  

一般标量类型都实现了copy trait.

  

所有的整型,如:u32布尔类型,如:true 或 false字符类型,如:char浮点数类型,如:f64当且仅当所有元素都是Copy的元组,如:(i32, i32)是Copy,但是(i32, String)就不是Copy的。

相关文章