5分钟速读之Rust权威指南(五)

645 阅读7分钟

所有权

所有权的概念是与一般的编程语言最大的不同,rust整个语言的设计都是围绕所有权来的,在学习所有权的时候需要反复练习,去体会其中的设计,书中总结了三个原则:

  • Rust中的每一个值都有一个对应的变量作为它的所有者。
  • 在同一时间内,值有且仅有一个所有者。
  • 当所有者离开自己的作用域时,它持有的值就会被释放掉。

变量作用域

  • x在进入作用域后变得有效。
  • 它会保持自己的有效性直到自己离开作用域为止。
fn main() {
  println!("{}", x); // 报错,x还未初始化
  { // 这里是一个作用域的开始
    let x = "string";
    println!("{}", x); // "string"
  } // 这里是一个作用域的结束,x将被回收
  println!("{}", x); // 报错,x不存在
}

String类型

字符串的堆存储和栈存储

初始化一个存放在堆中的字符串:

// 创建空字符串
let mut s1 = String::new();

// 从字面量创建字符串
let mut s2 = String::from("hello");

// 追加单个char
s1.push('h');

// 可以追加一个字符串
s2.push_str("world");

println!("{}-{}", s1, s2); // h-helloworld

初始化一个存放在栈中的字符串:

// 字面量声明的字符串会存在栈中,是不可变的
let mut x = "string";
x.push('x'); // 报错: 不存在 'push' 方法

所有权转移

存放在堆中的数据会发生所有权的转移:

// 初始化一个堆中的字符串
let s1 = String::from("1");

// 这里将s1的指针转移给了s2,s1会被释放
let s2 = s1;
println!("{}", s2) // "1"

// 尝试获取s1
println!("{}" s1) // 报错,s1的数据已经移动给了s2

栈中的数据会发生复制,而不会转移所有权:

// 初始化一个栈的字符串
let s1 = "hello";

// s2会复制给s1
let s2 = s1;

// 获取s1和s2
println!("{}" s1); // "hello"
println!("{}" s2); // "hello"

使用数据克隆复制堆中数据:

let s1 = String::from("hello");

// 复制s1的数据给s2
let s2 = s1.clone();

// 获取s1和s2
println!("{}" s1); // "hello"
println!("{}" s2); // "hello"

存在栈中的数据类型,是否使用clone是没有区别的:

let s1 = "hello";

// 复制s1的数据给s2
let s2 = s1.clone();
// 等价于
// let s2 = s1;

// 获取s1和s2
println!("{}" s1); // "hello"
println!("{}" s2); // "hello"

事实上上面只是拿字符串来举例,对于数字和char来说,也是存在栈中的,同样会进行数据复制,而不是所有权的转移。

关于元组

如果元组中所有成员都是栈中数据,那元组同样可以复制:

// 定义一个包含字符串,char,数字类型的元组
let t1 = ("abc", 'c', 1);

// t1复制给t2
let t2 = t1;

// 尝试获取
println!("{:?}", t1); // ("abc", 'c', 1)
println!("{:?}", t2); // ("abc", 'c', 1)

如果成员中有一个数据不是可复制的,那么元组也不可能复制,只是简单的所有权转移:


// 初始化一个包含堆中数据的元组
let t1 = ("abc", 'c', String::new());

// 这里t1会把所有权转移给t2
let t2 = t1;

// 尝试获取
println!("{:?}", t2);
println!("{:?}", t1); // 报错,t1的所有权已经移动给了t2

允许移动成员,但是移动后不能再使用:

let t1 = (String::from("hello"), String::from("world"));

// 将第一个成员的所有权转移给s
let s = t1.0;

println!("{}", s); // "hello"
println!("{}", t1.1); // "world"
println!("{:?}", t1); // 报错,成员的所有权已经被移动了
println!("{}", t1.0); // 报错,成员的所有权已经被移动了

数组成员如果是栈中数据,那数组同样可以复制:

let t1 = (1, 2);

// 复制第一个成员给t2
let t2 = t1.0;

println!("{}", t2); // 1
println!("{}", t1.0); // 1
println!("{}", t1.1); // 2

数组的规则与元组的规则相同,这里不再介绍,可以自行练习

所有权与函数参数

调用函数时,对函数的传参也会转移所有权:

// 由于函数参数类型是堆中数据,所以会发生所有权转移
fn take_ownership(s: String) {
  println!("{}", s) // "hello"
}

