前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust入门之严谨如你

Rust入门之严谨如你

原创
作者头像
Radar3
修改2020-11-25 20:29:28
1.7K1
修改2020-11-25 20:29:28
举报
文章被收录于专栏:巫山跬步巫山跬步

1,简介

Rust作为一门快速发展的编程语言,已经在很多知名项目中使用,如firecracker、libra、tikv,包括Windows和Linux都在考虑Rust【1】。其中很重要的因素便是它的安全性和性能,这方面特性使Rust非常适合于系统编程。

团队近期的一个新项目对于“资源占用”、“安全稳定”有较严格的要求,因此团队调研并最终采用了Rust作为该项目的编程语言。

本文将演示一些很常见的编译器报错,这些信息对于Rust初学者似乎有些“不可理喻”,但当你熟悉之后再回头看,原来一切是这么理所应当。

有一种夸张的说法:“if the code compiles, it works.”【2】,反映了Rust将更多Bug发现于编译阶段的能力。下面让我们一起领略。

2,变量声明与使用

2.1,默认不可变

代码语言:javascript
复制
fn immutable_var() {
    let x = 42;
    x = 65;
}

? ?这段代码在多数编程语言是再正常不过的,但在Rust,你会看到如下编译错误:

代码语言:javascript
复制
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关键字。

2.2,使用之前必须初始化

代码语言:javascript
复制
fn only_declare() {
    let x: i32;
    if true {
        x = 42;
    } else {
        ;
    }
    println!("x: {}", x);
}

? ?如果你的if分支比较多,在某个分支可能忘记给变量赋值,这将会引发一个Bug,而Rust会把这个Bug扼杀在编译阶段:

代码语言:javascript
复制
error[E0381]: borrow of possibly-uninitialized variable: `x`
  --> src\main.rs:20:23
   |
20 |     println!("x: {}", x);
   |                       ^ use of possibly-uninitialized `x`

2.3,默认move语义

C++11开始引入move语义,它可以在变量转移时避免内存拷贝,从而提升性能,是C++11的一个重要特性,但是它需要显式使用或在特定场景自动适用。

而Rust更进一步,在非基本类型场景自动适用move语义:

代码语言:javascript
复制
fn move_var() {
    let x = String::from("42");
    let y = x; //move occurred
    println!("x: {:?}", x);
}

? x的所有权被move到y中,x将失效,即:不允许再被使用。可以看到如下报错:

代码语言:javascript
复制
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

?3,所有权

所有权Ownership,这个概念我们在上一小节实际已经开始涉及到,所有权是Rust语言最为独特的一种机制和特性,Rust的“内存安全”很大程度正是依靠所有权机制。

值得注意的是,所有权的所有检查工作,均发生于编译阶段,所以它在运行时没有带来任何额外成本。

3.1,use of moved value

让我们回头看上一小节move_var例子,x在let y = x;之后,x原先的所有权已经转移给y,如果再使用x,就会报使用了一个已经被move走的值。

“42”这个字符串的值,实际是在堆区;x这个String对象内部保存有一个指向“42”的指针。

当move发生时,“42”这个堆区内存没有发生过拷贝,发生变化的只是y的栈指针指向了“42”这个堆地址,因此它是高效快速的。如果堆区内存非常大时,这种move的效率提升会更加明显。

3.2,借用默认不可变

借用Borrow,也就是C++里的引用,但它的默认可变性与C++不一样,这是Rust保守严谨的典型体现。

代码语言:javascript
复制
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的报错信息:

代码语言:javascript
复制
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,这与变量声明是类似的。

3.3,不能同时有两个可变借用

为了避免产生数据竞争,Rust直接在编译阶段禁止了两个可变借用的同时存在(不用担心,并发有其他安全的办法实现),先看这段代码:

代码语言:javascript
复制
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]);
}

? 会产生如下报错:

代码语言:javascript
复制
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行的两次借用。具体可以自行编译试一下。

3.4,不能同时有可变借用与不可变借用

下面将展示Rust更严格的一面,不仅不能同时出现两个不可变借用,可变与不可变借用也不能交叉出现,本质还是编译器“担心程序员没有注意到发生了交叉使用”,从而潜在产生Bug。

