人气的编程语言
Rust是一个优秀的,现代的编程语言,它兼顾开发效率和执行效率。它也是一个相当受欢迎的语言,在Stack Overflow 2020的调查中,有86%的开发者表示他们会继续使用Rust。Mozilla开发者Graydon Hoare在2006年创建该语言,Rust的人气逐渐上涨,现在更多被应用在Web应用,嵌入式开发等。
为什么是Rust?
内存管理:在不同的语言有不同的内存管理模式,例如Java和Golang使用的是垃圾回收机制,在运行时寻找和释放不再使用的内存。而Rust拥有一套独特的模式。
Rust是一个系统级编程语言,但它不仅能开发底层程序,Rust有较高的抽象层次。相比Hackell等超抽象的语言,Rust更多是在一个平衡的位置。
Rust并没有损失性能在内存安全方面,但Rust将内存安全做得相当优秀
在开始之前
若你拥有Rust的一些基础知识,或许可以更高效的阅读本文。我不会在太过基础的地方做过多解释(例如某些基础的名词),而是直接从主题出发,结合一些简单易懂的示例,着重介绍Rust的特性。
Rust所有权系统
所有权系统(Ownership System)是Rust最核心,最独特的特性之一。各个语言有各个语言管理资源的方式,例如C语言需要开发者手动分配和释放内存;Java语言的虚拟机提供垃圾回收机制。而Rust与它们都不同,Rust通过所有权系统管理内存,编译器会在编译阶段根据【所有权规则】进行检查。
所有权系统主要由三个部分组成,而这三个部分便是本文的重点:
所有权(Ownership)
借用(Borrowing)
生命周期(Lifetime)
所有权(Ownership)
所有权(Ownership)为Rust的高效与安全存在,让Rust在编译阶段能更有效的分析和管理内存资源。
原则
所有权的机制遵循下三条原则:
创建值或者资源时,将其分配给一个变量,变量称资源的【所有者】
一次分配只能有一个【所有者】
当变量(所有者)不在作用域内,该变量会被删除
变量作用域
作用域(scope)是一个项(item)在一段程序中有效的范围。首先,在Rust中任何一个可用于包含代码的大括号都是一个单独的作用域(定义数据类型的大括号除外)。拥有单独作用域的结果包括但不限于:
【if】等流程控制语句的大括号
单独存在的大括号
函数定义的大括号
【match】模式匹配的大括号
mod模块的大括号
例如,下文是一个单独存在的大括号:
{
let x = "test";
}
在x定义之前,x是无效的。从x被定义开始有效。大括号外不属于x的作用域,x无效。x绑定了字符串的值“test”,在跳出作用域后,变量x所绑定的值便会销毁。
悬垂引用
Rust是一门支持指针操作的语言,因此可以用于嵌入式开发。但是有时会因为释放内存而导致指向该数据的指针变成悬垂指针(dangling pointer)。Rust编译器不允许这种情况发生,它会在编译阶段检查引用必须是有效的。它不允许销毁数据后仍继续引用该数据。
fn main() {
let x = y();
}
fn y() -> &String {
let z = String::from("test");
&z
}
在上代码示例,函数的返回值z指向堆中字符串数据的引用,当函数y结束,z就跳出作用域,指向的字符串会被销毁,返回值便无效。不会通过编译。
数据的移动
Rust并没有其他语言深浅拷贝的概念,只有三个概念:
移动(Move)
拷贝(Copy)
克隆(Clone)
所以你不能在Rust这样写赋值
fn main(){
let x = String::from("test");
let y = x;
println!("{},{}", x, y);
}
在上代码示例,变量x绑定了String字符串数据,则该字符串数据的所有者是变量x。当执行let y = x;时,它不会像其他语言一样copy堆数据引用赋值给y。
Rust在执行let y = x;时,会让x变成未初始化的变量,你可以对它重新赋值,但在重新赋值之前不能直接使用这个未初始化变量。而绑定堆内存的则是变量y,y成为这个值的所有者。
值的所有权转移,Move的是栈中的指针而不是实际数据,以提高效率。Move时长发生在函数传参,变量赋值,函数返回数据时,Rust在对付这个过程相当高明。
Rust默认使用的是Move,但是Rust还有Copy,则要求Copy的数据类型实现了Copy Trait。例如下代码就是使用Copy。
let x = 1;
let y = x;
i32默认情况下实现了Copy Trait,在进行上代码示例的时候,n被赋值为3,但x仍然不变。
只有实现了Clone Trait的数据类型才能进行Clone,有些情况下不便使用Copy,如果需要继续使用原变量,可以使用Clone手动Copy变量的数据,而原始变量不影响。
Copy Trait的实现条件(部分)
Rust的规定里,自定义类型必须所有成员都实现了Copy Trait,这个类型才能实现Copy Trait。并非所以类型都能实现Copy Trait。
常见的数字、布尔类型,共享借用指针&都具有Copy属性,而Vec,可写借用指针&mut之类的类型都不具备Copy属性;数组如果内部元素有Copy属性,则这个数组具备Copy属性;Struct和enmu类型不会自动拥有Copy属性,并且只有当它们的内部元素都具有Copy属性,编译器才会允许我们手动实现Copy Trait。
借用(Borrow)
我一直在犹豫这段应不应该归纳于“所有权”下,还是决定开一个单独的段来细解。
所有权转移,原变量会丢失数据的所有权,你不仅可以这样做。Rust可以通过引用的方式来借用所有权(Borrow Ownership)
fn main(){
let x = String::from("test");
let y = &x;
let z = &x;
println!("{}, {}, {}",x, y, z);
}
在示例,y和z都借用了x的所有权,完成之后将会自动交还。
这是它们的借用关系
在上关系中,y是引用,可以Copy的,若新定义一个:
let n = y;
并不会影响y的有效,y仍然指向数据。这些变量也可以作为传递给函数的参数。
可变引用和不可变引用
变量引用分为
可变引用 &mut T:可变借用
不可变引用&T:不可变借用
不可变引用:借用只读权限,无修改引用数据权限
可变引用:借用读写权限,可以修改引用数据
例如上一个例子的x,是一个不可变引用,若我们需要将它定义为一个可变引用,应该写:
let mut x = String::from("test");
借用原则
多个不可变引用可共存,即可同时读;可变引用和不可变引用在同一作用域互斥,且多个可变引用互斥。为了保障数据一致性,只能拥有一个“写”或者多个“读”,它们两个不能同时出现。Rust会在编译阶段检查借用的安全。
当把所有权“借出去”后,就不应该再操作
不应该借用正在使用的所有权
生命周期(Lifetime)
Rust的生命周期是一个全新的概念,没有其他编程语言可以拿来借鉴。正因如此,这可能对于初学者来说相当有难度。
Rust的生命周期是与所有权机制一样重要的资源管理机制,这个概念用于应对复杂的类型系统中,资源管理的问题。
在上面讲解中,在同一作用域下,编译器可以检查出生命周期存在的问题,Rust要求我们为这些引用显示的生命周期标记,否则在函数之间传递引用时,编译器会很难识别这些问题。“指定显示”可能会略显繁琐,却在安全上能起到显著作用,就让我们欣然接受吧。
简单情况下
Rust大多数对象的生命周期只发生在块内,或者某个方法转移了它的所有权,而借用(Borrow)则能超出范围而使其存在。或者Copy它以让它生命周期能在外部作用域存活。但是这有两个类型例外,const类型和static类型。它们两个的生命周期是整个程序,const类型可以内联到代码的任何地方,但是static不行,它在内存的固定位置。
生命周期标注
Rust十分重视程序的安全,某些无法确定生命周期的情况,你就得手动标注,避免发生悬垂引用等问题。需要注意的是,这无法改变引用的生命周期,但是可以明确声明两个引用的生命周期一致。
生命周期注释使用单引号。例如 这是一个含有生命周期注释的引用
&'x i32
函数中的生命周期
在Rust函数、方法中,参数的生命周期被称为输入生命周期(Input Lifetimes)。而返回值的生命周期叫做输出生命周期(Output Lifetimes)
我们结合一个简单的示例来理解函数中参数的生命周期。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
这个名为【longest】的函数会获取两个生命周期为【'a】的参数,类型是字符串。我们在里面的生命周期标注,<'a>是泛型生命周期,它得到的生命周期是两个参数中字符串长度较短的那个。
在Rust中,一个函数执行结束后作用域内的数据会被清除,悬垂引用很大可能会发生在引用返回值的变量上。当函数返回一个引用,但引用的数据随着那个作用域一同销毁,就会发生悬垂引用。但实际情况只会比理论更复杂,例如引用的返回值与函数的参数有什么关系。我在第一段【所有权】的【悬垂引用】中有举例一个最简单的情况,变量“z”是返回值,却指向了一个已经被销毁的,在函数y作用域内的数据。
Struct定义生命周期标注
Rust的生命周期标注不仅可以用在函数,还能用在Struct等地方,我不多做解释,但是在这里留下一个Struct生命周期标注的 简单的示例:
struct My_Struct<'a> {
x: &'a str,
}
拓展:Rust非词法作用域(NLL)简介
计算机程序的作用域分为静态作用域和动态作用域。静态作用域又称词法作用域,采用词法作用域的变量也叫做词法变量。词法变量有一个在编译时,确定的,静态的作用域,在作用域以外这个变量无效。很多语言都是静态作用域规则,rust也不例外;而动态作用域的变量,不出乎意料叫做动态变量。程序执行动态变量的代码段时,这个变量将一直存在,直到代码段结束。
随着Rust编译器的升级,Rust变量的生命周期变成了NLL(None Lexical Lifetimes),NLL生命周期简单的说就是从借用时开始,到最后一次使用它结束。(泛型作用域生命周期不包括)
Rust编译器现在的借用检查变聪明了,能判断生命周期在适当的地方结束,而不再是简单粗暴地判断语句块。
结尾
那草草的收个尾,Rust是一个值得学习的现代编程语言,若想学习,你可以自行上网查看教程或者相关的文章。我留下几个链接:
Rust语言圣经(Rust Course):https://course.rs/about-book.html
《The RustProgramming Language》:https://doc.rust-lang.org/book/