// 定义一个堆中数据
let s = String::from("hello");

// 调用函数会将s的所有权转移给函数
take_ownership(s);

// 尝试获取s
println!("{}", s); // 报错,所有权被take_ownership拿走了

栈中的数据会发生复制:

// 由于函数参数类型是栈中数据,所以会发生复制
fn makes_copy(n: i32) {
   println!("{}", n) // 5
}

// 定义一个栈中数据
let num = 5;

// 调用函数会对num进行复制
makes_copy(num);

// num仍然有效
println!("{}", num); // 5

返回值与作用域

当一个持有堆数据的变量离开函数作用域时,它的数据就会被清理回收,但是可以通过返回值将数据返回而不被销毁:

// 函数中创建一个堆中数据字符串,并返回所有权
fn gives_owner_ship() -> String {
  let s = String::from("hello");
  s
}

// 函数接受一个字符串数据的所有权,然后直接返回
fn takes_and_gives_back(s: String) -> String {
  s
}

// 获取函数中创建的字符串的所有权
let s1 = gives_owner_ship();

// 将s2的所有权转移给函数,再通过函数返回值将所有权转移给s3
let s2 = String::from("world");
let s3 = takes_and_gives_back(s2);

println!("{}", s1); // "hello"
println!("{}", s3); // "world"
println!("{}", s2); // 报错, s2的所有权转移到了s3

利用返回值拿回所有权:

fn get_lenth(s: String) -> (String, usize) {
  let len = s.len(); // 获取s的长度
  (s, len) // 将s的所有权和长度返回
}

let s1 = String::from("hello");

// 使用返回值重新拿到所有权
let (s1, len) = get_lenth(s1);

println!("{}-{}", s1, len); // "hello-5"

引用变量

数据的所有权转移会降低开发的灵活性,所有有了引用的概念来解决这个问题:

let s1 = String::from("hello");

// 使用&符号创建两个对s1的引用,不会获取s1的所有权,可以将引用理解成一个指针
let s2 = &s1;
let s3 = &s2;

// 获取
println!("{}-{}-{}", s1, s2, s3); // "hello-hello-hello"

函数同样支持引用类型:

// 指定引用类型的参数
fn get_length(s: &String) -> usize {
  // 这里的 s 指向 s1 指向 String真实数据
  // 由于s没有取得String数据的所有权,所以这里不需要使用返回值归还所有权
  s.len()
}

let s1 = String::from("hello");

// 将s1的引用传给函数
let len = get_length(&s1);

// s1仍可使用
println!("{}-{}", s1, len); // "hello-5"

使用&可以引用数据,但是不可以对数据进行改变:

let s1 = String::from("hello");
let s2 = &s1;

// 通过s2试图改变数据
s2.push("world"); // 报错,s2是一个不可变引用,不能对数据进行更改

使用可变引用对数据进行更改:

// 使用&mut将参数声明为可变引用
fn get_length(s: &mut String) -> usize {
  s.push_str("world");
  s.len()
}

// 声明可变数据
let mut s1 = String::from("hello");

// 传入可变引用变量
let len = get_length(&mut s1);

// s1发生了数据变更
println!("{}-{}", len, s1); // "10-helloworld"

对于单个作用域中的数据来说,一次只能声明一个可变引用:

let mut s1 = String::from("hello");
let s2 = &mut s1;
let s3 = &mut s1 // 报错,不能有多个可变引用

在单个作用域中也不能存在可变引用和可变引用的函数传参:

let mut s1 = String::from("hello");
let s2 = &mut s1;
let len = get_length(&mut s1); // 报错,同样不能有多个可变引用,因为传参和s2都属于可变引用

可变与不可变引用不能同时存在:

let mut s1 = String::from("hello");

// 创建不可变引用
let s2 = &s1; // 报错,不能将s1进行不可变引用,因为下边有可变引用

// 创建可变引用
let mut s3 = &mut s1; // 报错,不能将s1进行可变引用,因为上边有不可变引用

// 前提是下面有使用到上边的值,否则就不会报错
println!("{}", s1);
println!("{}", s2);
println!("{}", s3);

引用的规则

  • 在任何一段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用,试想一下如果数据改变了,那么不可变的变量的不可变特性是否还有意义。。
  • rust会保证引用总是有效的。