Rust语言教程(1) - 一门没有GC的语言

简介: Rust语言是一门没有GC的语言,那么,它是如何管理内存的呢?

缘起

本来这一系列文章并不在计划中。昨天跟赵磊和七哥讨论没有GC管理内存的问题。
讨论到没有GC情况下管理内存的学习曲线,七哥认为学习曲线不陡而是使用曲线陡。诚然,如果只有malloc和free,确实还是学习容易使用难的。到了C++引用了new和delete之后,学习曲线也还算是平和的,因为后面还有auto_ptr, 自动引用计数,右值引用和std::move等一大堆要学习的慢慢地增加进来。多线程的情况下,还会有更复杂一些的问题。
但是这比起Rust语言来,学习曲线都要好一些,因为大不了是memory leak,起码还可以编译通过。而在Rust语言中,初始的这个小陡坡如果踏不过去的话,可能会连编译都编不过。

这就是Rust的设计原则,不希望有一个比较重的运行时,通过编译时的勤奋来减少运行时的麻烦。

Rust语言简介

Rust是Mozilla推出的,希望能够在多核时代充分发挥硬件能力的语言。Rust能够引起大家的关注,最初的原因是它的设计者之一的Brendan Eich是著名的JavaScript语言之父。不管程序员对于JavaScript有多少的诟病,JavaScript的成功是个难以重制的传奇。JavaScript在OOP如日中天的年代将self的原型链和scheme的函数式编程思想结合起来,显示出了Brendan Eich的功力。但是,JavaScript的最初版本的开发时间太短,没有充分给Brendan Eich发挥自己能力的机会。后来,有了历史包袱之后,就不是想怎么改就怎么改的情况了。JavaScript成为ECMA Script之后,4.0版本的被废弃掉,就是这个挫折的表现。
Rust是门新语言,三位主要创造人可以放开手脚去发挥。他们也确实是这么做的,语法不停改来改去。比如Rust最初是支持Go一样的轻量级多任务支持的,后来就被砍掉了,现在就是传统的线程之间像Go一样在来回发消息。
不过,其实Rust最早的创始人是Graydon Hoare. 他在2006年就开始开发了Rust,最早的编译器用OCaml来实现的。2009年,Hoare在Mozilla公司的支持下继续开发Rust。
2015年5月16日,Rust正式发布1.0版本。Hoare虽然离开了Mozilla,但是Rust在Mozilla的支持下在继续发展。

用Rust语言开发的Servo是一个类似Webkit的浏览器引擎。

Rust语言的一个重要特点就是没有一个厚重的运行时,所以也就没有GC这样的运行时的设施。坏处是我们得花精力学习在无GC情况下的设计,就是我们这一讲想做到的事情。好处是,轻量,可以用于系统编程,代码可以跑在裸机上,而不是一套运行时环境上。这样,理论上,Rust编译的代码可以像C和C++语言一样轻,可以用于嵌入式系统级软件开发,比如IoT设备上。做为对比的是Go语言,它有一个运行时,写应用的话Go很可能会胜出,但是在写系统级软件时,这个运行时环境可能就不是最理想的基础设施了。
举个小例子,比如Go和Rust同样来为Java写JNI代码。Rust就可以被当成C/C++用就是了,但是Go就会引入一个很好玩的问题,Java有GC,Go也有GC,这样的JNI代码如何处理二者间引用对象的问题?
Rust和C++之间基本可以无缝调用,反正本质上的思想也差不多。
Rust支持函数式的思想,支持C++所没有的类似于函数语言的宏。Rust支持泛型,支持traits,这是C++程序员所喜欢,而Java这样只是个语法糖方式所无法想象的。

常与Rust和Go一起比较的,还有Apple的swift语言和Digital Mars的D语言。它们的共同特点是高生产力的语法和全编译带来的高效率。

好啦,我们也先来一个Hello,World程序跟Rust语言打个招呼吧.
首先去官网下载Rust的包:https://www.rust-lang.org
在mac上,就用brew install rust就好了。
IDE的话,我用的是Visual Studio Code,大家可以自由选择。

fn main(){
    let s = "Hello,Rust!";
    println!("{}",s);
}

