学习 Rust(第一章 寻找牛刀,以便小试)

参考资料:https://course.rs/first-try/installation.html

1.1 安装 Rust 环境

(太简单,看原网页)

1.2 强推vscode

如果你的电脑慢,有一点一定要注意:

在编译器构建代码的同时,不要在终端再运行 cargo run 等命令进行编译,不然会获得一个报错提示,大意是当前文件目录已经被锁定,等待其它使用者释放。如果等了很久 IDE 还是没有释放(虽然我没遇到过,但是存在这个可能性),你可以关掉 IDE,并手动 kill 掉 rust-analyzer,然后重新尝试。

1.3 认识 Cargo

总而言之,cargo 提供了一系列的工具,从项目的建立、构建到测试、运行直至部署,为 Rust 项目的管理提供尽可能完整的手段。同时,与 Rust 语言及其编译器 rustc 紧密结合,可以说用了后就忘不掉,如同初恋般的感觉。

创建一个"你好,世界"项目

终于到了紧张刺激的 new new new 环节:

$ cargo new world_hello
$ cd world_hello 

上面的命令使用 cargo new 创建一个项目,项目名是 world_hello (向读者势力低头的项目名称,泪奔),该项目的结构和配置文件都是由 cargo 生成,意味着我们的项目被 cargo 所管理

早期的 cargo 在创建项目时,必须添加 --bin 的参数,如下所示:

$ cargo new world_hello --bin
$ cd world_hello 

现在的版本,已经无需此参数,cargo 默认就创建 bin 类型的项目,

顺便说一句,Rust 项目主要分为两个类型:bin 和 lib,前者是一个可运行的项目,后者是一个依赖库项目。

下面来看看创建的项目结构:

$ tree
.
├── .git
├── .gitignore
├── Cargo.toml
└── src
    └── main.rs

是的,连 git 都给你创建了,不仅令人感叹,不是女儿,胜似女儿,比小棉袄还体贴。

有两种方式可以运行项目:

  1. cargo run

  2. 手动编译和运行项目

首先来看看第一种方式,一码胜似千言,在之前创建的 world_hello 目录下运行:

