• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Rust编程语言入门教程二-核心概念所有权(Ownership) 和生命周期、变量声明遍历数组

武飞扬头像
西京刀客
帮助1

Rust编程语言入门教程(二)

一、堆栈知识基础

栈和堆都是代码运行时可以使用的内存空间,不过这两者通常以不同的结构组织而成。栈是后进先出。

所有存储在栈上的数据都必须拥有一个已知且固定的大小。编译器无法确定大小的数据,只能存储在堆上。

堆上数据通常是栈上变量存储一个指针进行访问,但是效率相对来说更慢。堆的空间更大,但是需要进行良好的管理,包括分配和释放。(数据入栈不叫分配)

栈内存“分配”和“释放”都很高效,在编译期就确定好了,因而它无法安全承载动态大小或者生命周期超出帧存活范围外的值。 所以,我们需要运行时可以自由操控的内存,也就是堆内存,来弥补栈的缺点。

堆内存足够灵活,然而堆上数据的生命周期该如何管理,成为了各门语言的心头大患。

C 采用了未定义的方式,由开发者手工控制;C 在 C 的基础上改进,引入智能指针,半手工半自动。Java 和 DotNet 使用 GC 对堆内存全面接管,堆内存进入了受控(managed)时代。所谓受控代码(managed code),就是代码在一个“运行时”下工作,由运行时来保证堆内存的安全访问。

各种语言都需要及时管理堆内存,最小化冗余的空间,并及时清理释放了的内存,通过程序员手动管理,或者引入GC的概念。而Rust则是引入了所有权的概念。

Rust所有权界定,使编译器清楚地知道:当一个值离开作用域的时候,这个值不会有任何人引用,它占用的任何资源,包括内存资源,都可以立即释放,而不会导致问题。

二、Rust的所有权(Ownership)

1. 什么是Rust的所有权

Rust的所有权,是一个跨时代的理念,是内存管理的第二次革命。Ownership是Rust的一个核心概念。

每种编程语言都有自己的一套内存管理的方法。有些需要显式的分配和回收内存(如C),有些语言则依赖于垃圾回收器来回收不使用的内存(如Java)。而Rust不属于以上任何一种,它有一套自己的内存管理规则,叫做Ownership。

Ownership的规则
Rust的所有权并不难理解,它有且只有如下三条规则:

  • 一个值只能被一个变量所拥有,这个变量被称为所有者(Each value in Rust has a variable that’s called its owner)。
  • 一个值同一时刻只能有一个所有者(There can only be one owner at a time),也就是说不能有两个变量拥有相同的值。所以对应刚才说的变量赋值、参数传递、函数返回等行为,旧的所有者会把值的所有权转移给新的所有者,以便保证单一所有者的约束。
  • 当所有者离开作用域,其拥有的值被丢弃(When the owner goes out of scope, the value will be dropped),内存得到释放。

这三条规则很好理解,核心就是保证单一所有权。其中第二条规则讲的所有权转移是 Move 语义,Rust 从 C 那里学习和借鉴了这个概念。

所有权规则,解决了谁真正拥有数据的生杀大权问题,让堆上数据的多重引用不复存在,这是它最大的优势。

Rust的一个设计原则:Rust永远都不会自动地创建数据的深度拷贝。**只有类似其他语言的浅度拷贝:复制指向堆中数据的指针。但是这里有一个不同之处:Rust中不仅会复制,还会使前一个变量无效,**所以这里使用术语移动(move)描述。

深度拷贝:需要通过调用.clone() 方法显示地执行深度拷贝。

  • 所有权:一个值只能被一个变量所拥有,且同一时刻只能有一个所有者,当所有者离开作用域,其拥有的值被丢弃,内存得到释放。
  • Move 语义:赋值或者传参会导致值 Move,所有权被转移,一旦所有权转移,之前的变量就不能访问。

Rust 通过单一所有权来限制任意引用的行为!

通过单一所有权模式,Rust 解决了堆内存过于灵活、不容易安全高效地释放的问题,既避免了手工释放内存带来的巨大心智负担和潜在的错误;又避免了全局引入追踪式 GC 或者 ARC 这样的额外机制带来的效率问题。

2. Rust 变量声明基础

Rust中合法的标识符(包括变量名、函数名、triat名等)必须由数字、字母、下划线组成,而且不能以数字开头。这个和很多语言都是一样的。