调用rustc,或者是通过cargo,就可以编译运行啦。
可以通过cargo init去创建一个工程,之后用cargo build或者cargo run就好了。
println!是一个输出用的宏,不要忘了后面那个提示它是个宏的叹号哟。

Rust是一门没有GC的语言

好吧,闲话少说,我们开始面对学习一门类似于C++的,没有GC的新型语言的第一道关口,管理内存吧。

Rust同C++一样,存储空间既有堆,也有栈。对象放在栈上,可以复制,也可以随着退栈而被自动销毁掉。在C引入malloc之前,要么是静态分配,要么是在栈上分配,并没有复杂的堆上内存泄漏的难题。而以Java为代表的一类语言,默认都是在堆上分配对象,所以需要GC来实现堆上的管理。

我们也以容器为例来说明这个问题:

    let v = vec![1,2,3];
    println!("{}",v[0]);

vec!宏用于初始化一个vector容器。let叫做绑定,将值绑定在一个变量上。
v变量本身分配在栈里面,而v中的三个元素是分配在堆里面。当函数退出之后,v的作用域结束了,它所引用的堆中的元素也会被自动回收。听起来很不错,至少比malloc和free一定要配对强。

下面问题来了,如果想要将v的值绑定在另一个变量v2上,会出现什么情况呢?
对于有GC的系统来说,这不是问题,v和v2都引用同一个堆中的引用,最终由GC来回收就是了。
而对于没有GC的系统,有三种选择:

  1. 复制:将堆中的数据重制一份,绑定到新的变量上。C++中的栈上的对象的赋值就是这么做的,会调用一个叫做拷贝构造函数的东西来负责这个复制。
  2. 移动:C++11引入的std::move语义就是做这个的,将所有权转移。堆上的值被v2所引用,v的引用失效。
  3. 引用计数:采用自动引用计数的方式,当引用计数为0的时候就释放掉堆上的数据。C+、+11和Boost的shared_ptr就是这种思想哈。

Rust默认是移动语义

C++是默认以复制语义为主的,而Rust默认是移动语义。
在Rust中,如果引用了移动了控制权之后的对象中的元素,会是什么下场呢?答案是,根本编不过。
假如说我们这么写:

    let v = vec![1,2,3];
    let v2 = v;
    println!("{}",v[0]);

编译器就给我们报这样的错误,直接编译失败。
其实编译器给出的错误信息还是蛮友好的哈。

error[E0382]: use of moved value: `v`
 --> src/main.rs:7:16
  |
6 |     let v2 = v;
  |         -- value moved here
7 |     println!("{}",v[0]);
  |                   ^ value used here after move
<std    macros>:2:27: 2:58 note: in this expansion of format_args!
<std    macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
src/main.rs:7:2: 7:22 note: in this expansion of println! (defined in <std macros>)
  |
  = note: move occurs because `v` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait

编译失败告诉我们,除非实现了Copy trait,否则一个类型的默认是移动语义的。

好吧,你认了,默认移动就移动呗。小心啦,我们看看下面这个例子。
我们只是调个无害的函数嘛:

fn take(v: Vec<i32>){
    println!("I did nothing on v!");
}

fn main(){
    let v = vec![1,2,3];
    take(v);
    println!("{}",v[0]);
}

编译结果是什么?

error[E0382]: use of moved value: `v`
 --> src/main.rs:8:16
  |
7 |     take(v);
  |          - value moved here
8 |     println!("{}",v[0]);
  |                   ^ value used here after move
<std    macros>:2:27: 2:58 note: in this expansion of format_args!
<std    macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
src/main.rs:8:2: 8:22 note: in this expansion of println! (defined in <std macros>)
  |
  = note: move occurs because `v` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait

竟然跟上面的绑定到另一个变量上一样,v的所有权已经被别的变量给拿走了。

这可如何是好呢?
其实也没什么神秘的。正如带有GC的语言一般都会引用弱引用一样,这时需要的是一个不引起所有权转移的方式,就是Rust语言的引用。采用与C++类似的符号,在类型前加上&就表示引用啦。
改写成下面的引用的方式,就可以正常编译了:

fn take(v: &Vec<i32>){
    println!("I did nothing on v!");
}

fn main(){
    let v = vec![1,2,3];
    take(&v);
    println!("{}",v[0]);
}