$ cargo run
   Compiling world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/world_hello`
Hello, world!

好了,你已经看到程序的输出:"Hello, world"

上述代码,cargo run 首先对项目进行编译,然后再运行,因此它实际上等同于运行了两个指令,下面我们手动试一下编译和运行项目:

编译

$ cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s

运行

$ ./target/debug/world_hello
Hello, world!

行云流水,但谈不上一气呵成。 细心的读者可能已经发现,在调用的时候,路径 ./target/debug/world_hello 中有一个明晃晃的 debug 字段,没错我们运行的是 debug 模式,在这种模式下,代码的编译速度会非常快,可是福兮祸所依,运行速度就慢了. 原因是,在 debug 模式下,Rust 编译器不会做任何的优化,只为了尽快的编译完成,让你的开发流程更加顺畅。

作为尊贵的读者,咱自然可以要求更多,比如你想要高性能的代码怎么办? 简单,添加 --release 来编译:

  • cargo run --release
  • cargo build --release

试着运行一下我们高性能的 release 程序:

$ ./target/release/world_hello
Hello, world!

cargo check

当项目大了后,cargo run 和 cargo build 不可避免的会变慢,那么有没有更快的方式来验证代码的正确性呢?大杀器来了,接着!

cargo check 是我们在代码开发过程中最常用的命令,它的作用很简单:快速的检查一下代码能否编译通过。因此该命令速度会非常快,能节省大量的编译时间。

$ cargo check
    Checking world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s

Rust 虽然编译速度还行,但是还是不能与 Go 语言相提并论,因为 Rust 需要做很多复杂的编译优化和语言特性解析,甚至连如何优化编译速度都成了一门学问: 优化编译速度

Cargo.toml 和 Cargo.lock

Cargo.toml 和 Cargo.lock 是 cargo 的核心文件,它的所有活动均基于此二者。

  • Cargo.toml 是 cargo 特有的项目数据描述文件。它存储了项目的所有元配置信息,如果 Rust 开发者希望 Rust 项目能够按照期望的方式进行构建、测试和运行,那么,必须按照合理的方式构建 Cargo.toml

  • Cargo.lock 文件是 cargo 工具根据同一项目的 toml 文件生成的项目依赖详细清单,因此我们一般不用修改它,只需要对着 Cargo.toml 文件撸就行了。

什么情况下该把 Cargo.lock 上传到 git 仓库里?很简单,当你的项目是一个可运行的程序时,就上传 Cargo.lock,如果是一个依赖库项目,那么请把它添加到 .gitignore 中

现在用 VSCode 打开上面创建的"世界,你好"项目,然后进入根目录的 Cargo.toml 文件,可以看到该文件包含不少信息:

package 配置段落

package 中记录了项目的描述信息,典型的如下:

[package]
name = "world_hello"
version = "0.1.0" edition = "2021" 

name 字段定义了项目名称,version 字段定义当前版本,新项目默认是 0.1.0edition 字段定义了我们使用的 Rust 大版本。因为本书很新(不仅仅是现在新,未来也将及时修订,跟得上 Rust 的小步伐),所以使用的是 Rust edition 2021 大版本,详情见 Rust 版本详解

定义项目依赖

使用 cargo 工具的最大优势就在于,能够对该项目的各种依赖项进行方便、统一和灵活的管理。

在 Cargo.toml 中,主要通过各种依赖段落来描述该项目的各种依赖项:

  • 基于 Rust 官方仓库 crates.io,通过版本说明来描述
  • 基于项目源代码的 git 仓库地址,通过 URL 来描述
  • 基于本地项目的绝对路径或者相对路径,通过类 Unix 模式的路径来描述

这三种形式具体写法如下:

[dependencies]
rand = "0.3"
hammer = { version = "0.5.0"} color = { git = "https://github.com/bjz/color-rs" } geometry = { path = "crates/geometry" } 

相信聪明的读者已经能看懂该如何引入外部依赖库,这里就不再赘述。详细的说明参见此章:Cargo 依赖管理,但是不建议大家现在去看,只要按照目录浏览,拨云见雾指日可待。

1.4 不仅仅是 hello world

接下来,对世界友人给予热切的问候:

fn greet_world() {
    let southern_germany = "Grüß Gott!"; let chinese = "世界,你好"; let english = "World, hello"; let regions = [southern_germany, chinese, english]; for region in regions.iter() { println!("{}", &region); } } fn main() { greet_world(); } 

打开终端,进入 world_hello 工程根目录,运行该程序。(你也可以在 VSCode 中打开终端,方法是点击 VSCode 上方菜单栏中的终端->新建终端,或者直接使用快捷键打开。)

上面的 So Easy 的余音仍在绕梁,我希望它能继续下去,可是… 人总是要面对现实,因此让我们来点狠活:

fn main() {
   let penguin_data = "\ common name,length (cm) Little penguin,33 Yellow-eyed penguin,65 Fiordland penguin,60 Invalid,data "; let records = penguin_data.lines(); for (i, record) in records.enumerate() { if i == 0 || record.trim().len() == 0 { continue; } // 声明一个 fields 变量,类型是 Vec // Vec 是 vector 的缩写,是一个可伸缩的集合类型,可以认为是一个动态数组 // <_>表示 Vec 中的元素类型由编译器自行推断,在很多场景下,都会帮我们省却不少功夫 let fields: Vec<_> = record .split(',') .map(|field| field.trim()) .collect(); if cfg!(debug_assertions) { // 输出到标准错误输出 eprintln!("debug: {:?} -> {:?}", record, fields); } let name = fields[0]; // 1. 尝试把 fields[1] 的值转换为 f32 类型的浮点数,如果成功,则把 f32 值赋给 length 变量 // // 2. if let 是一个匹配表达式,用来从=右边的结果中,匹配出 length 的值: // 1)当=右边的表达式执行成功,则会返回一个 Ok(f32) 的类型,若失败,则会返回一个 Err(e) 类型,if let 的作用就是仅匹配 Ok 也就是成功的情况,如果是错误,就直接忽略 // 2)同时 if let 还会做一次解构匹配,通过 Ok(length) 去匹配右边的 Ok(f32),最终把相应的 f32 值赋给 length // // 3. 当然你也可以忽略成功的情况,用 if let Err(e) = fields[1].parse::<f32>() {...}匹配出错误,然后打印出来,但是没啥卵用 if let Ok(length) = fields[1].parse::<f32>() { // 输出到标准输出 println!("{}, {}cm", name, length); } } } 

看完这段代码,不知道你的余音有没有戛然而止,反正我已经在颤抖了。这就是传说中的下马威吗?

上面代码中,值得注意的 Rust 特性有:

  • 控制流:for 和 continue 连在一起使用,实现循环控制。
  • 方法语法:由于 Rust 没有继承,因此 Rust 不是传统意义上的面向对象语言,但是它却从 OO 语言那里偷师了方法的使用 record.trim()record.split(',') 等。
  • 高阶函数编程:函数可以作为参数也能作为返回值,例如 .map(|field| field.trim()),这里 map 方法中使用闭包函数作为参数,也可以称呼为 匿名函数lambda 函数
  • 类型标注:if let Ok(length) = fields[1].parse::<f32>(),通过 ::<f32> 的使用,告诉编译器 length 是一个 f32 类型的浮点数。这种类型标注不是很常用,但是在编译器无法推断出你的数据类型时,就很有用了。
  • 条件编译:if cfg!(debug_assertions),说明紧跟其后的输出(打印)只在 debug 模式下生效。
  • 隐式返回:Rust 提供了 return 关键字用于函数返回,但是在很多时候,我们可以省略它。因为 Rust 是 基于表达式的语言

在终端中运行上述代码时,会看到很多 debug: ... 的输出,上面有讲,这些都是 条件编译 的输出,那么该怎么消除掉这些输出呢?

读者大大普遍冰雪聪明,肯定已经想到:是的,在 认识 Cargo中,曾经介绍过 --release 参数,因为 cargo run 默认是运行 debug 模式。因此想要消灭那些 debug: 输出,需要更改为其它模式,其中最常用的模式就是 --release 也就是生产发布的模式。

具体运行代码就不给了,留给大家作为一个小练习,建议亲自动手尝试下。

1.5. 下载依赖很慢或卡住?

在目前,大家还不需要自己搭建的镜像下载服务,因此只需知道下载依赖库的地址是 crates.io,是由 Rust 官方搭建的镜像下载和管理服务。

但悲剧的是,它的默认镜像地址是在国外,这就导致了某些时候难免会遇到下载缓慢或者卡住的情况,下面我们一起来看看。

下载很慢

解决下载缓慢有两种方式:

  1. 开启命令行或者全局FQ 经常有同学反馈,我明明开启FQ了,但是下载依然还是很慢,无论是命令行中下载还是 VSCode 的 rust-analyzer 插件自动拉取。

事实上,FQ工具默认开启的仅仅是浏览器的FQ代理,对于命令行或者软件中的访问,并不会代理流量,因此这些访问还是通过正常网络进行的,自然会失败。

因此,大家需要做的是在你使用的FQ工具中 复制终端代理命令 或者开启全局FQ。由于每个FQ软件的使用方式不同,因此具体的还是需要自己研究下。以我使用的 ClashX 为例,点击 复制终端代理命令 后,会自动复制一些 export 文本,将这些文本复制到命令行终端中,执行一下,就可以自动完成代理了。

export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7891
  1. 修改 Rust 的下载镜像为国内的镜像地址 这个效果最直接,但是就是稍有些麻烦。

为了使用 crates.io 之外的注册服务,我们需要对 $HOME/.cargo/config.toml ($CARGO_HOME 下) 文件进行配置,添加新的服务提供商,有两种方式可以实现:增加新的镜像地址和覆盖默认的镜像地址。

这里推荐使用科大的注册服务来提升下载速度,以下注册服务的链接都是科大的

增加新的镜像地址

首先是在 crates.io 之外添加新的注册服务,在 $HOME/.cargo/config.toml (如果文件不存在则手动创建一个)中添加以下内容:

[registries]
ustc = { index = "https://mirrors.ustc.edu.cn/crates.io-index/" }

这种方式只会新增一个新的镜像地址,因此在引入依赖的时候,需要指定该地址,例如在项目中引入 time 包,你需要在 Cargo.toml 中使用以下方式引入:

[dependencies]
time = {  registry = "ustc" }

在重新配置后,初次构建可能要较久的时间,因为要下载更新 ustc 注册服务的索引文件,还挺大的...

覆盖默认的镜像地址

事实上,我们更推荐第二种方式,因为第一种方式在项目大了后,实在是很麻烦,全部修改后,万一以后不用这个镜像了,你又要全部修改成其它的。

而第二种方式,则不需要修改 Cargo.toml 文件,因为它是直接使用新注册服务来替代默认的 crates.io

在 $HOME/.cargo/config.toml 添加以下内容:

[source.crates-io]
replace-with = 'ustc'

[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index" 

首先,创建一个新的镜像源 [source.ustc],然后将默认的 crates-io 替换成新的镜像源: replace-with = 'ustc'

简单吧?只要这样配置后,以往需要去 crates.io 下载的包,会全部从科大的镜像地址下载,速度刷刷的.. 我的 300M 大刀( 宽带 )终于有了用武之地。

这里强烈推荐大家在学习完后面的基本章节后,看一下 Cargo 使用指南章节,对于你的 Rust 之旅会有莫大的帮助!

下载卡住

下载卡住其实就一个原因:下载太慢了。

根据经验来看,卡住不动往往发生在更新索引时。毕竟 Rust 的包越来越多,索引也越来越大,如果不使用国内镜像,卡住还蛮正常的,好在,我们也无需经常更新索引 :P

Blocking waiting for file lock on package cache

不过这里有一个坑,需要大家注意,如果你同时打开了 VSCODE 和命令行,然后修改了 Cargo.toml,此时 VSCODE 的 rust-analyzer 插件会自动检测到依赖的变更,去下载新的依赖。

在 VSCODE 下载的过程中( 特别是更新索引,可能会耗时很久),假如你又在命令行中运行类似 cargo run 或者 cargo build 的命令,就会提示一行有些看不太懂的内容:

$ cargo build
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache

其实这个报错就是因为 VSCODE 的下载太慢了,而且该下载构建还锁住了当前的项目,导致你无法在另一个地方再次进行构建。

解决办法也很简单:

  • 增加下载速度,见前面内容
  • 耐心等待持有锁的用户构建完成
  • 强行停止正在构建的进程,例如杀掉 IDE 使用的 rust-analyzer 插件进程,然后删除 $HOME/.cargo/.package_cache 目录

1.6 避免从入门到放弃

仔细阅读编译错误

在一些编程语言中,你可能习惯了编译器给出的错误只要看前面(或后面)几行就行,大部分是不怎么用到的信息,总之编译器总感觉笨笨的。

但是 Rust 不是,它为我们提供了一个强大无比的编译器,而且会提示我们该如何修改代码以解决错误,简直就是一个优秀的老师!

因此在使用 Rust 过程中,如果错误你不知该如何解决,不妨仔细阅读下编译器或者 IDE 给出的错误提示,绝大多数时候,你都可以通过这些提示顺利的解决问题。

同时也不要忽略编译器给出的警告信息(warnings),因为里面包含了 cargo clippy 给出的 lint 提示,这些提示不仅仅包含代码风格,甚至包含了一些隐藏很深的错误!至于这些错误为何不是 error 形式出现,随着学习的深入,你将逐渐理解 Rust 的各种设计选择,包括这个问题。

不要强制自己使用其它编程语言的最佳实践来写 Rust

大多数其它编程语言适用的最佳实践在 Rust 中也可以很好的使用,但是 Rust 并不是一门专门的面向对象或者函数式语言,因此在使用自己喜欢的编程风格时,也要考虑遵循 Rust 应有的实践。

例如纯面向对象或纯函数式编程,在 Rust 中就并不是一个很好的选择。如果你有过 Go 语言的编程经验,相信能更加理解我这里想表达的含义。

不过大家也不用担心,在书中我们以专题的形式专门讲解 Rust 的最佳实践,看完后自然就明白了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值