变量声明中的let的使用也是借鉴了函数式语言的思想,let表明的是绑定的含义,表示的是将变量名和内存作了一层绑定关系。 在Rust中,一般把声明的局部变量并初始化的语句称为”变量绑定“, 这里强调的是”绑定“的含义,这里和C /C中的”赋值初始化语句有所不同。

let variable: i32;
println!("variable  = {}", variable); // error[E0381]: use of possibly unintialized 'variable' 

这个是声明,而没有给变量赋值,这个在别的语言中可能是行的通的,但是在rust中,编译器直接报错(如果在后面使用这个为赋值(定义)的变量, Rust编译器会对代码作基本的静态分支流程分析,确保变量在使用之前一定被初始化,variable没有绑定任何值,这样的代码会引起很多内存不安全的问题,比如计算结果非预期、程序崩溃,所以Rust编译器必须报错。

Rust中,每个变量必须被合理初始化之后才能被使用,使用未初始化变量这样的错误,在rust中是不可能出现的。

let x:i32=0; 定义一个32位整型变量,初值为0
let y=false; 定义一个布尔型变量,初值false
let str=String::from("String"); 定义一个字符串,初值String

声明变量需要使用let 关键字,后跟变量名称,然后跟[:type],最后可以用等号进行赋值,如果省略[:type],则必须绑定初值,否则编译器将无法确定该变量类型。

以上定义的几个变量都是变量值不可变的变量,这并不是说它们像常量一样不可变,而是不能像C/C ,Java,C#等语言一样通过等号直接改变其值,重新赋值的方式如下:

let x=100;
let x=1000;

还可以用mut 关键字来修饰,使其成为我们熟悉的普通变量:

let mut x=100;
x=1000;

Rust还添加了元组的的复合类型,元组可以用一对小括号包含一组不同类的数据集合:

let tup:(u32,bool,String)=(32,true,String::from("test));
let (num1,num2,num3)=(32,true,String::from("test"));
// num2的值为true

数组表示跟其他语言的类似:
数组的定义其实就是为分配一段 连续的相同数据类型 的内存块。
数组是静态的。这意味着一旦定义和初始化,则永远不可更改它的长度。
Rust 语言为数组的声明和初始化提供了 3 中语法

  1. 最基本的语法,指定每一个元素的初始值
    let variable_name:[dataType;size] = [value1,value2,value3];
let arr:[i32;4] = [10,20,30,40];

2)省略数组类型的语法
因为指定了每一个元素的初始值,所以可以从初始值中推断出数组的类型
let variable_name = [value1,value2,value3];

let arr = [10,20,30,40];
  1. 指定默认初始值的语法,这种语法有时候称为 默认值初始化。
    如果不想为每一个元素指定初始值,则可以为所有元素指定一个默认的初始值。
    let variable_name:[dataType;size] = [default_value_for_elements,size];

例如下面的代码为每一个元素指定初始值为 -1

let arr:[i32;4] = [-1;4];

测试demo:

fn main() {
   let arr:[i32;4] = [-1;4];
   println!("array is {:?}",arr);
   println!("array size is :{}",arr.len());
}

数组是一个复合类型,因此输出数组的时候需要使用 {:?} 格式符。

fn main(){
   let arr:[i32;4] = [10,20,30,40];
   println!("array is {:?}",arr);
   println!("array size is :{}",arr.len());
}
如何遍历数组
for in 循环遍历数组

在其它语言中,一般使用 for 循环来遍历数组,Rust 语言也可以,只不过时使用 for 语句的变种 for … in … 语句。

因为数组的长度在编译时就时已知的,因此我们可以使用 for … in 语句来遍历数组。

注意 for in 语法中的左闭右开法则。

fn main(){
   let arr:[i32;4] = [10,20,30,40];
   println!("array is {:?}",arr);
   println!("array size is :{}",arr.len());

   for index in 0..4 {
      println!("index is: {} & value is : {}",index,arr[index]);
   }
}
迭代数组 iter()

我们可以使用 iter() 函数为数组生成一个迭代器。

然后就可以使用 for in 语法来迭代数组。

fn main(){

let arr:[i32;4] = [10,20,30,40];
   println!("array is {:?}",arr);
   println!("array size is :{}",arr.len());

   for val in arr.iter(){
      println!("value is :{}",val);
   }
}

Rust 变量作用域

几乎所有有此概念的编程语言都是一样的,就是在一个作用域内定义的局部变量只能在它被定义的作用域内被使用,而不能被其他作用域使用。

Ownership的规则中,有一条是owner超过范围后,值会被销毁。那么owner的范围又是如何定义的呢?在Rust中,花括号通常是变量范围作用域的标志。最常见的在一个函数中,变量s的范围从定义开始生效,直到函数结束,变量失效。

所有权(Ownership)的移动
let x = 5;
let y = x;
println!("x: {}", x);

类似于整型的简单类型可以在编译器确定大小,并能够存储在栈中。对于这些值的复制操作是十分快速的。对于这类型而言,深度拷贝与浅度拷贝没有任何区别,因此不需要考虑移动,直接将5进行复制,然后赋给y。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("s1: {}", s1);
}

这两段代码看起来唯一的区别就是变量的类型,第一段使用的是整数型,第二段使用的是字符串型。而执行结果却是第一段可以正常打印x的值,第二段却报错了。这是什么原因呢?

我们来分析一下代码。对于第一段代码,首先有个整数值5,赋给了变量x,然后把x的值copy了一份,又赋值给了y。最后我们成功打印x。看起来比较符合逻辑。实际上Rust也是这么操作的。

对于第二段代码我们想象中,也可以是这样的过程,但实际上Rust并不是这样做的。先来说原因:对于较大的对象来说,这样的复制是非常浪费空间和时间的。

三、Rust生命周期

什么是Rust生命周期

Rust生命周期详解
参考URL: https://zhuanlan.zhihu.com/p/93193353

所有权和生命周期是 Rust 和其它编程语言的主要区别,也是 Rust 其它知识点的基础。

在任何语言里,栈上的值都有自己的生命周期,它和帧的生命周期一致,而 Rust,进一步明确这个概念,并且为堆上的内存也引入了生命周期。

在其它语言中,堆内存的生命周期是不确定的,或者是未定义的。因此,要么开发者手工维护,要么语言在运行时做额外的检查。而在 Rust 中,除非显式地做 Box::leak() / Box::into_raw() / ManualDrop 等动作,一般来说,堆内存的生命周期,会默认和其栈内存的生命周期绑定在一起。
所以在这种默认情况下,在每个函数的作用域中,编译器就可以对比值和其引用的生命周期,来确保“引用的生命周期不超出值的生命周期”。

根据所有权规则,值的生命周期可以确认,它可以一直存活到所有者离开作用域; 而引用的生命周期不能超过值的生命周期。在同一个作用域下,这是显而易见的。然而,当发生函数调用时,编译器需要通过函数的签名来确定,参数和返回值之间生命周期的约束。

“引用的生命周期不能超出值的生命周期”,否则编译器就会报错。
生命周期的主要目标是防止悬空指针。

fn main() {
   {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
   }
}

上述例子外部范围生命了变量r,内部区域声明了变量x。在内部区域,我们尝试设置r为x的引用,当内部区域终止时,我们尝试打印r。这份代码不会编译通过因为在打印r的地方我们尝试引用已经不再这个区域的变量x。
编译输出结果:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `x` does not live long enough
  --> src/main.rs:7:13
   |
7  |         r = &x;
   |             ^^ borrowed value does not live long enough
8  |     }
   |     - `x` dropped here while still borrowed