在Rust里面,引用的方式有个新的名字叫做“borrow"-借用。只管用,不管生命周期的销毁。跟弱引用很类似,大家应该能很好的理解。

Rust是不变性优先的语言

Rust语言在设计上,就是为线程安全多做考虑的一门语言。而什么样的数据在多线程下最安全呢?答案是不变的数据。Java世界的并发名著《Java Concurrency in Practice》在讲如何使用丰富的多任务工具之前,先讲了如何做状态封闭。
Rust的设计上也充分认识到了这一点。为什么其他语言是叫变量赋值,而在Rust就强调是变量绑定呢?其重要原因就是这个绑定默认是不变的。
比如我们默认定义个变量绑定,再对这个变量进行操作,会导致,编译不过。
我们试个最简单的例子:

    let a = 1;
    a = a + 2;

编译报下面的错:

  --> src\main.rs:10:5
   |
9  |     let a = 1;
   |         - first assignment to `a`
10 |     a = a + 2;
   |     ^^^^^^^^^ re-assignment of immutable variable

那么如果要定义可以改变的绑定可不可以呢?当然可以,但是在Rust里,这不是默认的行为,需要加mut关键字来说明,我们将上面的例子改写一下,在let后面加上mut:

    let mut a = 1;
    a = a + 2;
    println!("{}",a);

这就是Rust不同于很多其他语言的地方,比如C++和Java,它们是默认是可变的,不可变的要加const和final。而Rust反过来了,不变的是正常的,可变的要加mut。

比如我想要给vector v增加一个值:

    let v = vec![1,2,3];
    v.push(4);

会导致编译失败:

 --> src\main.rs:7:5
  |
6 |     let v = vec![1,2,3];
  |         - use `mut v` here to make mutable
7 |        v.push(4);
  |        ^ cannot borrow mutably

也是需要改成mut的才可以push:

    let mut v = vec![1,2,3];
    v.push(4);

有了默认是不变性的知识,我们再回头来看导致所有权转移的这条语句:

    let v = vec![1,2,3];
    let v2 = v;

实在是有点不人道啊,因为v和v2都不是mut的,都是常量啊。

为什么两个常量还是不可以共享同一份堆数据呢?答案很简单,因为两个不变量的生命周期可能是不一致的,默认的变量绑定可是没有引用计数这样的功能的,它是一直跟一个变量的生命周期所绑定的。

同样,引用默认也是不变的,要想改变引用的值,也需要加mut。
比如下面的还是编译不过的:

fn take(v: &Vec<i32>){
    v.push(4);
}

报同样的错:

error: cannot borrow immutable borrowed content `*v` as mutable
 --> src\main.rs:3:5
  |
3 |     v.push(4);
  |     ^

我们同样要用mut来修饰它,将凡是&出现的地方都换成&mut

fn take(v:  &mut Vec<i32>){
    v.push(4);
}

fn main(){
    let mut v = vec![1,2,3];
    take(&mut v);
    println!("{}",v[3]);
}

输出结果是4,终于大功告成了!

多重借用

下面我们继续思考一个问题,刚才都是只有一个借用者,只有一个引用,那么有多个引用会如何?
比如这样是不是可以?

    let mut v = vec![1,2,3];
    take(&mut v);
    println!("{}",v[3]);

    let v2 = &v;
    let v3 = &v;

是的,上面这些是可以的。

那么既有不变的,又有可变的,可不可以呢?

    let v2 = &v;
    let v3 = &v;
    let mut v4 = &mut v; 

结果是,又报错编不过了:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
  --> src\main.rs:12:23
   |
10 |     let v2 = &v;
   |               - immutable borrow occurs here
11 |     let v3 = &v;
12 |     let mut v4 = &mut v;
   |                       ^ mutable borrow occurs here
13 | }
   | - immutable borrow ends here

这又是Rust对于线程安全的考虑,如果大家都是只读的引用,可以有任意多个。但是只要有一个是可变的,对不起,那就只有这一个可以引用。违反了这条规则,不是运行时出问题,而是编译都不能通过。

