1 小时入门 Rust

作者: songtianyi create@2018-07-29, update@2020-11-22

前言

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 下可以直接执行这个命令来下载安装脚本并执行它。

curl https://sh.rustup.rs -sSf | sh

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

验证安装结果:

rustc -V

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

类型系统

10 分钟

Lang Typed Static and dynamic checks Strongly checked Weakly or strongly typed Dynamically or statically typed Type theories Paradigms
Rust ☑️ ☑️ ☑️ strongly statically generic, overloading, subtype IP, SP, PP, OOP, FP

类型理论

10 分钟

泛型

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

// generic function fn foo<T>(_x: &[T]) {} fn main() { foo(&[1, 2]); }

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

struct SGen<T>(T); fn main() { SGen(2); }

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

trait Graph { fn area(&self) -> f64; } // 限定 T 的类型必须是实现了 Graph 的类型 fn foo<T : Graph>(_x: &[T]) {}

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

fn foo<T>(_x: &[T]) where T : Graph {}

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

use std::fmt::Debug; trait PrintInOption { fn print_in_option(self); } // Because we would otherwise have to express this as `T: Debug` or // use another method of indirect approach, this requires a `where` clause: impl<T> PrintInOption for T where Option<T>: Debug { // We want `Option<T>: Debug` as our bound because that is what's // being printed. Doing otherwise would be using the wrong bound. fn print_in_option(self) { println!("{:?}", Some(self)); } } fn main() { let vec = vec![1, 2, 3]; vec.print_in_option(); }

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

多态

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

trait Graph { fn area(&self) -> f64; } // 定义一个结构体 struct Circle { x: f64, y: f64, radius: f64, } // 为 Circle 实现 Graph trait,或者说实现 Graph 接口 impl Graph for Circle { // 必须实现 area fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) } } // 定义一个新的结构体类型 struct Rec { x: f64, y: f64, length: f64, height: f64, } // 为 Rec 实现 Graph trait impl Graph for Rec { // 必须实现 area fn area(&self) -> f64 { self.length * self.height } } fn print_g(g : &Graph) { println!("graph area {}", g.area()); } fn main() { let c = Circle{ x: 1.0, y: 1.0, radius: 1.0 }; let r = Rec { x: 1.0, y: 1.0, length: 1.0, height: 2.0, }; print_g(&c); print_g(&r); }

上述代码中 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,即重载了它的加法操作符。

use std::ops::Add; #[derive(Debug)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } impl std::fmt::Display for Point { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "({},{})", self.x, self.y) } } fn main() { let p1 = Point { x: 1, y: 0 }; let p2 = Point { x: 2, y: 3 }; let p3 = p1 + p2; println!("{}", p3) }

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, usize The 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 闭包
map rust 的标准库提供了 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 都可以递归
trait rust 里的 interface
tuple 元组

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

never type

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

fn from_str(s: &str) -> Result<String, !> { Ok(String::from(s)) }

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

let x: ! = panic!()
unit struct
struct u {}
tuple struct
struct t {i32, char}
recursive type
enum List<T> { Nil, Cons(T, Box<List<T>>) } let a: List<i32> = List::Cons(7, Box::new(List::Cons(13, Box::new(List::Nil))));
struct

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

