首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Rust入门之严谨如你

Rust入门之严谨如你

原创
作者头像
Radar3
修改于 2020-11-25 12:29:28
修改于 2020-11-25 12:29:28
1.9K20
代码可运行
举报
文章被收录于专栏:巫山跬步巫山跬步
运行总次数:0
代码可运行

1,简介

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

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

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

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

2,变量声明与使用

2.1,默认不可变

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn immutable_var() {
    let x = 42;
    x = 65;
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
fn only_declare() {
    let x: i32;
    if true {
        x = 42;
    } else {
        ;
    }
    println!("x: {}", x);
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
fn move_var() {
    let x = String::from("42");
    let y = x; //move occurred
    println!("x: {:?}", x);
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
fn mut_immut_borrow_var() {
    let mut v = vec![1, 2, 4];
    let x = &v;
    v[1] += 42;
    println!("v[1]:{}", x[1]);
}

  报错如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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++里是undefined behavior,而Rust会把这种问题拦截在编译阶段:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn dangle_ref() {
    let x: &i32;
    {
        let y = 42;
        x = &y;
    }
    println!("x:{}", x);
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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++会产生undefined behavior,而Rust不建议这样的事情发生:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn invalid_mem_use() {
    let x = 42 as *mut i32;
    *x = 65;
}

  报错如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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.2,空指针

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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 删除。

评论
登录后参与评论
2 条评论
热度
最新
关注Rust,关注R站
关注Rust,关注R站
回复回复点赞举报
Rust这门严谨的语言在云服务领域正有着越来越多的应用。
Rust这门严谨的语言在云服务领域正有着越来越多的应用。
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
Rust 入门 (Rust Rocks)
做区块链的基本几乎没有人不知道 Rust 这门编程语言,它非常受区块链底层开发人员的青睐。说来也奇怪,Rust 起源于 Mazilla,唯一大规模应用就是 Firefox,作为小众语言却在区块链圈子里火了。这其中应该和以太坊的发起人 Govin Wood 创建的 Parity 项目有关,Parity 是一款用 Rust 编写的以太坊客户端。
lambeta
2019/09/24
2.5K0
Rust 让人迷惑的 “借用”
本篇尽量深入浅出,不想学 Rust 的也可以读读,多种语言对比很有很大的收获,Go 再好也不是所有场景通吃^_^
MikeLoveRust
2021/07/16
5120
Rust 让人迷惑的 “借用”
掌握Rust:从零开始的所有权之旅
所有权是 Rust 很有意思的一个语言特性,但对于初学者却是一个比较有挑战的内容。
newbmiao
2023/11/27
3860
掌握Rust:从零开始的所有权之旅
【翻译】Rust生命周期常见误区
我曾经有过的所有这些对生命周期的误解,现在有很多初学者也深陷于此。我用到的术语可能不是标准的,所以下面列了一个表格来解释它们的用意。
MikeLoveRust
2020/07/28
1.7K0
Rust 提升安全性的方式
Rust 1 是 Mozilla 公司开发的编程语言,它在 2010 才开始发布第一个版本,可以说是一个非常年轻的语言了。在提出一个新的编程语言的时候,设计者必须要回答的一个问题是「为什么要设计这样一个编程语言?」。对于 Rust 来说,他的目的就是要在保证安全的基础上不失对底层的控制力。
zhiruili
2021/08/10
1.1K0
🌱 Rust内存管理黑魔法:从入门到"放弃"再到真香
内存管理是程序世界的"隐形战场",而 Rust 用一套所有权系统直接重构规则——没有 GC、没有手动 malloc/free,却能在编译期拦截 90% 的内存错误!
Jimaks
2025/04/27
2920
【Rust精彩blog】Rust 中几个智能指针的异同与使用场景
想必写过 C 的程序员对指针都会有一种复杂的情感,与内存相处的过程中可以说是成也指针,败也指针。一不小心又越界访问了,一不小心又读到了内存里的脏数据,一不小心多线程读写数据又不一致了……我知道讲到这肯定会有人觉得“出这种问题还不是因为你菜”云云,但是有一句话说得好:“自由的代价就是需要时刻保持警惕”。
MikeLoveRust
2020/02/20
2K0
Rust到底值不值得学--Rust对比、特色和理念
其实我一直弄不明白一点,那就是计算机技术的发展,是让这个世界变得简单了,还是变得更复杂了。 当然这只是一个玩笑,可别把这个问题当真。
俺踏月色而来
2019/10/14
2.8K0
聊聊Rust的Cell和RefCell
内部可变性(interior mutability)是Rust用来表示在一个值的外部看起来是不可变的,但是在内部是可变的。这种模式通常用于在拥有不可变引用的同时修改目标数据。
newbmiao
2023/11/27
5970
聊聊Rust的Cell和RefCell
Rust语法入门
Rust 是一种系统级编程语言,它的设计目标是提供高性能、安全性和并发性。Rust 的主要优势包括:
码客说
2023/04/17
1.4K0
Rust学习笔记(4)-Ownership
Ownership是Rust语言所特有的,用于运行时内存管理的一套规则。这是Rust语言的核心特点。
TestOps
2022/04/07
4430
Rust学习笔记(4)-Ownership
一网打尽 Rust 语法
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust及AI应用知识分享」的Coder
前端柒八九
2024/04/30
2310
一网打尽 Rust 语法
rust闭包(Closure)
闭包在现代化的编程语言中普遍存在。闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值。Rust 闭包在形式上借鉴了 Smalltalk 和 Ruby 语言,与函数最大的不同就是它的参数是通过 |parm1| 的形式进行声明,如果是多个参数就 |param1, param2,…|, 下面给出闭包的形式定义:
zy010101
2023/04/27
7580
rust闭包(Closure)
详细解答!从C++转向Rust需要注意哪些问题?
导语 | 在日常开发过程中,若长期使用C++语言,在初次使用Rust的过程中可能会碰到一些问题。本文尝试从C++的角度来说明在使用Rust时需要特别注意的一些地方,特别是其中的思维方式的转变(mind shift)。 一、赋值的move语义 (一)C++ vs Rust C++的赋值操作是copy语义,在不考虑优化的情况下,从语义的角度理解,赋值后内存中的某个对象即变成了两份。修改新的对象并不会对旧对象产生副作用。 ‍ 而Rust对赋值操作有更加精细的控制,以下两条: 对于所有实现了Copy trai
腾讯云开发者
2021/10/15
1.1K0
【Rust学习】05_引用与借用
在这章我们将开始学习Rust的引用和借用,它们是Rust中重要的概念,它们允许我们创建可变引用,以及创建不可变引用。
思索
2024/07/29
1850
【Rust学习】05_引用与借用
第5章 | 共享与可变,应对复杂关系
迄今为止,本书讨论的都是 Rust 如何确保不会有任何引用指向超出作用域的变量。但是还有其他方法可能引入悬空指针。下面是一个简单的例子:
草帽lufei
2024/05/08
2540
第5章 | 共享与可变,应对复杂关系
【Rust学习】06_切片
这一章我们一起来学习下切片类型,通过切片,您可以引用集合中连续的元素序列,而不是整个集合。切片是一种引用,因此它没有所有权。
思索
2024/08/08
1250
【Rust学习】06_切片
Golang的逃逸分析和C以及Rust的此类问题的处理对比
首先回答第2个问题,分配在栈上还是堆上是由编译器决定的,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有超出函数范围,就可以在栈上,反之则必须分配在堆上。
后端云
2023/02/10
6860
Golang的逃逸分析和C以及Rust的此类问题的处理对比
【Rust学习】17_常见集合_向量
Rust的标准库包含许多非常有用的数据结构,称为集合。大多数其他数据类型代表一个特定的值,但集合可以包含多个值。与内置的数组和元组类型不同,这些集合指向的数据存储在堆上,这意味着数据的数量不需要在编译时知道,并且可以在程序运行时增长或缩小。每种集合都有不同的能力和成本,选择适合当前情况的集合是您会随着时间推移而发展的一项技能。在本章中,我们将讨论 Rust 程序中经常使用的三个集合:
思索
2024/11/22
2150
【Rust学习】17_常见集合_向量
Rust入坑指南:智能指针
在了解了Rust中的所有权、所有权借用、生命周期这些概念后,相信各位坑友对Rust已经有了比较深刻的认识了,今天又是一个连环坑,我们一起来把智能指针刨出来,一探究竟。
Jackeyzhe
2020/03/12
9550
相关推荐
Rust 入门 (Rust Rocks)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档