那么一段代码中,业务逻辑就是要多于一种可变的引用来操作它怎么办?
解决方案是,分时操作。只要不是同时针对一个变量作引用就好了,我们可以通过作用域的方式将它们进行分时,比如这样做:

    {
        let v2 = &v;
        let v3 = &v;
    }
    {
        let mut v4 = &mut v;
    }

只加了两对括号,顺序上没有任何明影响。但是因为错开了作用域的重叠,就可以顺利编译通过,而且也避免了风险。

但是,要注意,一个长的作用域的引用,想要引用一个生命周期比它短的对象是编译不过的。
比如上面我们是在大作用域内定义对象,子作用域里定义引用,我们反过来,在大定义域里定义引用,引用子作用域里的对象,像这样:

    let vv: &mut Vec<i32>;
    {
        let vv2 = vec![4,5,6];
        vv = &mut vv2;
    }

看起来不错,但是编不过:

  --> src\main.rs:21:19
   |
21 |         vv = &mut vv2;
   |                   ^^^
   |
note: reference must be valid for the block suffix following statement 5 at 18:26...
  --> src\main.rs:18:27
   |
18 |     let vv: &mut Vec<i32>;
   |                           ^
note: ...but borrowed value is only valid for the block suffix following statement 0 at 20:30
  --> src\main.rs:20:31
   |
20 |         let vv2 = vec![4,5,6];
   |                               ^

小结一下

  • Rust没有GC
  • Rust的对象,默认是移动语义。在特殊情况下也支持复制语义。但是不管是移动还是复制,变量之间不能共享同一份堆上的数据
  • 要想共享对象的数据,需要通过引用来借用。同一作用域内同时只能有一个可变的引用。没有可变的引用时,支持多个不变引用共享。
  • Rust的变量绑定默认是不变的,可变的需要加mut关键字说明。引用也是如此。
  • 较大作用域的引用不能引用活得比它短的小作用域内的对象,反之可以。