struct Cicle { x: f64, y: f64, radius: f64, } impl Cicle { fn area(&self) -> f64 { // area std::f64::consts::PI * (self.radius * self.radius) } fn pos(&self) { println!("({}, {})", self.x, self.y); } } fn main() { let c : Cicle = Cicle{ x: 1.0, y: 1.0, radius: 1.0 }; println!("{}", c.area()); c.pos() }
元组索引
fn main() { let b = 32; let tuple = ("a", 3, b); println!("{} {} {}", tuple.0, tuple.1, tuple.2); }
函数
fn main() { let x = foo; println!("{}", x()); } fn foo() -> i32 { 110 // return 110; }

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

fn foo() -> i32 { 110; // compile error }
类型别名
fn main() { type Alias = (i32, char); // 为元组定义一个别名 let _t : Alias = (10, 'a'); }
闭包

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

fn f(_g: F) { _g(); } type F = fn() -> String; fn main() { f(||{ println!("rust-lang is best lang!"); String::from("foo") }) }
枚举

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

enum Animal { Dog, Cat, } let mut a: Animal = Animal::Dog; a = Animal::Cat;
enum Animal { Dog(String, f64), Cat { name: String, weight: f64 }, } let mut a: Animal = Animal::Dog("Cocoa".to_string(), 37.2); a = Animal::Cat { name: "Spotty".to_string(), weight: 2.7 };
联合

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

#[repr(C)] union MyUnion { f1: u32, f2: f32, } fn main() { let u = MyUnion { f1: 1 }; unsafe { let f = u.f1; println!("{}", f); } }
Trait

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

trait HasArea { fn area(&self) -> f64; }

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

Statements
If

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

let x = 5; if x == 5 { println!("x is five!"); } else if x == 6 { println!("x is six!"); } else { println!("x is not five or six :("); }

但是语法糖会多一些。

let x = Some(3); let a = if let Some(1) = x { 1 } else if x == Some(2) { 2 } else if let Some(y) = x { y } else { -1 };
let

let 语句,用来绑定变量。

Operators
Operator/Expression Associativity
?
Unary - * ! & &mut
as left 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++类似。

let y = String::from("bar");
ErrorPropagation Expression

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

Expression ?

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

fn main() { fn try_to_parse() -> Result<i32, std::num::ParseIntError> { let x: i32 = "123".parse()?; // x = 123 let y: i32 = "24a".parse()?; // returns an Err() immediately Ok(x + y) // Doesn't run. } let res = try_to_parse(); println!("{:?}", res); }
TypeCastExpression

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

let size: f64 = len(values) as f64;
Array expression

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

let a = [0; 128];

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

a[0]

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

Closure expression

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

let plus_one = |x: i32| x + 1;

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

fn main() { let _f = |x: i32, y: i32| -> i32 {x + y}; println!("{}", _f(1, 2)); }

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

fn factory(n: i32) -> Box<Fn(i32) -> i32> { Box::new(move |x| x + n) } fn main() { // factory 函数返回了一个闭包 let f = factory(5); let answer = f(1); assert_eq!(6, answer); }

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

For expression

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

for x in 1..100 { if x > 12 { break; } last = x; }
Range expression

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

Production Syntax Type Range
RangeExpr start .. end std::ops:: Range start ≤ x < end
RangeFromExpr start .. std::ops:: RangeFrom start ≤ x
RangeToExpr .. end std::ops:: RangeTo x < end
RangeFullExpr .. std::ops:: RangeFull -
RangeInclusiveExpr start ..= end std::ops:: RangeInclusive start ≤ x ≤ end
RangeToInclusiveExpr ..= end std::ops:: RangeToInclusive x ≤ end
1..2; // std::ops::Range 3..; // std::ops::RangeFrom ..4; // std::ops::RangeTo ..; // std::ops::RangeFulls 5..=6; // std::ops::RangeInclusive ..=7; // std::ops::RangeToInclusive
Match expression

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

let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), 4 => println!("four"), 5 => println!("five"), _ => println!("something else"), }

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

match message { Message::Quit => println!("Quit"), Message::WriteString(write) => println!("{}", &write), Message::Move{ x, y: 0 } => println!("move {} horizontally", x), Message::Move{ .. } => println!("other move"), Message::ChangeColor { 0: red, 1: green, 2: _ } => { println!("color change, red: {}, green: {}", red, green); } };

_ 代表默认逻辑。

Modules

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

pub mod math { pub fn f() -> f64 { 1.1 } }

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

mod math_mod; fn main() { println!("{}", math_mod::math::f()); }

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

#[path = "math_mod.rs"] mod m; fn main() { println!("{}", m::math::f()); }

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

#[path = "math_mod.rs"] mod m; use m::math; fn main() { println!("{}", m::math::f()); println!("{}", math::f()); }

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

#[path = "math_mod.rs"] mod m; use m::{math::f as mf}; fn main() { println!("{}", m::math::f()); println!("{}", mf()); }

编程范式

5 分钟

函数式编程

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

面向对象编程

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

trait One { fn one(&self); } trait Two: One { fn two(&self); } trait Com: One + Two; struct Foo; impl Com for Foo { fn one(&self) {} fn two(&self) {} }

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

元编程

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

#define INCI(i) do { int a=0; ++i; } while(0) int main(void) { int a = 4, b = 8; INCI(a); INCI(b); printf("a is now %d, b is now %d\n", a, b); return 0; }

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

int main(void) { int a = 4, b = 8; do { int a=0; ++a; } while(0); do { int a=0; ++b; } while(0); printf("a is now %d, b is now %d\n", a, b); return 0; }

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

a is now 4, b is now 9

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

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

macro_rules! foo { (x => $e:expr) => (println!("mode X: {}", $e)); (y => $e:expr) => (println!("mode Y: {}", $e)); } fn main() { foo!(y => 3); }

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

总结

1 分钟

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

参考资料