1小时入门Rust

作者: songtianyi 2018-07-29

前言

1分钟

在系统地了解了编程语言的类型特性和编程范式之后,一直想验证下对这些知识的掌握程度,以及验证这些知识能否为我们入门一门语言提速,那么最佳的途径应该是选择一门新语言来上手实践。为什么选择Rust?Rust和Go一样年轻,如果说go是C-like language,那么Rust就是C++-like的语言,有人称其为更安全的C++, 对于长期从事C++开发的程序猿可以学来尝鲜。初看Rust有点想放弃,Rust没有GC,并发的书写方式也并没有Go那么方便,但是多看几眼,它的某些优点还是吸引到了我,比如安全性,高性能,闭包等,所以本文最终以Rust为例,来做这两方面的验证。在阅读本文之前,掌握类型特性和编程范式里的概念是必要的,且阅读本文需要一定的编程基础。

Rust是什么

3分钟

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety

零开销抽象

简单来说,在Java里,class A有个成员b,b的类型是class B,如果A的实例a引用b的成员函数m,那么调用链是 a.b.m(), 这个调用过程需要两次指针访问,意味着更多的开销,而c++里的class,同样是抽象,但只需一次指针访问,相比未抽象的情况没有额外的一次开销,所以叫零开销,这种抽象实现方式叫零开销抽象。

安装

5分钟

linux下可以直接执行这个命令来下载安装脚本并执行它。

由于墙的原因,失败的概率会很高,在这个页面可以找到对应个系统的离线安装包,比如mac的

验证安装结果:

编辑工具用vscode,然后在命令行使用rustc(v1.27.2)编译。

类型系统

10分钟

LangTypedStatic and dynamic checksStrongly checkedWeakly or strongly typedDynamically or statically typedType theoriesParadigms
Rust☑️☑️☑️stronglystaticallygeneric, overloading, subtypeIP,SP,PP,OOP,FP

类型理论

10分钟

泛型

和c++/Java一样,Rust的泛型风格基本类似

除此之外还有泛型结构体等,这里不再一一列举,需要在实战中摸索。

值得一提的是,我们可以限定T的类型范围:

还有另外一种表达能力更强的写法,where语句:

当我们限定的不是T,而是使用T的方式时where会很有用:

上述代码中,我们限定了 Option<T> : Debug, 即Option<T>必须实现了Debug trait。

多态

即subtyping。Rust和golang一样并没有继承这一说,也没有class,但struct是可以定义函数的,也能指定可见性。struct和 trait结合使用能够达到通常OOP中继承和多态的效果。

上述代码中print_g的入参类型为&Graph, 既能将&Circle作为输入,也能将&Rec作为输入,即是多态用法。需要强调的是,trait是没有类型关系的,我们不能说Rec是Graph的子类型, 这和其他基于类型关系的subtyping不一样。Rust传统意义上的subtyping是在lifetime中体现的,'big <: 'small 意味着big的生命周期比small长,big是small的子类型(subtype), 在使用'small(它是一个类型)的地方都可以使用'big代替。

重载

在Rust里操作符其实是语法糖,a + b等价于 a.Add(b), 能够用操作符操作的类型都实现了std::ops::Add这个trait,那我们为某个类型实现Add trait,即重载了它的加法操作符。

Rust没有函数重载。

语法规范

25分钟

