Rust作为一门快速发展的编程语言,已经在很多知名项目中使用,如firecracker、libra、tikv,包括Windows和Linux都在考虑Rust【1】。其中很重要的因素便是它的安全性和性能,这方面特性使Rust非常适合于系统编程。
团队近期的一个新项目对于“资源占用”、“安全稳定”有较严格的要求,因此团队调研并最终采用了Rust作为该项目的编程语言。
本文将演示一些很常见的编译器报错,这些信息对于Rust初学者似乎有些“不可理喻”,但当你熟悉之后再回头看,原来一切是这么理所应当。
有一种夸张的说法:“if the code compiles, it works.”【2】,反映了Rust将更多Bug发现于编译阶段的能力。下面让我们一起领略。
fn immutable_var() {
let x = 42;
x = 65;
}
这段代码在多数编程语言是再正常不过的,但在Rust,你会看到如下编译错误:
error[E0384]: cannot assign twice to immutable variable `x`
--> src\main.rs:7:5
|
6 | let x = 42;
| -
| |
| first assignment to `x`
| help: make this binding mutable: `mut x`
7 | x = 65;
| ^^^^^^ cannot assign twice to immutable variable
编译器的提示已经非常友好,如果你需要一个可变变量,请在声明变量时显式添加mut关键字。
fn only_declare() {
let x: i32;
if true {
x = 42;
} else {
;
}
println!("x: {}", x);
}
如果你的if分支比较多,在某个分支可能忘记给变量赋值,这将会引发一个Bug,而Rust会把这个Bug扼杀在编译阶段:
error[E0381]: borrow of possibly-uninitialized variable: `x`
--> src\main.rs:20:23
|
20 | println!("x: {}", x);
| ^ use of possibly-uninitialized `x`
C++11开始引入move语义,它可以在变量转移时避免内存拷贝,从而提升性能,是C++11的一个重要特性,但是它需要显式使用或在特定场景自动适用。
而Rust更进一步,在非基本类型场景自动适用move语义:
fn move_var() {
let x = String::from("42");
let y = x; //move occurred
println!("x: {:?}", x);
}
x的所有权被move到y中,x将失效,即:不允许再被使用。可以看到如下报错:
error[E0382]: borrow of moved value: `x`
--> src\main.rs:29:25
|
27 | let x = String::from("42");
| - move occurs because `x` has type `std::string::String`, which does not implement the `Copy` trait
28 | let y = x; //move occurred
| - value moved here
29 | println!("x: {:?}", x);
| ^ value borrowed here after move
所有权Ownership,这个概念我们在上一小节实际已经开始涉及到,所有权是Rust语言最为独特的一种机制和特性,Rust的“内存安全”很大程度正是依靠所有权机制。
值得注意的是,所有权的所有检查工作,均发生于编译阶段,所以它在运行时没有带来任何额外成本。
让我们回头看上一小节move_var
例子,x在let y = x;之后,x原先的所有权已经转移给y,如果再使用x,就会报使用了一个已经被move走的值。
“42”这个字符串的值,实际是在堆区;x这个String对象内部保存有一个指向“42”的指针。
当move发生时,“42”这个堆区内存没有发生过拷贝,发生变化的只是y的栈指针指向了“42”这个堆地址,因此它是高效快速的。如果堆区内存非常大时,这种move的效率提升会更加明显。
借用Borrow,也就是C++里的引用,但它的默认可变性与C++不一样,这是Rust保守严谨的典型体现。
fn borrow_var() {
let v = vec![1, 2, 4];
immu_borrow(&v);
println!("v[1]:{}", v[1]);
}
fn immu_borrow(v: &Vec<i32>) {
v.pop();
}
上述引用使用方式,在C++是很常见的,我们看看Rust的报错信息:
error[E0596]: cannot borrow `*v` as mutable, as it is behind a `&` reference
--> src\main.rs:40:5
|
39 | fn immu_borrow(v: &Vec<i32>) {
| --------- help: consider changing this to be a mutable reference: `&mut std::vec::Vec<i32>`
40 | v.pop();
| ^ `v` is a `&` reference, so the data it refers to cannot be borrowed as mutable
如果需要可变借用,应该显式使用:&mut,这与变量声明是类似的。
为了避免产生数据竞争,Rust直接在编译阶段禁止了两个可变借用的同时存在(不用担心,并发有其他安全的办法实现),先看这段代码:
fn mut_borrow_var_twice() {
let mut v = vec![1, 2, 4];
let x = &mut v;
v[1] += 42;
(*x)[1] += 42;
println!("v[1]:{}", v[1]);
}
会产生如下报错:
error[E0499]: cannot borrow `v` as mutable more than once at a time
--> src\main.rs:46:5
|
45 | let x = &mut v;
| ------ first mutable borrow occurs here
46 | v[1] += 42;
| ^ second mutable borrow occurs here
47 | (*x)[1] += 42;
| ---- first borrow later used here
x是第一个可变借用,v是第二个可变借用,两个发生了交叉,编译器出于“担心你没有意识到代码交叉使用可变借用”,报出该错误。因为46行改值可能影响你原先对47行及其后的预期。
事实上,如果可变借用不是交叉,编译器会放行,比如:交换46、47行的两次借用。具体可以自行编译试一下。
下面将展示Rust更严格的一面,不仅不能同时出现两个不可变借用,可变与不可变借用也不能交叉出现,本质还是编译器“担心程序员没有注意到发生了交叉使用”,从而潜在产生Bug。
fn mut_immut_borrow_var() {
let mut v = vec![1, 2, 4];
let x = &v;
v[1] += 42;
println!("v[1]:{}", x[1]);
}
报错如下:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src\main.rs:54:5
|
53 | let x = &v;
| -- immutable borrow occurs here
54 | v[1] += 42;
| ^ mutable borrow occurs here
55 | println!("v[1]:{}", x[1]);
| - immutable borrow later used here
同上一小节一样,如果交换53、54行,编译器会放行。
前面两节介绍了编译器对于同时有两个借用的合法性检查,现在我们看一个同时有两个可变借用,但编译器无法覆盖的情况。【5】
fn two_mut_ref_compile_ok() {
let mut v = vec![1, 2, 3];
mut1(&mut v);
mut2(&mut v);
}
fn mut1(v: &mut Vec<i32>) {
*v = vec![0];
}
fn mut2(v: &mut Vec<i32>) {
println!("{}", v[1]);
}
这段代码编译是ok的,但是执行的话会报:thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src\main.rs:96:20。即数组索引越界,由此可见:可变借用的检查范围仅限于同一作用域内。
引用失效会产生类似“悬空指针”的效果,在C++里是undefined behavior,而Rust会把这种问题拦截在编译阶段:
fn dangle_ref() {
let x: &i32;
{
let y = 42;
x = &y;
}
println!("x:{}", x);
}
严谨如Rust,它发现了你在使用一个悬垂引用,报错如下:
error[E0597]: `y` does not live long enough
--> src\main.rs:62:13
|
62 | x = &y;
| ^^ borrowed value does not live long enough
63 | }
| - `y` dropped here while still borrowed
64 | println!("x:{}", x);
| - borrow later used here
如果你把64行注释掉,即:不在失效后继续使用失效引用,则编译器予以放行。
到这里其实已经涉及到“生命周期lifetime”的概念,这是Rust又一特色特性,在其他语言里也有类似生命周期、作用域的概念,但是Rust的生命周期更加高级、复杂,却也让Rust更加安全、保守,本文作为一篇入门暂不深入涉及它。
C++对程序员没有限制,一个指针可以指向任何地方,当你对一个野指针解引用,在C++会产生undefined behavior,而Rust不建议这样的事情发生:
fn invalid_mem_use() {
let x = 42 as *mut i32;
*x = 65;
}
报错如下:
error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block
--> src\main.rs:69:5
|
69 | *x = 65;
| ^^^^^^^ dereference of raw pointer
|
= note: raw pointers may be NULL, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior
报错提示使用unsafe函数包裹这段代码,这里涉及到“unsafe”的概念。
由于Rust默认是保守的,如果在部分场景下程序员能够对代码负责,而Rust无法确认该代码是否安全,这时可以用unsafe关键字包住这段代码,提示编译器这里可以对部分检查进行放行。
但是unsafe并不代表这段代码不安全或存在内存问题【3】,unsafe一个常见的使用场景是通过libc进行系统调用。
空指针的发明者对于这个发明无比懊悔【4】,Rust没有历史包袱,它没有空指针。但是Rust依靠枚举和类型检查,提供了一个安全的空指针功能。先来看Rust标准库提供的这个名为Option的类型:
enum Option<T> {
None,
Some(T),
}
T是模板类型,Option可以是None或Some二选一,如果是Some的话可以带一个T类型的值。
即None代表空,Some代表非空,值是T。
比如你有一个A类型,你不直接操作A的对象a,你操作的是Option<A>类型的对象x。
如果你想调用a.f(),你必须先判断x是一个None还是Some,在Some分支内才可以拿到a去操作a.f()。而这一切都在Rust编译器的检视能力之内。任何能通过编译的代码,都没有机会在None上调用f()。
struct A {}
impl A {
fn f(&self) {}
}
fn safe_null() {
let x: Option<A> = None;
match x {
Some(a) => a.f(),
None => (),
}
}
如此巧妙地避开了空指针问题!
1,Rust不会因为你永远没有调用到某些代码,而不去对其中的代码进行编译检查。比如本文的所有实例,都没有被main调用,却被进行了编译检查。
2,使用他人提供的库时,认值阅读函数原型,根据第一个入参是&self、&mut self还是self来决定你的使用方式,self意味着move语义。
如果你不注意,一定会遇见一个编译报错,不要慌,按照”编译器驱动“的开发模式来即可,编译器多数时候甚至会提示你正确的写法是什么。
3,Rust还有智能指针、channel、trait、包管理、闭包、协程等现代化编程语言标配功能,逐个学习,祝你早日打开新世界的大门!
【1】https://www.oschina.net/news/109553/rust-for-linux-kernel
【3】https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#unsafe-rust
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。