Rust 语言学习笔记(一)

学了不少编程语言,多数是离不开垃圾回收的,要么像 C++ 仍然是过于复杂,对于通用,编译型编程语言 Rust 是个不错的选择, Rust 不需要垃圾回收器。Rust 是由 Mozilla 主导开发的,设计准则为 "安全,并发,实用", 支持函数式,并发式,过程式以及面向对象的编程风格。Rust 能达到与 C++ 接近的性能,它又是编译型语言,编译出来二进制文件执行时不再依赖于运行时。Rust 有自带的 Cargo 作为依赖管理与构建工具,免除了关键工具的选择综合症。AWS 在今年也推出了 Rust 的 AWS SDK, 所以学习 Rust 的过程中也打算使用它来操作 AWS 的资源。

Rust 的 Hello World

在 macOS 下的安装
$ brew install rust

$ curl https://sh.rustup.rs | sh    # 安装 $ rustc --version                              # 查看版本 $ rustup update                               # 更新 $ rustup self uninstall                     # 卸载
当前 Rust 版本为 1.74.0, 安装后有 rustc, rustdoc, rust-gdb, rust-lldb, cargo 等相关命令

创建一个并运行 hello
 1$ cargo new hello
 2$ tree hello
 3hello
 4├── Cargo.toml
 5└── src
 6    └── main.rs
 7$ cd hello
 8$ cargo run
 9   Compiling hello v0.1.0 (/Users/yanbin/test/hello)
10    Finished dev [unoptimized + debuginfo] target(s) in 3.12s
11     Running `target/debug/hello`
12Hello, world!
cargo new 生成的项目有一个标准布局,不像 C++ 的项目那么自由混乱。自然的,Cargo.toml 中可配置项目的信息及管理依赖。

src/main.rs 的内容如下
1fn main() {
2    println!("Hello, world!");
3}
main.rs 中的 Hello World 代码本身没多大意义,我们由此可了解 Cargo 的使用

cargo run 生成的二进制文件是在 target/debug/hello,可以直接执行它或分发到相同的平台中,不需要预先安装任何的运行时。

Cargo 也像 Maven 那样,用 cargo --help 可查看所有的构建命令。cargo xxx -v 可看到具体调用的对应命令的详细命令。

cargo doc 会生成自己项目和所用到的所有第三方依赖的文档,用 cargo doc --open 能一步生成文档并在在浏览器中打开相应的 index.html 文件。如果只需生成自己代码的文档用 cargo doc --no-deps

Rust 的注释格式:
  1. // 普通单行注释
  2. /* */ 块注释
  3. //! 当前项目的标识注释
  4. /// 文档注释,可应用于处,像 Java 的 /** **/, 其中支持 Markdown

比如我们可以重新构前面的 hello 执行文件
$ cargo clean $ cargo build --release $ target/release/hello Hello, world! $ ls -l target/release/hello -rwxr-xr-x 1 yanbin staff 488704 Dec 30 23:40 target/release/hello
执行文件 488K (通过 strip 可缩减到  363K)

在 Ubuntu 20.04 中可以用 apt 命令安装 rust
sudo apt install -y rust-all
然后在 Ubuntu 20.04 下编译 Rust Hello World
$ cargo new hello && cd hello $ cargo build --release $ ls -lh target/release/hello -rwxrwxr-x 2 vagrant vagrant 13M Dec 31 06:26 target/release/hello $ strip target/release/hello $ ls -lh target/release/hello -rwxrwxr-x 2 vagrant vagrant 327K Dec 31 06:26 target/release/hello $ cargo rustc --release -- -C prefer-dynamic $ ls -lh target/release/hello -rwxrwxr-x 2 vagrant vagrant 17K Dec 31 06:27 target/release/hello
直接构建生成的可执行文件 13M, strip 之后是 327K, 只要没有使用 -C prefer-dynamic 构建的执行文件挪到其他没安装 rust 的 Linux 下可直接执行(当然要匹配 CPU 架构了)。如果用了 -C prefer-dynamic 生成的只有 17K, 它运行时还须连接到 Rust 运行库,所以拿到一个纯净的 Ubuntu 20.04 下直接执行的话,出现如下错误
$ ./hello $ ./hello: error while loading shared libraries: libstd-90f6ddbf82de36ec.so: cannot open shared object file: No such file or directory
如果把安装了 rust-all 机器下的 /usr/local/rustup/toolchains/1.75.0-x86_64-unknown-linux-gnu/lib/libstd-90f6ddbf82de36ec.so 文件拷到那台纯净 Ubuntu 20.04 的当前目录下,并且设置 export LD_LIBRARY_PATH=., 再执行 ./hello 就没问题了。libstd-90f6ddbf82de36ec.so 文件大小有 14M。

strip 和 -C prefer-dynamic 可配置在 Cargo.toml 文件中

注:如果用 docker 镜像 rust:1.75-buster 的 cargo build --release 生成的 target/release/hello 文件大小是 4.3M, strip 之后是  355K