Types
Primitive types
基础类型解释
i8,i16,i32,i64,u8,u16整数, i代表有符号, u代表无符号
f32, f64浮点数, IEEE-754标准
bool布尔型, true or false. ex. let b: bool = false;
char, str字符/字符串
isize, usizeThe pointer-sized unsigned integer type. 依赖运行平台指针大小的类型,在32bit的机器上,它是占用四个字节的整形,在64位的机器上它占用8个字节
fn函数
never书写为!,用来标记never类型
Compound types
复合类型解释
array, slice, Vec数组/切片/向量数组,let b = [0; 20]; 定义大小为20的不可修改数组,并将所有的值初始化为0,切片则是数组的引用 let sli = &b[..],数组需定义大小,切片则不用; Vec则是标准库提供的分配在堆上的变长数组
struct结构体. ex. struct test{a:b, c:d}, 可以使用pub来标记字段的可见性
closure闭包
maprust的标准库提供了hash map等高级数据结构
fn pointer函数指针. ex. type Binop = fn(i32, i32) -> i32; Binop是一个函数指针
pointer, reference指针/引用, 指针的值和普通类型一样,可以被移动,拷贝,存储和返回,标准库提供了智能指针;引用则是指向别的值所在的内存地址的类型,分为shared reference和mutable reference
enum枚举, Rust的枚举比golang表达更加丰富,它的成员可以为struct或者tuple struct或者unit struct
union联合
recursive使用递归方式定义的类型,struct,union,enum都可以递归
traitrust里的interface
tuple元组

如果你看了我之前写的《编程语言选型》里的基础类型和复合类型,理解Rust的类型就会轻松一些,但是Rust还是提供了很多新的东西。

never type

never类型是Rust里的特殊类型,在早期的版本里甚至称不上是类型,因为它不占用任何空间,不能像普通类型一样初始化。你可以认为它是一个不存在的类型,可以用来占位,下面的代码中,Result枚举中的第二个类型是never,当我们不需要返回错误时,可以用它来占位。

2016年,Rust将!升级成了一个标准的类型,意味着你可以用它来绑定变量。它的主要用途不变,但目前还在experimental的阶段。

unit struct
tuple struct
recursive type
struct

给struct定义成员函数的方式和golang类似, 但使用impl关键字来标记, self用来代替所实现的结构体。

元组索引
函数

和Groovy一样,Rust支持隐式return语句,而且推荐这么做,当你这么做的时候末尾不要接分号,否则它会被当成一个表达式而不是return语句,对于这类书写错误,rustc编译器会提示你怎么修正。

类型别名
闭包

闭包的入参参数写在||内,之后是函数逻辑。注意,下面的代码如果不加||, 花括号内的值会被当作表达式先执行,然后将执行的返回值作为入参。

枚举

Rust的枚举用法较多,和java一样可以带构造器。

联合

rust一开始是没有union类型的,因为rust的enum即是tagged union,属于比较安全的union实现方式。后来加入了untagged union, 在访问它的字段时要加unsafe

Trait

Rust的trait类似于golang中的interface,它告诉编译器一个类型必须提供哪些函数。你可以为任意类型实现某个trait。

关于trait的使用,会在subtyping中介绍。Rust中有个特殊的trait Drop,作用类似于析构函数,大家可以自行了解。

Statements
If

if语句和golang一样没有括号。

但是语法糖会多一些。

let

let语句,用来绑定变量。

Operators
Operator/ExpressionAssociativity
? 
Unary - * ! & &mut 
asleft to right
* / %left to right
+ -left to right
<< >>left to right
&left to right
^left to right
|left to right
== != < > <= >=Require parentheses
&&left to right
||left to right
.. ..=Require parentheses
= += -= *= /= %= &= |= ^= <<= >>=right to left
Expressions

表达式是语句(statements)的子集,这里按照rust reference文档,部分关于语句的语法也写在expression的范畴内,且只列举较陌生的表达式写法。

Path expression

Rust使用::的方式来表达引用路径,和C++类似。

ErrorPropagation Expression

错误传播表达式,和Result<T, E>相结合用来解决异常处理的问题,格式为表达式接问号:

当表达式为正常值时继续运行,当表达式为Err时立即返回。

TypeCastExpression

类型转换表达式,用as来表示:

Array expression

数组表达式,仅列举Rust中特别的数组初始化方式:

这里提一下,我们知道,数组是可以通过下标来访问的:

如果其他的类型实现了 std::ops::Index trait和std::ops::IndexMut trait,那么这些类型也可以通过下标来访问,因为方括号的形式只是语法糖。

Closure expression

即闭包。Rust的闭包写法稍微有些奇特,可能是因为->被用来标记函数回参了吧。