目录
相关文章
|
19天前
|
Rust 安全 Go
揭秘Rust语言:为何它能让你在编程江湖中,既安全驰骋又高效超车,颠覆你的编程世界观!
【8月更文挑战第31天】Rust 是一门新兴的系统级编程语言,以其卓越的安全性、高性能和强大的并发能力著称。它通过独特的所有权和借用检查机制解决了内存安全问题,使开发者既能享受 C/C++ 的性能,又能避免常见的内存错误。Rust 支持零成本抽象,确保高级抽象不牺牲性能,同时提供模块化和并发编程支持,适用于系统应用、嵌入式设备及网络服务等多种场景。从简单的 “Hello World” 程序到复杂的系统开发,Rust 正逐渐成为现代软件开发的热门选择。
36 1
|
1月前
|
Rust 安全 编译器
初探 Rust 语言与环境搭建
Rust 是一门始于2006年的系统编程语言,由Mozilla研究员Graydon Hoare发起,旨在确保内存安全而不牺牲性能。通过所有权、借用和生命周期机制,Rust避免了空指针和数据竞争等问题,简化了并发编程。相较于C/C++,Rust在编译时预防内存错误,提供类似C++的语法和更高的安全性。Rust适用于系统编程、WebAssembly、嵌入式系统和工具开发等领域。其生态系统包括Cargo包管理器和活跃社区。学习资源如&quot;The Book&quot;和&quot;Rust by Example&quot;帮助新手入门。安装Rust可通过Rustup进行,支持跨平台操作。
101 2
初探 Rust 语言与环境搭建
|
19天前
|
Rust 安全 程序员
Rust 语言的防错机制太惊人了!安全编码从此不再是难题,快来一探究竟!
【8月更文挑战第31天】《安全编码原则:Rust 语言中的防错机制》探讨了代码安全的重要性,并详细介绍了Rust语言如何通过内存安全模型、所有权与借用规则等特性,在编译阶段检测并阻止潜在错误,如缓冲区溢出和悬空指针。文章还讨论了类型安全、边界检查等其他安全特性,并提出了遵循不可变引用、避免裸指针及充分测试等实用编码原则,以进一步提升代码质量和安全性。随着Rust在软件开发中的应用日益广泛,掌握其安全编码原则变得尤为重要。
31 0
|
19天前
|
Rust 安全 调度
从零构建梦想操作系统:用Rust语言亲手打造专属内核,你也可以成为系统开发者!
【8月更文挑战第31天】开发操作系统内核虽具挑战,却也充满乐趣。本文将指导你从零开始,使用Rust语言构建一个简单的操作系统内核。首先安装Rust环境和交叉编译工具链,然后创建新项目`my_kernel`。通过修改`Cargo.toml`和编写启动函数,结合串口输出和`multiboot2`库,最终使用QEMU运行内核。此教程旨在帮助你理解Rust在系统开发中的应用,激发深入探索的兴趣。
36 1
|
19天前
|
Rust 安全 算法
揭秘Rust语言如何重塑区块链安全:打造坚不可摧的分布式账本新篇章!
【8月更文挑战第31天】自比特币诞生以来,区块链技术凭借其去中心化和不可篡改的特点备受关注。为了应对安全性挑战,Rust 语言凭借其内存安全特性逐渐成为区块链开发的优选。本文探讨了 Rust 如何助力区块链实现更安全的分布式账本。通过示例展示了 Rust 在避免内存泄漏、空指针引用及数据竞争等方面的优势,预示着 Rust 在高性能、高安全性需求的区块链应用中拥有广阔前景。
31 1
|
18天前
|
Rust Linux Go
Rust/Go语言学习
Rust/Go语言学习
|
19天前
|
开发者 vr&ar 机器学习/深度学习
Xamarin 开发者的未来趋势展望:掌握跨平台开发新机遇,引领移动应用创新潮流与技术变革方向
【8月更文挑战第31天】Xamarin 作为领先的跨平台开发框架,通过 C# 和 .NET 框架实现一次编写、多平台运行,简化了 iOS、Android 和 Windows 应用的开发流程。未来几年,Xamarin 开发者将面临跨平台开发普及、云集成、机器学习、AR/VR、性能优化及安全性等关键趋势。通过学习新技术并积极采用新工具,开发者能够提升应用质量和用户体验,如利用 Azure AD B2C 实现身份认证,从而在竞争激烈的市场中脱颖而出。
39 0
|
19天前
|
Rust 安全 开发者
惊爆!Xamarin 携手机器学习,开启智能应用新纪元,个性化体验与跨平台优势完美融合大揭秘!
【8月更文挑战第31天】随着互联网的发展,Web应用对性能和安全性要求不断提高。Rust凭借卓越的性能、内存安全及丰富生态,成为构建高性能Web服务器的理想选择。本文通过一个简单示例,展示如何使用Rust和Actix-web框架搭建基本Web服务器,从创建项目到运行服务器全程指导,帮助读者领略Rust在Web后端开发中的强大能力。通过实践,读者可以体验到Rust在性能和安全性方面的优势,以及其在Web开发领域的巨大潜力。
29 0
|
19天前
|
开发者 API 开发框架
Xamarin 在教育应用开发中的应用:从课程笔记到互动测验,全面解析使用Xamarin.Forms构建多功能教育平台的技术细节与实战示例
【8月更文挑战第31天】Xamarin 作为一款强大的跨平台移动开发框架,在教育应用开发中展现了巨大潜力。它允许开发者使用单一的 C# 代码库构建 iOS、Android 和 Windows 应用,确保不同设备上的一致体验。Xamarin 提供广泛的 API 支持,便于访问摄像头、GPS 等原生功能。本文通过一个简单的教育应用示例——课程笔记和测验功能,展示了 Xamarin 在实际开发中的应用过程。从定义用户界面到实现保存笔记和检查答案的逻辑,Xamarin 展现了其在教育应用开发中的高效性和灵活性。
26 0
|
19天前
|
Rust 安全 Java
Rust语言在Web后端的应用:基于Actix-web构建高性能、安全可靠的服务器实践
【8月更文挑战第31天】随着互联网的发展,Web应用对性能和安全性要求不断提高。Rust凭借卓越的性能、内存安全及丰富生态,成为构建高性能Web服务器的理想选择。本文通过一个简单示例,展示如何使用Rust和Actix-web框架搭建基本Web服务器,从创建项目到运行服务器全程指导,帮助读者领略Rust在Web后端开发中的强大能力。通过实践,读者可以体验到Rust在性能和安全性方面的优势,以及其在Web开发领域的无限潜力。
44 0