代码语言:javascript
复制
fn mut_immut_borrow_var() {
    let mut v = vec![1, 2, 4];
    let x = &v;
    v[1] += 42;
    println!("v[1]:{}", x[1]);
}

? 报错如下:

代码语言:javascript
复制
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行,编译器会放行。

3.5,严谨性不能覆盖的一面

前面两节介绍了编译器对于同时有两个借用的合法性检查,现在我们看一个同时有两个可变借用,但编译器无法覆盖的情况。【5】

代码语言:javascript
复制
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。即数组索引越界,由此可见:可变借用的检查范围仅限于同一作用域内。

3.6,借用的有效性

引用失效会产生类似“悬空指针”的效果,在C++里是?developer/article/1752859/undefined behavior,而Rust会把这种问题拦截在编译阶段:

代码语言:javascript
复制
fn dangle_ref() {
    let x: &i32;
    {
        let y = 42;
        x = &y;
    }
    println!("x:{}", x);
}

? 严谨如Rust,它发现了你在使用一个悬垂引用,报错如下:

代码语言:javascript
复制
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更加安全、保守,本文作为一篇入门暂不深入涉及它。

4,内存安全

4.1,非法内存使用

C++对程序员没有限制,一个指针可以指向任何地方,当你对一个野指针解引用,在C++会产生?developer/article/1752859/undefined behavior,而Rust不建议这样的事情发生:

代码语言:javascript
复制
fn invalid_mem_use() {
    let x = 42 as *mut i32;
    *x = 65;
}

? 报错如下:

代码语言:javascript
复制
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 ?developer/article/1752859/undefined behavior

?报错提示使用unsafe函数包裹这段代码,这里涉及到“unsafe”的概念。

由于Rust默认是保守的,如果在部分场景下程序员能够对代码负责,而Rust无法确认该代码是否安全,这时可以用unsafe关键字包住这段代码,提示编译器这里可以对部分检查进行放行。

但是unsafe并不代表这段代码不安全或存在内存问题【3】,unsafe一个常见的使用场景是通过libc进行系统调用。

4.2,空指针

空指针的发明者对于这个发明无比懊悔【4】,Rust没有历史包袱,它没有空指针。但是Rust依靠枚举和类型检查,提供了一个安全的空指针功能。先来看Rust标准库提供的这个名为Option的类型:

代码语言:javascript
复制
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()。

代码语言:javascript
复制
struct A {}
impl A {
    fn f(&self) {}
}
fn safe_null() {
    let x: Option<A> = None;
    match x {
        Some(a) => a.f(),
        None => (),
    }
}

? 如此巧妙地避开了空指针问题!

5,其他

1,Rust不会因为你永远没有调用到某些代码,而不去对其中的代码进行编译检查。比如本文的所有实例,都没有被main调用,却被进行了编译检查。

2,使用他人提供的库时,认值阅读函数原型,根据第一个入参是&self、&mut self还是self来决定你的使用方式,self意味着move语义。

如果你不注意,一定会遇见一个编译报错,不要慌,按照”编译器驱动“的开发模式来即可,编译器多数时候甚至会提示你正确的写法是什么。

3,Rust还有智能指针、channel、trait、包管理、闭包、协程等现代化编程语言标配功能,逐个学习,祝你早日打开新世界的大门!

6,参考

【1】https://www.oschina.net/news/109553/rust-for-linux-kernel

【2】https://doc.rust-lang.org/book/ch20-02-multithreaded.html?highlight=it,compiles,it,works#building-the-threadpool-struct-using-compiler-driven-development

【3】https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#unsafe-rust

【4】http://mp.163.com/article/FJM5K1UG0511D3QS.html

【5】https://blog.csdn.net/valada/article/details/101570012

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1,简介
  • 2,变量声明与使用
    • 2.1,默认不可变
      • 2.2,使用之前必须初始化
        • 2.3,默认move语义
        • ?3,所有权
          • 3.1,use of moved value
            • 3.2,借用默认不可变
              • 3.3,不能同时有两个可变借用
                • 3.4,不能同时有可变借用与不可变借用
                  • 3.5,严谨性不能覆盖的一面
                    • 3.6,借用的有效性
                    • 4,内存安全
                      • 4.1,非法内存使用
                        • 4.2,空指针
                        • 5,其他
                        • 6,参考
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                        http://www.vxiaotou.com