管道符内的是入参,其后的内容是一个表达式,注意,{}也是一个表达式。

也可以不指定返回类型,让编译器自行推断。Rust的闭包||{}也是语法糖,底层仍然是通过trait来实现的。对于闭包的使用,较复杂的是如何返回一个闭包,具体代码如下:

在创建闭包的时候添加了一个关键字move,因为n是临时变量且在闭包中被借用,当闭包在当前函数factory外被调用,n是不存在的,所以编译器不允许这么做,move则告诉编译器,n需要被拷贝,这样被拷贝的n就存在于闭包自己的内存栈中。Rust要求返回值必须是固定大小的,而函数的大小不确定,用Box来封装之后,它的大小就确定了。

For expression

For语句, 像脚本语言的写法。

Range expression

此表达式会创建并初始化一个结构体,看表和例子即可。

ProductionSyntaxTypeRange
RangeExprstart..endstd::ops::Rangestart ≤ x < end
RangeFromExprstart..std::ops::RangeFromstart ≤ x
RangeToExpr..endstd::ops::RangeTox < end
RangeFullExpr..std::ops::RangeFull-
RangeInclusiveExprstart..=endstd::ops::RangeInclusivestart ≤ x ≤ end
RangeToInclusiveExpr..=endstd::ops::RangeToInclusivex ≤ end
Match expression

match表达式也是Rust的主要特色之一。除了可以当作常规的switch语句使用之外

还可以匹配更复杂的类型实例

_代表默认逻辑。

Modules

rust的模块(包)引入语法和其他语言不太一样,我们先看下怎么定义一个模块。

将上面的内容写入math_mod.rs中,然后在另外一个文件引入:

引入时,也可以显式地指定路径:

可以使用use关键字来简化包的使用

也可以为mod或者mod里的item定义别名

编程范式

5分钟

函数式编程

Rust并不是纯函数式语言,语言的设计者们并不教条,执着在一种范式或类型特性上,而是从开发的角度出发,提供解决问题的方法,因此Rust作为一门年轻的现代语言,还是提供了函数式编程的途径。

面向对象编程

面向对象是一个很好的概念,但并不是所有情况下都是最优的选择,语言的设计者们权衡之后,都放弃了纯面向对象的方式,比如Go。Rust并不是一门面向对象语言,也没有类或者继承的概念,但确实可以像面向对象语言那样编程,因此也认为它具备面向对象编程这个范式。面向对象的三大特征是封装/继承/多态。封装不用说,struct是可以定义成员函数的,多态特性我们在subtyping中也讲到过,那么继承呢?Rust的继承写法也是通过trait来完成的,通过组合trait来建模对象之间的共性。

One + Two的语法是有说法的,TODO: Algebraic data type

元编程

元编程一种程序修改自身的行为。学过C语言的应该知道它的宏概念,宏也属于元编程的范畴,预处理器会将宏代码替换成新的代码。C/C++的宏实现是基于文本替换的,属于不安全的宏。举一个例子:

C的预处理器进行宏替换后的代码为:

我们预期的结果应该是a被加1等于5,b被加1等9,但是输出为:

这个例子不太恰当,一般我们不会犯这类错误,这里仅为了说明C语言宏实现的缺陷。

而Rust的宏实现要强大复杂的多,Rust的宏系统借助了语法树及它的模式匹配。

Rust的宏用!标记,比C/C++的用大写来标记可读性要高。上述代码中foo这个宏会对y => 3进行模式匹配,将代码替换成 =>后的内容,同时将表达式的值绑定到变量e上,可以看出它并不是简单的文本替换。Rust的宏还有更高级的用法,这里不展开讲。

总结

1分钟

单从本文涉及到的内容来看,Rust上手已经不算简单,因为Rust引入了一些其他语言没有的概念,比如ownership/lifetime等,trait是Rust类型系统的核心特性,很多语法都是基于它来实现的。此外,本文未涉及到的内容还有很多,如条件编译/注释及文档/并发编程/内联汇编/C binding/crate/cargo等,感兴趣的可以关注之后的文章。

参考资料