今天,我们正式进入 Rust 基础的学习。在本文的内容中,我会为大家介绍以下内容:
if、if let、while、while let、break 和 continue
。Rust 是一种新的编程语言,在 2015 年发布了 1.0 版本,我会从以下方面让你知道 Rust 出现的意义:
rustc
使用 LLVM
作为它的编译框架(关于 LLVM
的具体了解,可以访问其官方网址:https://llvm.org/)。我们发现,Rust 有与 c++ 相同的特性:
了解了 Rust 是什么后,就让我们先来体验一番 Rust 最简单的程序:
fn main() {
println!("Hi, I am Rust!");
}
从上面的代码中,我们看到 rust 代码具有如下一些特征:
fn
引入。main
函数是程序的入口点。hygienic macros
),println!
就是它的一个例子。UTF-8
编码的,可以包含任何 Unicode
字符。什么是 卫生宏?卫生宏和普通宏的区别有点类似词法作用域函数和动态作用域函数的区别。比如宏调用处有个名字 name1,同时宏内部也有一个名字 name1,那么卫生宏展开的时候就会把自己内部的 name1 改名成 name2;普通宏则不改名,“捕捉”外部的名字。
为了方便你理解,我在这里再小结一下上面的内容:
Rust 非常像其他遵循 C/ c++ /Java 范式的传统语言。
Rust 是现代的,完全支持 Unicode
之类的东西。
Rust 在需要可变数量的参数(不允许函数重载)的情况下使用宏。
宏是“卫生的”,意味着它们不会意外地从它们所使用的范围中捕获标识符。Rust 宏实际上只是部分卫生的。
Rust 是多范式的。例如,它具有强大的面向对象编程特性,而且,虽然它不是函数式语言,但它包含了一系列函数式概念。
根据上面的小结,你是否也能发现 Rust 的一些独特卖点:
接下来,我会为你从几个方面介绍为什么 Rust 会在众多语言中突出重围。先来一个示例。
首先,我们来看一个 C 语言的例子:
#include
#include
#include
int main(int argc, char* argv[]) {
char *buf, *filename;
FILE *fp;
size_t bytes, len;
struct stat st;
switch (argc) {
case 1:
printf("Too few arguments!\n");
return 1;
case 2:
filename = argv[argc];
stat(filename, &st);
len = st.st_size;
buf = (char*)malloc(len);
if (!buf)
printf("malloc failed!\n", len);
return 1;
fp = fopen(filename, "rb");
bytes = fread(buf, 1, len, fp);
if (bytes = st.st_size)
printf("%s", buf);
else
printf("fread failed!\n");
case 3:
printf("Too many arguments!\n");
return 1;
}
return 0;
}
上面的代码中,你发现了多少 bug?
尽管只有29行代码,但这个 C 语言示例中至少有 11 行包含了严重的错误:
=
而不是相等比较==
(第28行)printf
的多余参数(第23行)if
中忘记使用大括号(第22行)switch
语句中忘记了中断(第32行)buf
字符串的 null
终止,导致缓冲区溢出(第29行)malloc
分配的缓冲区导致内存泄漏(第21行)switch
语句中的情况(第11行)stat
和 fopen
的返回值(第18行和第26行)即使对于 C 编译器,这些错误也不应该很明显吗?
不,令人惊讶的是,即使在最新的GCC版本(撰写本文时为13.2)中,该代码也会在默认警告级别下编译无警告。
这不是一个非常不现实的例子吗?
绝对不是,这类错误在过去会导致严重的安全漏洞。例如:
=
代替相等比较==
: 2003年 Linux 后门尝试漏洞goto fail
漏洞那么,你可能会问 Rust 在这里又能好到哪里去呢?
—— Safe Rust
使所有这些 bug 都不可能出现,例如以下:
Drop
特性在作用域结束时被释放。if
子句都需要大括号。match(在Rust中相当于switch)
不会失败,因此开发者不会不小心忘记了 break
。Drop
特性释放堆分配的内存。panic
,或者可以通过切片的 get
方法进行检查。match
会要求所有 case 都要得到处理。#[must_use]
标记的函数的返回值,编译器会发出警告。编译时的静态内存会进行如下验证:
double-frees
。use-after-free
。NULL
指针。以下行为将会判定为是在运行时无未定义的行为:
整数溢出是通过编译时溢出检查标志定义的。如果启用,程序将陷入奔溃,否则开发者将获得环绕语义。默认情况下,将在调试模式(
cargo build
)和发布模式(cargo build --release
)中获得 panic。
不能使用编译器标志禁用边界检查。它也不能直接使用不安全关键字禁用。但是,不安全允许开发者调用诸如
slice::get_unchecked
之类的函数,这些函数不进行边界检查。
Rust 是用过去几十年积累的所有经验构建起来的,汲取几大语言的精华,又进行了改进。在语言特性上,它具备以下几点:
FFI
。在工具支持上,具备以下几点:
往更细的说,主要是以下几点:
零成本抽象,类似于c++,意味着你不必为使用内存或 CPU 的高级编程结构“付费”。例如,使用 For 编写循环应该产生与使用.iter().fold()
结构大致相同的低级指令。
值得一提的是,Rust 枚举是“代数数据类型”,也被称为“和类型”,它允许类型系统表达像Option
这样的东西。
提醒开发者关注错误——许多开发者已经习惯忽略冗长的编译器输出。Rust 编译器明显比其他编译器更健谈。它通常会为开发者提供可操作的反馈,准备复制粘贴到你的代码中。
与Java、Python和Go等语言相比,Rust 标准库很小。Rust 没有提供一些你可能认为是标准和必要的东西,例如:
Rust 附带了 Cargo
形式的内置包管理器,这使得下载和编译第三方 crate
变得非常简单。这样做的结果是标准库可以更小。https://lib.rs 这个网站可以帮助你找到更多第三方库。
rust-analyzer
对主要的 ide
和文本编辑器实现了支持。大部分 Rust 语法对于 C、c++或Java
来说都很熟悉。例如:
//
开头,块注释以/*…* /
。if
和while
这样的关键词的工作原理是一样的。=
完成,比较用==
完成。类型 | 示例 | |
---|---|---|
有符号整数 | i8, i16, i32, i64, i128, isize | -10, 0, 1_000, 123_i64 |
无符号整数 | u8, u16, u32, u64, u128, usize | 0, 123, 10_u16 |
浮点数 | f32, f64 | 3.14, -10.0e20, 2_f32 |
字符串 | &str | “foo”, “two\nlines” |
Unicode标量值 | char | ‘a’, ‘α’, ‘∞’ |
布尔型 | bool | true, false |
上方表格中,数字中的所有下划线都可以省略,它们只是为了便于阅读。因此,1_000
可以写成1000(或10_00)
,123_i64
可以写成123i64
。
同时,上面的类型的宽度如下:
除此之外,原始字符串允许开发者创建一个转义值,如: r"\n" == "\\n"
。你可以嵌入双引号,在引号的两边加上等量的#:
fn main() {
println!(r#"link"#);
println!("link");
}
字节(Byte)字符串允许你直接创建&[u8]
值:
fn main() {
println!("{:?}", b"abc");
println!("{:?}", &[97, 98, 99]);
}
类型 | 示例 | |
---|---|---|
数组 | [T; N] | [20, 30, 40], [0; 3] |
元组 | (), (T,), (T1, T2), … | (), (‘x’,), (‘x’, 1.2), … |
数组的赋值和访问:
fn main() {
let mut a: [i8; 10] = [42; 10];
a[5] = 0;
println!("a: {a:?}");
}
[T;N]
保存了 N
个(编译时常量)相同类型 t 的元素。注意,数组的长度是其类型的一部分,这意味着 [u8;3]
和 [u8;4]
被认为是两种不同的类型。#
,例如{a:#?}
,可以有“漂亮的输出”格式,这样更容易阅读。元组的赋值和访问:
fn main() {
let t: (i8, bool) = (7, true);
println!("t.0: {}", t.0);
println!("t.1: {}", t.1);
}
与数组一样,元组也有固定的长度。
元组将不同类型的值组合成一个复合类型。
元组的字段可以通过周期和值的索引来访问,例如 t.0, t.1
。
空元组 ()
也被称为“单元类型”。它既是一个类型,又是该类型的唯一有效值——也就是说,该类型及其值都表示为 ()
。例如,它用于表示函数或表达式时没有返回值。
和c++一样,Rust 也有引用类型:
fn main() {
let mut x: i32 = 10;
let ref_x: &mut i32 = &mut x;
*ref_x = 20;
println!("x: {x}");
}
需要注意的是:
ref_x
的引用,类似于 C 和 c++ 指针。ref_x.count_ones()
)。mut
的引用可以在其生命周期内绑定到不同的值。let mut ref_x: &i32
和 let ref_x: &mut i32
之间的区别。第一个表示可以绑定到不同值的可变引用,而第二个表示对可变值的引用。Rust 将静态地禁止悬垂引用:
fn main() {
let ref_x: &i32;
{
let x: i32 = 10;
ref_x = &x;
}
println!("ref_x: {ref_x}");
}
切片为开发者提供了更大集合的视图:
fn main() {
let mut a: [i32; 6] = [10, 20, 30, 40, 50, 60];
println!("a: {a:?}");
let s: &[i32] = &a[2..4];
println!("s: {s:?}");
}
上述代码中,我们通过借用 a 并在括号中指定起始和结束索引来创建切片。
如果切片从索引0开始,Rust的范围语法允许我们删除起始索引,这意味着&a[0.. .len()]
和&a[.. .. len()]
是相同的。
对于最后一个索引也是如此,所以a &a[2.. .len()]
和a &a[2..]
都是一样的。
因此,为了方便地创建整个数组的切片,我们可以使用&a[…]
。
S是对i32s
切片的引用。注意,s (&[i32])
的类型不再提到数组长度。这允许我们对不同大小的切片执行计算。
切片总是从另一个对象借用。在本例中,a 必须保持“活动”(在作用域中)至少与我们的切片一样长。
现在我们可以理解Rust中的两种字符串类型:
fn main() {
let s1: &str = "World";
println!("s1: {s1}");
let mut s2: String = String::from("Hello ");
println!("s2: {s2}");
s2.push_str(s1);
println!("s2: {s2}");
let s3: &str = &s2[6..];
println!("s3: {s3}");
}
&str: 对字符串切片的不可变引用
String: 可变字符串缓冲区
&str
引入了一个字符串切片,它是对存储在内存块中的UTF-8编码字符串数据的不可变引用。字符串字面值(" Hello ")存储在程序的二进制文件中。
Rust 的 String 类型是一个字节向量的包装器。与Vec
一样,它是私有的。
与许多其他类型一样,String::from()
从字符串字面值创建字符串。String::new()
创建一个新的空字符串,可以使用push()
和push_str()
方法向其添加字符串数据。
宏是一种从动态值生成私有字符串的方便方法。它接受与 println!()
相同的格式规范。
你可以通过 &
和可选的范围选择从 String 中借用 &str
切片。
对于c++程序员: 你可以将 &str
看作 c++
中的 const char*
,但它总是指向内存中的有效字符串。Rust String
大致相当于c++中的std:: String
(主要区别:它只能包含UTF-8编码的字节,并且永远不会使用小字符串优化)。
方法是与类型相关联的函数。方法的self参数是它所关联类型的一个实例:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn inc_width(&mut self, delta: u32) {
self.width += delta;
}
}
fn main() {
let mut rect = Rectangle { width: 10, height: 5 };
println!("old area: {}", rect.area());
rect.inc_width(5);
println!("new area: {}", rect.area());
}
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
虽然从技术上讲,Rust没有自定义构造函数,但静态方法通常用于初始化结构。实际的构造函数Rectangle {width, height}
可以直接调用。
添加 Rectangle::square(width: u32)
构造函数来说明此类静态方法可以接受任意参数。
不支持重载:
每个函数有一个单独的实现:
不支持默认值:
然而,函数参数可以是泛型的:
fn pick_one<T>(a: T, b: T) -> T {
if std::process::id() % 2 == 0 { a } else { b }
}
fn main() {
println!("coin toss: {}", pick_one("heads", "tails"));
println!("cash prize: {}", pick_one(500, 1000));
}
当使用泛型时,标准库的Into可以在参数类型上提供一种有限的多态性。这一点我将在后面的小节中介绍更多细节。