在 macOS 下可通过 Docker 来编译出 Linux 版本的二进制执行文件。IntelliJ IDEA 中可用 Dev Containers 插件,然后当前目录中创建 .devcontainer/devcontainer.json 文件,基本内容
1{
2  "name": "rust-build",
3  "image": "rust:1.75-buster"
4}
在 devcontainer.json 上下文菜单中有 Dev Containers 菜单来启动开发容器. devcontainer.json 的详细配置请参考 Dev Container metadata reference.

使用 IntelliJ IDEA 的话,在运行 Cargo run 时可指琮 Run on -- SSH 或 Docker

Rust 支持跨平台编译, 比如在 macOS 下编译出 Linux 下的可执行文件,这需另开一个专题来研究。

如果简单的代码不使用 Cargo 的话,可直接创建 main.rs 源文件,然后用
$ rustc main.rs
就会生成可执行的 main 文件

Rust 语言基本采用了 C/C++ 的大括号/分号的代码风格。最后来一段代码作为对 Rust 的总体的感受
 1use std::env::args;  // Rust 的 use 和 Python 的 import 一样,可以写在任何位置
 2
 3fn main() {       // 函数定义的关键字最简化成 fn
 4    greet_world()  // 函数中最后一条语句不写分号作为函数的返回值,相当于 return greet_world(); 有 return 必须要分号
 5}
 6
 7fn greet_world() {
 8    let x: i8 = 1; // 变量声明(准备讲是变量绑定)用 let, 类型可省略,会自动推断; 像 JS 的 let,但 x 是不可变的
 9    // x = 2;      // x 是不可变的,所以重新赋值是非法的
10    let x = "10"; // 但是重样报声明 x 是可以的,之前的 x 就不见了,所以 let 应理解变量绑定
11    let mut y = 3; // 加了 mut 修饰的变量才是可变的
12    y = 5;
13    const MAX_RETRY: u8 = 3; // 常量声明必须指明类型, 常量不像 let 那样可重复声明
14    let regions = ["USA", "China"]; // 数组声明直接用 [], 和 Python 的 list 一样
15    for region in regions.iter() {
16        println!("{}", &region) // 感叹号(!) 使用宏, "&" 符号表示"借用"-只读访问
17    }
18
19    // Rust 的 Closure/Lambda 的写法,像 Ruby 那样竖线分隔参数与代码
20    let regions_in_lower_case: Vec<_> = regions.iter().map(|r|r.to_lowercase()).collect();
21    println!("{:?}", &regions_in_lower_case); // ["usa", "china"]
22
23    let a: i8 = 127;
24    // let b: i8 = a + 1; // i8 的范围是 -128 ~ 127, 由于编译期会计算 a + 1,会报告溢出错误
25
26    let config = r#"
27        {"image":"rust:1.75-buster"}
28    "#;  // heredoc 语法,如果是长字符串可像 Java 的 properties 那样用 \ 换行
29
30    for i in 0..3 {  // Range 类型,(0..3) 相当 Java 的 IntStream.range(0,3), (0..=3) 就是 IntStream.rangeClosed(0,3)
31        print!("{} ", i)  // 打印 0 1 2
32    }
33    println!();
34
35    let args: Vec<String> = args().collect(); // 运行参数,和 bash 一样, args[0] 是命令本身,从 1 起才是用户输入, cargo run -- arg1 arg2
36    if let Ok(length) = args[1].parse::<u32>() { // let Ok(length) 相当于模式匹配, 无法解析不报错,不匹配则是 Err(E)
37        println!("input length: {}", length)
38    }
39}
因为是编译型语言,Rust 的函数不需像 Python 那样提前声明,但作为编译型的 C 也是要提前声明函数的。

Rust 函数默认的返回值是 (), 读作  unit, 和 Scala 的空返回值是一样的。 声明函数的返回值用 ->, 如 fn foo() -> String {...}。Rust 的赋值语的返回值不是最终的变量值,而是 (), 所以即使变量 a 是 bool 类型,if a = true {} 也是非法的,更不需要像 Java 中建议的 if true == a 的写法。

Rust 像 C/C++/Objective C 那样也是支持宏的,Rust 的宏支持可没有 Scala 的那么复杂

Rust 的编译器尽可能的做更多的事情,更多的优化,以使用在程序运行时更安全,更好的性能。

像局部变量因为是在内部使用,它会建议声明为下划线开始的变量名,如 config -> _config。未使用的变量会有警告,但不像 Go 编译器发现未使用的变量直接报错 ; 未使用的 use 引用也会有警告。

比如线程竞争访问外部变量,访问删除(drop) 的变量,像上面的数值溢出,遍历集合时集合被修改了编译时都会有警告。在编译信息中经常看到 borrow, move, trait 这样的表述。

Rust 方法默认是静态分派的,和 C++ 一样,方法默认不是虚的; 而 Java 默认都是虚方法,这也会降低程序执行的性能。

链接:

  1. Cross-compiling Rust From Mac to Linux
永久链接 https://yanbin.blog/rust-language-learning-1/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。