前言
进入大学以来(计算机应用技术——大数据方向),就像很多程序猿🐒一样,小北开始每学期学习一种新的编程语言。通过学习另一个编程语言,可以了解很多规范和规则,并得到了一些想法,然后小北在大二的第一学期就发现了Rust。一开始写Rust代码就觉得非常有趣,让人无法挪步,而且经过一份陡峭的学习曲线后,她变得越发有趣。所以,总共学习了两个额外的语言(C++和Python)后,我坚定地选择了Rust。为什么?下面来告诉友友们具体的原因...
首先,Rust是一个系统编程语言,会默认地提供内存安全性而无需垃圾回收器,这会影响其运行时行为。尽管如此,Rust仍然是一个相当全能的语言,可以在不同的领域中使用,不论是Web编程、游戏引擎还是Web客户端,都可以使用Rust。另外,她对相关作用域和内存分配的传统思维发出挑战,会让小北成为一个可以驾驭任何语言的更好的程序猿🐒,无论是C#、Java还是Python。Amazon、Microsoft和Google等公司的最新动向表明,这个生态系统现在已经是发展到相当稳定的程度,已经足以让企业安心使用,我想这对于未来的Rust专业人员来说是一个很好的信号~~~
友友们还在 Rust Playground 中在线试用 Rust,而无需在计算机上安装任何东西。
在这篇博客中,小北会继续汇编一些实验以及实战案例,快速在生产环境中使用。小北会尽力覆盖各种不同的应用场景,找到属于自己的理解和概念,以及可以直接用于日常开发工作的解决方案。入门 - Rust 编程语言 (rust-lang.org)https://www.rust-lang.org/learn/get-started
Cargo .toml
src/main.rs
cargo run
第一章 Rust 入门
(工具链和项目结构)
1.1建立环境(老路子,需要科学上网,不建议,小北在后面会补充中文版)
由于编程语言会带来各种工具链、工具、连接器和编译器版本,选择最合适的版本并不容易。另外,Rust在所有主要的操作系统上都可以使用,这又增加了一个变数。
不过是用rustup(https://rudtuo.rs/),安装Rust会变成一个非常简单的任务。在这个网站上,可以下载一个有用的脚本(或用于Windows的安装程序),它会负责获取和安装所需的组件。这个工具还允许你切换和更新(以及卸载)这些组件。
- Visual Studio Code (http://code.visualstudio.com);
- rustup (https://rustup.rs);安装 Rust - Rust 程序设计语言 (rust-lang.org)https://www.rust-lang.org/zh-CN/tools/install
- rustc(及其与编译工具链);什么是rustc?- rustc 手册 English (rustwiki.org.cn)https://www.rustwiki.org.cn/zh-CN/rustc/
- cargo:安装 - Cargo 手册 中文版 (rustwiki.org)https://rustwiki.org/zh-CN/cargo/getting-started/installation.html
- RLS(Rust Language Server)的简写,用于自动补全;Rust 语言服务器 (booyaa.wtf)https://rls.booyaa.wtf/
- Visual Studio Code 的Rust语言支持.
在Linux或UNIX系统(如macOS)上,Rust调用cc来链接,而在Windows上,选择的链接器是Microsoft Visual Studio 的链接器, Rust要求你的系统上有一个可用的原生链接器。
它需要先安装 Microsoft Visual C++ BuildTools。
尽管也可以在 Windows 上使用一个开源工具链,不过这个练习留给更高级需求的友友来完成。 要编写代码,还需要一个编辑器。由于Visual Studio Code支持一些Rust特性,因此结合使用Visual Studio Code 和 Rust扩展是一个很好的选择。这也是Microsoft开发的一个开源编辑器,在全世界和Rust社区中都很受欢迎。在这个技巧中,我们将安装以下组件:
接下来是小北推荐的一种安装方法:
Rustup必须安装在本地开发环境中。可以用以下链接进行安装:
GitHub - rust-lang/rustup: The Rust toolchain installerhttps://github.com/rust-lang/rustup官方安装说明网址如下:
Install Rust - Rust Programming Language (rust-lang.org)https://www.rust-lang.org/tools/install1.1.1准备工作
在运行macOS、Linux或Windows的计算机上,只需要一个Web浏览器以及互联网连接。要记住,Windows上的安装与*nix系统(Linux和macOS)稍有不同,后者使用脚本。
1.1.2技术要求
用rustup.rs管理Rust安装
为了测试是否用rustup成功地安装了Rust工具链,可以在Terminal中或者Windows的PowerShell中运行以下命令检查rustc、cargo是否已经正确安装:
rustc--version
cargo--version
更新
要更新 Rust,在终端执行以下命令即可更新:
win + r 唤出运行对话框 输入cmd 打开命令提示符 输入
rustup update
卸载
要卸载 Rust 和 rustup,在终端执行以下命令即可卸载:
win + r 唤出运行对话框 输入cmd 打开命令提示符 输入
rustup self uninstall
(终端Win+R——cmd)
(Win+X——终端管理员/Win+R+PowerShell)
(注意:运行这两个命令时,可能会有一个更新的版本。友友你的代码是否使用Rust 2024并不重要~)
安装MSYS(自选)
MSYS2是工具和库的集合,为您提供了一个易于使用的环境来构建、安装和运行本机 Windows 软件。
官网地址:MSYS2https://www.msys2.org/安装好后,不要直接运行,先配置软件源,否则下载超级慢此地址里也有它的安装步骤并配置MSYS2
配置好后,在 MSYS 中输入下面的命令来安装 rustup。
curl https://sh.rustup.rs -sSf | sh
按1 继续根据提示完成默认安装
MSYS2 安装完成后,友友你可以看到同时安装了6子程序:
你可以称它们为MSYS2环境或子系统。
MSYS2 之 Pacman 常用命令
配置国内镜像
使用[清华大学开源软件镜像站]中的地址,修改\etc\pacman.d目录下的三个文件。
配置教程:msys2 | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirrorhttps://mirrors.tuna.tsinghua.edu.cn/help/msys2/
MSYS2 使用 Pacman 进行软件包管理(包括 安装、更新、卸载等)
配置系统变量:在path中加入路径
注意: 此处不要勾选"立即运行 MSYS2 64bit", 因为接下来要配置国内更新源:
1. pacman -Suy 更新软件包
该命令会更新核心包和非核心包,核心包指MSYS2本身依赖的包(如:mintty、msys2-runtime、pacman等)
如果核心包、非核心包都需要更新,则该命令会先更新核心包,然后才能更新非核心包。更新完核心包后,会要求关闭所有已打开的终端,然后再重新执行一遍 pacman -Suy 命令,才会去更新非核心包。
2. pacman -Qs 搜索已安装的软件包
3. pacman -Ss 在所有软件包仓库中搜索(
已安装的软件包,会提示 [installed])
4. pacman -S make安装软件包
5. pacman -R 卸载软件包
pacman -R <name of the package>
注意,该命令仅卸载软件包,无法卸载该软件包安装时的依赖包,也无法移除该软件运行过程中产生的任何文件。
6. pacman -Sg 列出软件包
列出所有软件包组:
pacman -Sg
列出某个软件包组中所有的软件包:
pacman -Sg <group>
7. pacman -Q 列出已安装的软件包
列出已安装的软件包:
pacman -Q
列出所有从 Arch Linux 官方软件仓库安装的软件包:
pacman -Qe
8. pacman -Sw 下载软件包但不安装
pacman -Sw <package>
9. pacman -U 从本地安装软件包
pacman -U <packagefile.tar.zst>
支持 tar.xz,tar.zst 两种类型的归档包安装。归档包可以从这里下载:Index of / (msys2.org)https://repo.msys2.org/
但需要注意,该本地安装方式,无法自动安装软件包的依赖,需要你自行解决依赖的软件包。
可以通过 pacman -Qi 查看依赖的软件包。
10. pacman -Qi 查看软件包详细信息
pacman -Qi <package>
关于pacman命令的更多用法,可通过 pacman -h 查看:
$ pacman -h
usage: pacman <operation> [...]
operations:pacman {-h --help}pacman {-V --version}pacman {-D --database} <options> <package(s)>pacman {-F --files} [options] [file(s)]pacman {-Q --query} [options] [package(s)]pacman {-R --remove} [options] <package(s)>pacman {-S --sync} [options] [package(s)]pacman {-T --deptest} [options] [package(s)]pacman {-U --upgrade} [options] <file(s)>use 'pacman {-h --help}' with an operation for available options
通过 pacman <operation> -h 查看某个参数的更详细用法,如:
$ pacman -S -h
usage: pacman {-S --sync} [options] [package(s)]
options:-b, --dbpath <path> set an alternate database location-c, --clean remove old packages from cache directory (-cc for all)-d, --nodeps skip dependency version checks (-dd to skip all checks)-g, --groups view all members of a package group(-gg to view all groups and members)-i, --info view package information (-ii for extended information)-l, --list <repo> view a list of packages in a repo-p, --print print the targets instead of performing the operation-q, --quiet show less information for query and search-r, --root <path> set an alternate installation root-s, --search <regex> search remote repositories for matching strings-u, --sysupgrade upgrade installed packages (-uu enables downgrades)-v, --verbose be verbose-w, --downloadonly download packages but do not install/upgrade anything-y, --refresh download fresh package databases from the server(-yy to force a refresh even if up to date)--arch <arch> set an alternate architecture--asdeps install packages as non-explicitly installed--asexplicit install packages as explicitly installed--assume-installed <package=version>add a virtual package to satisfy dependencies--cachedir <dir> set an alternate package cache location--color <when> colorize the output--config <path> set an alternate configuration file--confirm always ask for confirmation--dbonly only modify database entries, not package files--debug display debug messages--disable-download-timeoutuse relaxed timeouts for download--gpgdir <path> set an alternate home directory for GnuPG--hookdir <dir> set an alternate hook location--ignore <pkg> ignore a package upgrade (can be used more than once)--ignoregroup <grp>ignore a group upgrade (can be used more than once)--logfile <path> set an alternate log file--needed do not reinstall up to date packages--noconfirm do not ask for any confirmation--noprogressbar do not show a progress bar when downloading files--noscriptlet do not execute the install scriptlet if one exists--overwrite <glob>overwrite conflicting files (can be used more than once)--print-format <string>specify how the targets should be printed--sysroot operate on a mounted guest system (root-only)
小北的参考资料:
What is MSYS2? - MSYS2https://www.msys2.org/docs/what-is-msys2/
MSYS2 Installer - MSYS2https://www.msys2.org/docs/installer/
Environments - MSYS2https://www.msys2.org/docs/environments/ (扩展)有些有用的特性只在nightly版本中提供。要安装rustc的nightly 版本,需要完成以下步骤:
(1)在一个 Terminal或 PowerShell 窗口运行 rustup install nightly(如果你没有使用GNU 工具链,可以在 Windows上使用 nightly-msvc)
(2)命令完成之后,可以使用rustup default nightly切换默认工具链(cargo 中使用)。
安装 Visual Studio Code 和扩展:
在 vanilla版本中,Visual Studio Code 为很多语言提供了语法突出显示功能。不过,对于自动补全或/和检查语法,还需要一个扩展。Rust项目提供了这个扩展:
(1)打开 Visual Studio Code.(2)使用Ctrl+P(macOS上为cmd+P)打开命令行界面,然后输入ext install rust-lang.rust来安装这个扩展。
ext install rust-lang.rust
安装界面
这个扩展使用RLS完成静态代码分析,并提供自动补全和语法检查。这个扩展应该会自动安装RIS组件,但有时也可能未成功安装。一种解决方法是在Visual Studio Code的 settings.json 文件中(可以使用 Ctrl+P/cmd+P来找到)增加以下配置:
{"rust-client. channel":"stable"
}
或者,rustup也可以用rustup component add rls命令安装RLS:
迁移:Rust support for Visual Studio Code (deprecated)2020
rust-analyzer - Visual Studio Marketplacehttps://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer故障排除:
有时,更新工具会导致文件丢失或无法覆盖等错误。这可能是各种各样的原因造成的不过完全重新安装就能解决问题。在Linux或macOS系统上,以下命令会负责删除rustup安装的所有内容:
rm -Rf~/.rustup
现在 Windows的PowerShell也支持很多类 Linux的命令:
PS> rm~/.rustup
这会得到同样的结果。删除当前安装后,再从头安装rustup一这应该会安装最新的版本下面来分析原理从而更好地理解这个技巧
安装其他好用的插件:
在此,再推荐友友们几个好用的插件:
Even Better TOML
,支持 .toml 文件完整特性Error Lens
, 更好的获得错误展示One Dark Pro
, 非常好看的 VSCode 主题CodeLLDB
, Debugger 程序
好了,至此,VSCode 的配置就已经全部结束,是不是很简单?下面让我们来用 Cargo
创建一个 Rust 项目,然后用 VSCode 打开。
1.1.3 工作原理
shell脚本rustup.sh是安装Rust的一个好方法,这也是当前安装Rust 和其他组件的主要方法。实际上,在CI系统中也经常使用这个脚本来安装编译器和其他工具。rustup是一个开源项目,由Rust项目维护,可以在GitHub上找到:
我们已经学习了如何建立我们的环境。现在来看下一个技巧
小试牛刀:使用命令行I/O
在命令行上与用户通信的传统方式是使用标准流。Rust包括一些有用的宏来处理这些简单的情况。在这个技巧中,我们将研究经典的Hello World程序的基本工作原理。
实现过程:
我们将通过5个步骤来研究命令行I/〇和格式化:
(1)打开一个Terminal窗口(Windows上打开PowerShell),运行 cargo new hello-world命令,这会在 hello-world 文件夹中创建一个新的Rust项目。
(2)一旦创建,用 cd hello-world 切换到这个目录,并用 Visual Studio Code 打开 src/main.rs。cargo生成的默认代码如下:
fn main() {println!("Hello, world!");
}
(3)下面来扩展这个代码!以下是前面那个传统print语句的一些变体,显示了一些格式化选项、参数以及在流上写输出等。下面先来看一些常见的print语句(和导人):
use std::io::{self,Write};
use std::f64;fn main(){println!("Let's print some lines:");println!();printIn!("Hello, world!");println!("{},{}!","Hello","world");print!("Hello,");println!("world!");
不过,还可以有更复杂的参数组合:
println!("Arguments can be referred to by their position:{0},
{1}! and {1},{0}! are built from the same arguments", "Hello",
'world");println!("Furthermore the arguments can be named: \"(greeting),
{object}!\"", greeting = "Hello", object = "World");println!("Number formatting: Pi is (0:.3} or (0:.0} for short",
f64::consts::PI);println!("...and there is more:{0:>0width$}={0:>width$}=
{0:#x}",1535,width = 5);let_= write!(&mut io::stdout(),"Underneath, it's all writing
to a stream...");
println!();println!("Write something!");
let mut input = String::new();
if let Ok(n)= io::stdin(),read_line(&mut input){println!("You wrote:{}({}bytes)", input, n);}else{eprintln!("There was an error :(");}
}
这会提供读写控制台的多种不同方式。
(4)再回到 Terminal,导航到 Cargo.toml 所在的目录。
(5)使用cargo run 查看这个代码段的输出:
Compiling hello-world v0.1.0(/tmp/hello-world)Finished dev[unoptimized + debuginfo] target(s) in 0. 37sRunning 'target/debug/hello- world'
Let's print some lines:Hello, world!
Hello, world!
Hello, world!
Arguments can be referred to by their position: Hello, world! and
world, Hello! are built from the same arguments
Furthermore the arguments can be named: "Hello, World!"
Number formatting: Pi is 3. 142 or 3 for short
..and there is more:01535=1535 =0x5ff
Underneath, it's all writing to a stream...
Write something!
Hello, world!
You wrote: Hello, world!
输出中的每一行表示了向控制台打印文本的一种方式!建议尝试不同方式,看看结果有什么变化。另外需要说明,rustc会检查所有println!()或format!()调用中参数个数否正确。
工作原理:
步骤1执行 cargo new hello-world时,会生成初始的代码段。作为一个二进制类型的项目,需要一个 main 函数(rustc 会找到这个 main 函数)。调用 cargo run 时,cargo 会组织编译(使用rustc)和链接(Windows上使用msve,*nix上使用ce),并通过其人口点(main函数)运行得到的二进制项目(步骤5)。
步骤3在创建的函数中,我们写了一系列print!/println!/eprintln!语句,这些是Rust宏。这些宏可以帮助我们写人一个命令行应用的标准输出或标准错误通道,而且可以包含额外的参数。实际上,如果缺少参数,编译器将无法编译程序。
Rust的宏直接处理这个语言的语法树,会提供类型安全性,并且能检查形参和实参。所以,它们被看作是有一些特殊能力的函数调用。
使用格式化器时可以组合各种参数和模板字符串,这是一种功能很强大的方法,可以向输出增加实际变量,而不需要使用连接或类似的变通方法。这会减少分配数,大大提高性能和内存效率。对于如何格式化数据类型,有很多不同的方式,要想更深入地了解有关内容,请查看 Rust 的优秀文档(https:/doc.rustlang.org/std/fmt/)。然后最后一步会显示各种组合生成的输出。
我们已经学习了如何使用命令行 I/O。现在来看下一个技巧。
1.2为项目选择正确的Rust配置
使用Rust语言编程时,必须首先选择Rust发布通道和Rust项目类型。
本节将讨论Rust发布通道(release channel)的详细信息,并就如何为项目选择这些通道提供指导。
Rust还允许你构建不同类型的二进制文件——独立的可执行文件、静态库和动态库,如果你预先知道你要构建的内容,则可以使用生成的脚手架代码(scaffolding code)创建正确的项目类型。
ext install rust
1.2.1选择Rust发布通道(小北这边只选择稳定版给友友们介绍使用)
Rust编程语言是不断开发的,并且在任何时间点都会同时开发3个版本,每个版本都称为一个发布通道(release channel),每个通道都有一个用途,并具有不同的功能和稳定性的特征。这三个发布通道如下:
- stable(稳定版)
- beta(测试版)
- nightly(夜间版)
在夜间版和测试版中会开发一些写不稳定的语言功能和库,而稳定版中可提供稳定功能。
Rustup是安装Rust编译器、Rust标准库和Cargo包管理器等工具,也可以用于安装代码格式化、测试、基准测试和文档生成等活动的核心工具。所以这些工具都有多种版本,它们被称为工具链(toolchain),工具链发布通道和主机(host)的组合,并且还可以选择关联的存档日期。
Rustup 可以从发布通道或者其他来源(比如官方存档和本地构建)安装工具链。Rustup还可以根据主机平台确定工具链。Rust在Linux、windows和macOS上正式可用。Rustup被称为工具多路选择器(tool multiplexer),因为它可以安装和管理多个工具链,在该意义上类似于Ruby、Python,以及Node.js中的rbenv、pyenv和nvm。
Rustup可管理与工具链相关的复杂性,但由于它提供了合理的默认值,因此安装过程相当简单。这些可以稍后有开发人员修改。
注意:
Rust的稳定版每6周发布一次,例如,Rust1.42.0于2020年3月12日发布,6周后Rust1.43于2020年4月23日发布。
Rust的夜间版每天都会发布一次。每6周,夜间版的最新主分支升级为测试版。该过程是平行发生的,所以每隔6周的同一天,夜间版会变成测试版,测试版会变成稳定版。当 1.x版本发布时,同时发布1.(x+1)的测试版和 1.(x+2)的夜间版。
大多数 Rust开发人员主要使用稳定版,主动使用测试版的较少,测试版仅用于测试Rust 语言版本中的回归。
夜间版用于积极的语言开发,每晚发布。夜间版可以让Rust开发新的和实验性的功能,并允许早期采用者在它们稳定之前对其进行测试。为抢先体验付出的代价是,这些功能在进入稳定版本之前可能会发生重大变化。
Rust使用功能标志来确定在给定的每晚发布中启用了哪些功能。想要在夜间版中使用前沿功能的用户必须使用适当的功能标志(featurefag)来注释代码。
以下就是一个功能标志的示例:
#![feature(try_trait)]
请注意,测试版和稳定版不能使用功能标志。
Rustup默认配置为使用稳定版。要使用其他版本通道,方法如下。
有关完整列表,请参考官方链接:GitHub - rust-lang/rustup: The Rust toolchain installerhttps://github.com/rust-lang/rustup要安装Rust 夜间版,可使用以下命令:
rustup toolchain install nightly
要全局激活Rust 夜间版,可使用以下命令:
rustup default nightly
要在目录级别激活Rust夜间版,可使用以下命令:
rustup override set nightly
要获取Rust夜间版的编译器版本,可使用以下命令:
rustup run nightly rustc --version
要重置Rustup 以使用Rust稳定版,可使用以下命令:
rustup default stable
要显示已安装的工具链和当前处于活动状态的版本,可使用以下命令:
rustup show
要将已安装的工具链更新到最新版本,可使用以下命令:
rustup update
请注意,一旦设置了 rustup default <channel-name>,其他相关工具(如 Cargo 和 Rustc就会使用默认通道集。
你应该在你的项目中使用哪个Rust版本?对于任何要应用于实际生产环境的项目,建议只使用稳定版本发布通道。对于任何实验性项目,可以考虑使用夜间版或测试版通道,但要谨慎,因为在未来版本中的代码可能需要进行重大更改。
1.3 Rust初体验
接下来友友们将和小北一起初体验Rust。我们首先要了解如何使用编译器,然后会快速编写个程序。在接下来的章节中,我们会写一个完整的项目。
注意:要安装Rust,需要使用官方提供的安装器(imstaller)进行安装。
1.3.1 直通“Hello, world!”
大多数程序员在接触一门新的语言时,要做的第一件事就是学习如何在控制台上“Hello,world!”。接下来你也需要这样做。在遇到令人讨厌的语法错误之前,你要先验证所有境是否已经准备就绪。(参考上述小试牛刀:使用命令行I/O)
1.3.2第一个Rust程序
作为第一个 Rust 程序,我们可能还会想编写代码,用于输出以下文本信息:
Hello, world!
GrüB Gott!
ハロ-・リ-ルド
在小北的 Rust 之旅中,友友们应该已经见过此输出内容的第一行了。另外的两行是为了展示出Rust的以下特点:易用的选代和内置对Unicode的支持。与1.3.1节中的程序一样,在这个
程序中我们会使用 cargo。具体步骤如下。
(1)打开控制台窗口。
(2)如果是在 Windows上,就执行 cd%TMP%;如果在其他操作系统上,就执行 cd $TMP;
(3)执行 cargo new hello2 命令,创建一个新项目;
(4)执行cd hello2命令,移动到此项目的根目录中;
(5)在一个文本编辑器中打开 src/main.rs 文件;
(6)用以下的的内容替换该文件中的文本:
fn greet_world(){println!("Hello,world!");let southern_germany="GrüB Gott!";let japan ="ハロ-・フ-ルド";let regions =[southern_germany,japan];for region in regions.iter(){println!("{}", ®ion);}
}fn main(){great_world();
}
1.3.3认识Cargo
但凡经历过 C/C++ 或 Go 语言 1.10 版本之前的用户都知道,一个好的包管理工具有多么的重要!!我那个时候是如此的渴望类似
nodejs
的npm
包管理工具,但是却求而不得。包管理工具最重要的意义就是任何用户拿到你的代码,都能运行起来,而不会因为各种包版本依赖焦头烂额。
Go 语言在 1.10 版本之前,所有的包都是在
github.com
下存放,导致了所有的项目都公用一套依赖代码,在本地项目复杂后,这简直是一种灾难。说多了都是泪/(ㄒoㄒ)/~~,小北目前还有一个早期 Go 的项目(22年写的),用到了
iris
(一个坑爹的HTTP 服务),结果现在运行不起来了,因为找不到iris
当时的那个版本了!!!作为一门现代化语言,Rust 吸收了多个语言的包管理优点,为大家提供超级大杀器: cargo,真的,再挑剔的开发者,都对她赞不绝口。👍
总而言之,cargo提供了一系列的工具,从项目的建立、构建到测试、运行直至部署,为 Rust 项目的管理提供尽可能完整的手段。同时,与 Rust 语言及其编译器
rustc
紧密结合,可以说用了后就忘不掉,如同初恋般的感觉~
创建一个”你好,世界“项目
又见"你好,世界",肯定有友友在批评了:小北你就不能有点创意吗?"世界,你好"难道不配?😔╥﹏╥...你是读者,你说了算,那我们就来创建一个"世界,你好"。
上文提到,Rust 语言的包管理工具是 cargo。不过,我们无需再手动安装,之前安装 Rust 的时候,就已经一并安装了。
终于到了紧张刺激的( ఠൠఠ )ノ new 🐂new 🐂new 环节:
cargo new world_hello
cd world_hello
上面的命令使用 cargo new
创建一个项目,项目名是 world_hello
(向友友们低头的项目名称,泪奔),该项目的结构和配置文件都是由 cargo 生成,意味着我们的项目被 cargo 所管理。
如果你在终端无法使用这个命令,考虑一下 环境变量 是否正确的设置:把cargo 可执行文件所在的目录添加到环境变量中。
如果是在 Windows 的 WSL2 子系统下,出现以下错误:
error: command failed: 'rustc'
error: caused by: Permission denied (os error 13)
可尝试先卸载,再使用
sudo
命令进行安装:$ sudo curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
早期的 cargo在创建项目时,必须添加 --bin
的参数,如下所示: 现在的版本,已经无需此参数,cargo默认就创建 bin
类型的项目,顺便说一句, Rust项目主要分为两个类型:bin
和 lib
,前者是一个可运行的项目,后者是一个依赖库项目。
下面来看看创建的项目结构:
tree
.
├── .git
├── .gitignore
├── Cargo.toml
└── src└── main.rs
是的,连 git
都给你创建了,不禁令人感叹,“不是女儿,胜似女儿,比小棉袄还体贴。”
运行项目:
有两种方式可以运行项目:
-
cargo
run
-
手动编译和运行项目
首先来看看第一种方式,一码胜似千言,在之前创建的 world_hello
目录下运行:
cargo runCompiling world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello)Finished dev [unoptimized + debuginfo] target(s) in 0.43sRunning `target/debug/world_hello`
Hello, world!
好了,友友你已经看到程序的输出:"Hello, world"
。
如果你安装的 Rust 的 host triple
是 x86_64-pc-windows-msvc
并确认Rust 已经正确安装,但在终端上运行上述命令时,出现类似如下的错误摘要 linking with `link.exe` failed: exit code: 1181
,请使用 Visual Studio Installer 安装 Windows SDK
。
可能有的友友不愿意了,说好了"世界,你好"呢?别急,在下一节,小北再对代码进行修改。(认真想来,"你好,世界“强调的是我对世界说你好,而"世界,你好“是世界对我说你好,明显是后者更有包容性和国际范儿,友友们真·好眼光~)
上述代码,cargo run
首先对项目进行编译,然后再运行,因此它实际上等同于运行了两个指令,下面我们手动试一下编译和运行项目:
编译:
cargo buildFinished 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 checkChecking 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
中记录了项目的描述信息,典型的如下:
Name
字段定义了项目名称,Version
字段定义当前版本...
定义项目
使用 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 的项目组织结构
前文小北有提到 cargo默认生成的项目结构,真实的项目肯定会有所不同,但是在目前的学习阶段,还无需关注。
至此,友友们对 Rust 项目的创建和管理已经有了初步的了解,那么来完善刚才的"世界,你好"
项目吧~
不仅仅是Hello,world!
几乎所有语言编程教程中安装的最后一个环节都是 hello world
,友友我们也不能免俗。但是,在 hello world
之后,还有一个相亲,啊呸,Rust 初印象环节,希望大家喜欢。
多国语言的“世界,你好”
还记得大明湖畔等你的 VS Code IDE和通过 Cargo 创建的"世界,你好"工程吗?
现在使用 VSCode 打开之前创建的 world_hello
工程,然后进入 main.rs
文件。(此文件是当前 Rust 工程的入口文件,和其它语言几无区别。)
接下来,友友们请和小北一起对世界友人给予热切的问候:
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!("{}", ®ion);}
}fn main() {greet_world();
}
打开终端,进入 world_hello
工程根目录,运行该程序。(友友你也可以在 VSCode 中打开终端,方法是点击 VSCode 上方菜单栏中的终端->新建终端,或者直接使用快捷键打开。)
为友友们献上一首小区:" 你的热情,就像一把火,燃烧了整个世界~ "花点时间来看看上面的代码:
cargo runCompiling world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello)Finished dev [unoptimized + debuginfo] target(s) in 0.21sRunning `target/debug/world_hello`
Grüß Gott!
世界,你好
World, hello
首先,Rust 原生支持 UTF-8 编码的字符串,这意味着你可以很容易的使用世界各国文字作为字符串内容。
其次,关注下 println
后面的 !
,如果你有 Ruby 编程经验,那么你可能会认为这是解构操作符,但是在 Rust 中,这是 宏
操作符,你目前可以认为宏是一种特殊类型函数。
对于 println
来说,我们没有使用其它语言惯用的 %s
、%d
来做输出占位符,而是使用 {}
,因为 Rust 在底层帮我们做了大量工作,会自动识别输出数据的类型,例如当前例子,会识别为 String
类型。
最后,和其它语言不同,Rust 的集合类型不能直接进行循环,需要变成迭代器(这里是通过 .iter()
方法),才能用于迭代循环。在目前来看,你会觉得这一点好像挺麻烦,不急,以后就知道这么做的好处所在。
实际上这段代码可以简写,在 2021 edition 及以后,支持直接写
for region in regions
,原因会在于Rust的迭代器,是因为 for 隐式地将 regions 转换成迭代器。
至于函数声明、调用、数组的使用,和其它语言没什么区别,So easy~
Rust语言初印象
Haskell 和 Java 开发者们可能会觉得Rust 这门语言很熟悉,因为它们在高阶表达方面都很优秀。简而言之,就是可以很简洁的写出原本需要一大堆代码才能表达的含义。但是,Rust 又有所不同:它的性能是底层语言级别的性能,可以跟 C/C++ 相媲美。
上面的 So easy~的余音仍在绕梁,小北希望它能继续下去,可是… 人总是要面对现实,因此让我们来点狠活:
fn main() {let penguin_data = "\common name,length (cm)Little penguin,33Yellow-eyed penguin,65Fiordland penguin,60Invalid,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 是 基于表达式的语言
cargo new greet_world
src/main.rs
报错:在终端中运行上述代码时,会看到很多 debug: ...
的输出,上面有讲,这些都是 条件编译
的输出,那么该怎么消除掉这些输出呢?
结合GPT 和小北一起来Debug
尝试第一次Debug:(error)
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().is_empty() { continue; } let fields: Vec<&str> = record .split(',') .map(|field| field.trim()) .collect(); if cfg!(debug_assertions) { // 输出到标准错误输出 eprintln!("debug: {:?} -> {:?}", record, fields); } if fields.len() < 2 { continue; // 如果字段不足两个,则跳过 } let name = fields[0]; // 尝试把 fields[1] 的值转换为 f32 类型的浮点数 if let Ok(length) = fields[1].parse::<f32>() { // 输出到标准输出 println!("{}, {}cm", name, length); } }
}
分析:
错误信息
error: unknown character escape:
`` 指出在greet_world\src\main.rs
文件的第2行有一个未知的字符转义序列。在这个上下文中,是因为Rust的字符串字面量试图解析一个反斜杠(\
)后面的字符作为转义序列,但是后面并没有一个有效的转义字符。Rust的字符串字面量使用反斜杠
\
作为转义字符,比如\n
表示换行,\t
表示制表符等。但是,如果你只是想在字符串中包含一个普通的反斜杠字符,你需要用两个反斜杠\\
来表示。
第二次Debug:(编译正确)
fn main() { let penguin_data = r#"
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().is_empty() { continue; } let fields: Vec<&str> = record .split(',') .map(|field| field.trim()) .collect(); if cfg!(debug_assertions) { // 输出到标准错误输出 eprintln!("debug: {:?} -> {:?}", record, fields); } if fields.len() < 2 { continue; // 如果字段不足两个,则跳过 } let name = fields[0]; // 尝试把 fields[1] 的值转换为 f32 类型的浮点数 if let Ok(length) = fields[1].parse::<f32>() { // 输出到标准输出 println!("{}, {}cm", name, length); } }
}
在这段代码中,我使用了
r#"..."#
来定义了一个原始字符串字面量,它允许字符串内部包含任意的字符,包括换行符和引号,而不需要使用转义字符。此外,我还添加了对
cfg!(debug_assertions)
的检查,这是一个编译时配置,当在Cargo.
toml
中设置了[profile.dev]
或[profile.test]
的debug_assertions
为true
时,会包含这个检查。在开发模式下,这通常是默认设置。这个检查允许我们在调试模式下输出额外的调试信息,但在发布模式下则不会输出这些信息。现在这段代码应该可以正常编译并运行,输出企鹅的名称和长度。
非常好!现在小北的代码已经成功编译并运行,并且正确地解析了多行字符串数据。每个企鹅的名称和长度都被打印出来了,同时还包括了调试模式下的额外输出。
在GPT的辅助下:
“您的代码中,
fields.split(',')
用于根据逗号分割每一行,并通过map(|field| field.trim())
去除每个字段前后的空白字符。然后,您使用parse::<f32>()
尝试将第二个字段(即长度)解析为浮点数。在
cfg!(debug_assertions)
的条件下,您使用了eprintln!
宏来在标准错误输出中打印调试信息。这是非常有用的,因为当您想要查看程序在运行时的某些内部状态时,它可以提供额外的上下文。最后,请注意,由于
Invalid,data
这一行没有有效的浮点数作为长度,所以parse::<f32>()
会失败,但您的代码已经通过if let Ok(length) = fields[1].parse::<f32>()
的语法正确地处理了这个情况,并跳过了这一行的进一步处理。”
友友们普遍冰雪聪明,肯定已经想到:是的,在之前的1.3.3认识Cargo中,曾经介绍过 --release
参数,因为 cargo run
默认是运行 debug
模式。因此想要消灭那些 debug:
输出,需要更改为其它模式,其中最常用的模式就是 --release
也就是生产发布的模式。
具体运行代码就不给友友们了,留给大家作为一个小练习,建议亲自动手尝试下。
至此,Rust 安装入门就已经结束。相信看到这里,你已经发现了本书与其它书的区别,其中最大的区别就是:小北的这篇博客集合了多篇优秀的国外书籍资料做参考,内容也不太会枯燥,也希望用小北不太枯燥的话术,能伴友友们长行,犹如一杯奶茶,细细品之,唇齿留香~
1.5下载依赖很慢或卡住?
在目前,友友们还不需要自己搭建的镜像下载服务,因此只需知道下载依赖库的地址是:crates.iohttps://crates.io/ 是由 Rust 官方搭建的镜像下载和管理服务。
但悲剧的是,它的默认镜像地址是在国外,这就导致了某些时候难免会遇到下载缓慢或者卡住的情况,下面和小北一起来看看吧~
下载很慢?
作为国外的语言,下载慢是正常的,隔壁的那位还被墙呢:)
解决下载缓慢有 :
开启命令墙或者全局翻墙:
经常有同学反馈,我明明开启翻墙了,但是下载依然还是很慢,无论是命令行中下载还是 VSCode 的 rust-analyzer 插件自动拉取。
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
下载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 1.68 版本开始支持稀疏索引,不再需要完整克隆 crates.io-index 仓库,可以加快获取包的速度,如:
[source.ustc] registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"
- cargo search 无法使用镜像
科大镜像
上面使用的是科大提供的注册服务,也是 Rust 最早期的注册服务,感谢大大们的贡献。除此之外,大家还可以选择下面的镜像服务:
字节跳动
最大的优点就是不限速,当然,如果友友你的的网速如果能跑到 1000Gbps,我们也可以认为它无情的限制了你,咳咳~
[source.crates-io] replace-with = 'rsproxy'[source.rsproxy] registry = "https://rsproxy.cn/crates.io-index"# 稀疏索引,要求 cargo >= 1.68 [source.rsproxy-sparse] registry = "sparse+https://rsproxy.cn/index/"[registries.rsproxy] index = "https://rsproxy.cn/crates.io-index"[net] git-fetch-with-cli = true
覆盖默认的镜像地址
事实上,小北更推荐第二种方式,因为第一种方式在项目大了后,实在是很麻烦,全部修改后,万一以后不用这个镜像了,你又要全部修改成其它的。
而第二种方式,则不需要修改 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 大刀(宽带)终于有了用武之地。
下载卡住
下载卡住其实就一个原因:下载太慢了。
根据经验来看,卡住不动往往发生在更新索引时。毕竟 Rust 的包越来越多,索引也越来越大,如果不使用国内镜像,卡住还蛮正常的,好在,我们也无需经常更新索引 :P
Blocking waiting for file lock on package cache
不过这里有一个坑,需要大家注意,如果你同时打开了 VSCode 和命令行,然后修改了
Cargo.toml
,此时 VSCode 的 rust-analyzer 插件会自动检测到依赖的变更,去下载新的依赖。在 VSCODE 下载的过程中(特别是更新索引,可能会耗时很久),假如你又在命令行中运行类似
cargo run
或者cargo build
的命令,就会提示一行有些看不太懂的内容:cargo buildBlocking waiting for file lock on package cacheBlocking waiting for file lock on package cache
其实这个报错就是因为 VSCode 的下载太慢了,而且该下载构建还锁住了当前的项目,导致你无法在另一个地方再次进行构建。
解决办法也很简单:
- 增加下载速度,见前面内容
- 耐心等待持有锁的用户构建完成
- 强行停止正在构建的进程,例如杀掉 IDE 使用的 rust-analyzer 插件进程,然后删除
$HOME/.cargo/.package_cache
目录
补充:
rust 卸载重新安装 安装
卸载Rust
在之前的命令行窗口中继续输入
rustup self uninstall
问题:error: could not remove 'rustup_home' directory: 'C:\Users\支一郎\.rustup': 拒绝访问。 (os error 5) 原因:rust程序在后台还在运行。解决:任务管理器结束任务。再去卸载
再次安装时,选择默认的 x86_64-pc-windows-msvc,不修改为 x86_64-pc-windows-gnu 了。别的参数可以按照自己的需要去调整。
装完后,再创建hello项目编译运行,成功。
rustup:Rust 工具链管理器,rustup 是 Rust 的工具链管理器,它负责安装、更新和管理 Rust 工具链。
rustc:Rust 编译器,rustc 是 Rust 的编译器,它将 Rust 源代码编译成可执行的机器代码。rustc 遵循 Rust 语言的规则和约定,确保生成的代码既安全又高效。
cargo:Rust 的包管理器和构建工具,cargo 是 Rust 的包管理器和构建工具,它简化了 Rust 项目的创建、依赖管理和构建过程。
rustc cargo rustup 的关系:
rustc是编译器
cargo是用rust写的一个包管理工具(可以直接在http://crates.io上搜到并当作依赖来用) 最后cargo build这种,最后编译还是用的rustc
rustup是工具链管理,安装哪个版本的rust和常用组件
可以类比一下:python pip pyenv
1,安装Visual Studio。2,手动安装的预设条件。3,无需预设条件。
安装Visual Studio也是一个不错的选择官网同感。小北这里选择3,因为需要下载的文件非常小,也是C/C++编译器,就是 GCC 的 Windows 版本 。
接下来打开MinGW-w64 - 适用于 32 位和 64 位 Windows - 浏览 文件 SourceForge.nethttps://sourceforge.net/projects/mingw-w64/files/ 选择x86_64-win32-seh下载并且配置环境变量,解压:环境变量配置:(自拟)
Path写入bin,记得两个%
配置好后别忘了点击 确认!确认!确认!
打开cmd输入gcc -v,假如出现下述的提示,表明安装成功:
gcc -v
浅浅滴测试一下C程序(可有可无)
Hello World 程序 C 语言写法https://www.techug.com/post/6-crazy-hello-world-programs-with-c/
#include<stdio.h>int main()
{/*我的第一个C程序*/printf("Hello,World!\n");return 0;
}
用记事本写一段C的代码;
找到路径:D:\桌面\Rust,再输入gcc hello.c后Enter,虽然没有输出,但是在文件夹里面多了一个.exe文件
输入a.exe这个文件,执行这个文件,输出:
(小结:小北的个人理解,gcc是C/CC++的编译器,但C是通常在 Linux 上才能用。如果要在Windows上用,我们要借助MinGW使用它。在MinGW官网上,可以下载 zip 也可以下载 exe 执行文件,但是 .exe执行文件在安装时,其实是会根据你的选择去下载 zip ,而且网速不好根W本下不下来,会报错,所以我们直接选择对应版本的 zip 文件下载就可以了。如有错误,还请友友们评论区指正!蟹蟹~🙂)
更多库从 crates.io 上搜索:
crates.io: Rust Package Registryhttps://crates.io/
另外一个tauri app 框架,后台也是用rust开发的
还可以使用 tauri 开发应用,后台也是使用 rust 做服务的Build smaller, faster, and more secure desktop applications with a web frontend | Tauri Appshttps://tauri.app/
2、Rust基础入门
从现在开始,友友们和小北正式踏入了 Rust 大陆,这片广袤而神秘的世界,在这个世界中,将接触到很多之前都没有听过的概念:
- 所有权、借用、生命周期
- 宏编程
- 模式匹配
类似的还有很多,不过不用怕,引用武林外传一句话:“咱上面有人”。有小北在,一切虚妄终将烟消云散。
接下来主要介绍 Rust的基础语法、数据类型、项目结构等,和小北一起学完这些,友友们将对 Rust 代码有一个清晰、完整的认识。
开始之前先通过一段代码来简单浏览下Rust 的语法:
// Rust 程序入口函数,跟其它语言一样,都是 main,该函数目前无返回值
fn main() {// 使用let来声明变量,进行绑定,a是不可变的// 此处没有指定a的类型,编译器会默认根据a的值为a推断类型:i32,有符号32位整数// 语句的末尾必须以分号结尾let a = 10;// 主动指定b的类型为i32let b: i32 = 20;// 这里有两点值得注意:// 1. 可以在数值中带上类型:30i32表示数值是30,类型是i32// 2. c是可变的,mut是mutable的缩写let mut c = 30i32;// 还能在数值和类型中间添加一个下划线,让可读性更好let d = 30_i32;// 跟其它语言一样,可以使用一个函数的返回值来作为另一个函数的参数let e = add(add(a, b), add(c, d));// println!是宏调用,看起来像是函数但是它返回的是宏定义的代码块// 该函数将指定的格式化字符串输出到标准输出中(控制台)// {}是占位符,在具体执行过程中,会把e的值代入进来println!("( a + b ) + ( c + d ) = {}", e);
}// 定义一个函数,输入两个i32类型的32位有符号整数,返回它们的和
fn add(i: i32, j: i32) -> i32 {// 返回相加值,这里可以省略returni + j
}
注意: 在上面的
add
函数中,不要为i+j
添加;
,这会改变语法导致函数返回()
而不是i32
,具体参见Rust的语句和表达式。
有几点可以留意下:
- 字符串使用双引号
""
而不是单引号''
,Rust 中单引号是留给单个字符类型(char
)使用的 - Rust 使用
{}
来作为格式化输出占位符,其它语言可能使用的是%s
,%d
,%p
等,由于println!
会自动推导出具体的类型,因此无需手动指定。
2.1变量绑定与解构
鉴于小北的友友们(别慌,能够坚持到这里来到就说明友友你就是小北的目标读者)已经熟练掌握其它任意一门编程语言,因此这里就不再对何为变量进行赘述,让我们开门见山来谈谈,为何 Rust 选择了手动设定变量的可变性。
为何要手动设置变量的可变性
在其它大多数语言中,要么只支持声明可变的变量,要么只支持声明不可变的变量( 例如函数式语言 ),前者为编程提供了灵活性,后者为编程提供了安全性,而 Rust 比较野,选择了两者我都要,既要灵活性又要安全性。
能想要学习 Rust ,说明小北的友友们都是相当有水平的程序员了,你们应该能理解一切选择皆是权衡,那么两者都要的权衡是什么呢?这就是 Rust 开发团队为我们做出的贡献,两者都要意味着Rust 语言底层代码的实现复杂度大幅提升,因此 Salute to The Rust Team!
除了以上两个优点,还有一个很大的优点,那就是运行性能上的提升,因为将本身无需改变的变量声明为不可变在运行期会避免一些多余的 runtime
检查。
变量命名
在命名方面,和其它语言没有区别,不过当给变量命名时,需要遵循Rust 命名规范。
Rust 语言有一些关键字(keywords),和其他语言一样,这些关键字都是被保留给 Rust 语言使用的,因此,它们不能被用作变量或函数的名称。在 小北的这篇博客末尾附录中可找到关键字列表。
变量绑定
在其它语言中,我们用 var a = "hello world"
的方式给 a
赋值,也就是把等式右边的 "hello world"
字符串赋值给变量 a
,而在 Rust 中,我们这样写: let a = "hello world"
,同时给这个过程起了另一个名字:变量绑定。
为何不用赋值而用绑定呢(其实你也可以称之为赋值,但是绑定的含义更清晰准确)?这里就涉及 Rust 最核心的原则——所有权,简单来讲,任何内存对象都是有主人的,而且一般情况下完全属于它的主人,绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人(聪明的读者应该能猜到,在这种情况下,该对象之前的主人就会丧失对该对象的所有权),像极了我们的现实世界,不是吗?
那为什么要引进“所有权”这个新的概念呢?请稍安勿躁,时机一旦成熟,我们就回来继续讨论这个话题。
变量可变性
Rust 的变量在默认情况下是不可变的。前文提到,这是Rust 团队为我们精心设计的语言特性之一,让我们编写的代码更安全,性能也更好。当然你可以通过 mut
关键字让变量变为可变的,让设计更灵活。
使用下划线开头忽略未使用的变量
如果友友你创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为这可能会是个 BUG。但是有时创建一个不会被使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头,更多关于 _x
的使用信息,请阅读后面的2.2模式匹配。
变量解构
let
表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容:
fn main() {let (a, mut b): (bool,bool) = (true, false);// a = true,不可变; b = false,可变println!("a = {:?}, b = {:?}", a, b);b = true;assert_eq!(a, b);
}
解构式赋值
在Rust1.59版本后,我们可以在赋值语句的左式中使用元组、切片和结构体模式了。
struct Struct {e: i32
}fn main() {let (a, b, c, d, e);(a, b) = (1, 2);// _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _[c, .., d, _] = [1, 2, 3, 4, 5];Struct { e, .. } = Struct { e: 5 };assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}
这种使用方式跟之前的 let
保持了一致性,但是 let
会重新绑定,而这里仅仅是对之前绑定的变量进行再赋值。
需要注意的是,使用 +=
的赋值语句还不支持解构式赋值。
这里用到了模式匹配的一些语法,如果友友们看不懂没关系,可以在学完2.2模式匹配后,再回头来看。
变量和常量之间的差异
变量的值不能更改可能让你想起其他另一个很多语言都有的编程概念:常量(constant)。与不可变变量一样,常量也是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异:
- 常量不允许使用
mut
。常量不仅仅默认不可变,而且自始至终不可变,因为常量在编译完成后,已经确定它的值。 - 常量使用
const
关键字而不是let
关键字来声明,并且值的类型必须标注。
小北将在数据类型中介绍,因此现在暂时无需关心细节。
下面是一个常量声明的例子,其常量名为 MAX_POINTS
,值设置为 100,000
。(Rust 常量的命名约定是全部字母都使用大写,并使用下划线分隔单词,另外对数字字面量可插入下划线以提高可读性):
const MAX_POINTS: u32 = 100_000;
常量可以在任意作用域内声明,包括全局作用域,在声明的作用域内,常量在程序运行的整个过程中都有效。对于需要在多处代码共享一个不可变的值时非常有用,例如游戏中允许玩家赚取的最大点数或光速。
在实际使用中,最好将程序中用到的硬编码值都声明为常量,对于代码后续的维护有莫大的帮助。如果将来需要更改硬编码的值,友友们也只需要在代码中更改一处即可。
变量遮蔽(shadowing)
Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的,如下所示:
fn main() {let x = 5;// 在main函数的作用域内对之前的x进行遮蔽let x = x + 1;{// 在当前的花括号作用域内,对之前的x进行遮蔽let x = x * 2;println!("The value of x in the inner scope is: {}", x);}println!("The value of x is: {}", x);
}
这个程序首先将数值 5 绑定到 x,然后通过重复使用 let x = 来遮蔽之前的 x,并取原来的值加上 1,所以 x 的值变成了 6。第三个 let 语句同样遮蔽前面的 x,取之前的值并乘上 2,得到的 x 最终值为 12。当运行此程序,将输出以下内容:$ cargo runCompiling variables v0.1.0 (file:///projects/variables)... The value of x in the inner scope is: 12 The value of x is: 6
这和
mut
变量的使用是不同的,第二个let
生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配 ,而mut
声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。变量遮蔽的用处在于,如果友友在某个作用域内无需再使用之前的变量(在被遮蔽后,无法再访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。
附录:
A关键字
下面的列表包含Rust 中正在使用或者以后会用到的关键字。因此,这些关键字不能被用作标识符(除了原生标识符————在下面),包括函数、变量、参数、结构体字段、模块、包、常量、宏、静态值、属性、类型、特征或生命周期。
目前正在使用的关键字
如下关键字目前有对应其描述的功能。
as
- 强制类型转换,或use
和extern crate
包和模块引入语句中的重命名break
- 立刻退出循环const
- 定义常量或原生常量指针(constant raw pointer)continue
- 继续进入下一次循环迭代crate
- 链接外部包dyn
- 动态分发特征对象else
- 作为if
和if let
控制流结构的 fallbackenum
- 定义一个枚举类型extern
- 链接一个外部包,或者一个宏变量(该变量定义在另外一个包中)false
- 布尔值false
fn
- 定义一个函数或 函数指针类型 (function pointer type)for
- 遍历一个迭代器或实现一个 trait 或者指定一个更高级的生命周期if
- 基于条件表达式的结果来执行相应的分支impl
- 为结构体或者特征实现具体功能in
-for
循环语法的一部分let
- 绑定一个变量loop
- 无条件循环match
- 模式匹配mod
- 定义一个模块move
- 使闭包获取其所捕获项的所有权mut
- 在引用、裸指针或模式绑定中使用,表明变量是可变的pub
- 表示结构体字段、impl
块或模块的公共可见性ref
- 通过引用绑定return
- 从函数中返回Self
- 实现特征类型的类型别名self
- 表示方法本身或当前模块static
- 表示全局变量或在整个程序执行期间保持其生命周期struct
- 定义一个结构体super
- 表示当前模块的父模块trait
- 定义一个特征true
- 布尔值true
type
- 定义一个类型别名或关联类型unsafe
- 表示不安全的代码、函数、特征或实现use
- 在当前代码范围内(模块或者花括号对)引入外部的包、模块等where
- 表示一个约束类型的从句while
- 基于一个表达式的结果判断是否继续循环
保留做将来使用的关键字
如下关键字没有任何功能,不过由 Rust 保留以备将来的应用。
abstract
async
await
become
box
do
final
macro
override
priv
try
typeof
unsized
virtual
yield
原生标识符(Raw identifiers)允许你使用通常不能使用的关键字,其带有 r#
前缀。
例如,match
是关键字。如果尝试编译如下使用 match
作为名字的函数:
fn match(needle: &str, haystack: &str) -> bool {haystack.contains(needle)
}
会得到这个错误:
error: expected identifier, found keyword `match`--> src/main.rs:4:4|
4 | fn match(needle: &str, haystack: &str) -> bool {| ^^^^^ expected identifier, found keyword
该错误表示你不能将关键字 match
用作函数标识符。你可以使用原生标识符将 match
作为函数名称使用:
文件名: src/main.rs
fn r#match(needle: &str, haystack: &str) -> bool {haystack.contains(needle)
}fn main() {assert!(r#match("foo", "foobar"));
}
此代码编译没有任何错误。注意 r#
前缀需同时用于函数名定义和 main
函数中的调用。
原生标识符允许使用你选择的任何单词作为标识符,即使该单词恰好是保留关键字。 此外,原生标识符允许你使用其它Rust 版本编写的库。比如,try
在 Rust 2015 edition 中不是关键字,却在 Rust 2018 edition 是关键字。所以如果用 2015 edition 编写的库中带有 try
函数,在 2018 edition 中调用时就需要使用原始标识符语法,在这里是 r#try
。