9  | 
10 |     println!("r: {}", r);
   |                       - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` due to previous error

上述编译错误告诉我们x没有存在足够长的时间。原因是当运行到第7行时,x将会被销毁,在第10行的时候,r还在引用x,但是此时x已经不存在了,此时编译器将会告诉我们x需要存在知道第10行,但是第7行就被销毁了!

Borrow检查
Rust有个Borrow检查器用于检查Borrow对象(或者叫引用)是否有效。

{
    let r;                // --------- -- 'a
                          //          |
    {                     //          |
        let x = 5;        // - -- 'b  |
        r = &x;           //  |       |
    }                     // -        |
                          //          |
    println!("r: {}", r); //          |
}

编译器会隐式生成生命周期a和b,我们可以看到x存在b生命周期内,而r引用则存在a生命周期内,Borrow检查器会在编译的时候会检查r的生命周期和r所指向的x的生命周期,而r的生命周期a大于x的生命周期,于是编译器会报错。

四、参考

https://time.geekbang.org/column/article/464856
Rust 类型系统探究——所有权等
参考URL: https://baijiahao.百度.com/s?id=1714652869854349559
Rust 数组
参考URL: https://www.twle.cn/c/yufei/rust/rust-basic-array.html
Rust生命周期详解
参考URL: https://zhuanlan.zhihu.com/p/93193353

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgcjcbf
系列文章
更多 icon
同类精品
更多 icon
继续加载