所有权与移动

88 阅读9分钟

所有权

  • 在rust 中所有权这个概念内置于语言本身, 并通过编译器检查强制执行.
  • 每个值都有决定其生命周期的唯一拥有者,
  • 当拥有者被释放时,它拥有的值也会同时被释放.释放的行被称为丢弃
  • 这些规则便于通过检查代码确定任意值的生命周期,也提供了系统级语言本应支持的对生命周期的控制
  • 变量拥有自己的值,当控制流程离开声明变量的快时,变量就会被丢弃,因此它的值也会一起被丢弃
// 注意: 保存padovan指针,容量和长度的字都直接位于print_padovan 函数栈帧中,只有向量的缓冲区才会分配在堆上
fn print_padovan() {
    let mut padovan = vec![1, 1, 1];
    for i in 3..10 {
        let next = padovan[i - 3] + padovan[i - 2];
        padovan.push(next);
    }
    println!("p(1..10) = {:?}", padovan);
} 
  • rust 中的box类型是所有权的另一个例子.

// Box<T> 是指向存储在堆上的T类型值的指针.可以调用 Box::new(v) 分配一些堆空间
// 将值v移入其中,并返回一个指向该堆空间的Box. 因为box拥有它所指向的空间,所以当丢弃Box 也会释放此空间
let point = Box::new((0.625,0.5,));
let label = format!("{:?}",point);
assert_eq!(label,"(0.625, 0.5)")

// 当程序调用Box::new 时,它会在堆上为由两个f64值构成的元祖分配空间,然后将其参数(0.625, 0.5)移进去,并返回指向该空间的指针

// 帧本身包含变量point 和label ,其中每个变量都指向其拥有的堆内存,当丢弃它们时,它们拥有的堆中内存也会一起被释放.
/// 就像变量拥有自己的值一样,结构体拥有自己的字短,元祖,数组和向量则拥有自己的元素
struct Persion{name:String,birth:i32};
let mut composers = Vec::new();
composers.push(Persion{name:"Palestrina".to_string(),birth:1525});
composers.push(Persion{name:"dowland".to_string(),birth:1563});

composers.push(Persion{name:"Lully".to_string(),birth:1632});
for composer in &composers {
    println!("{},born {}",composer.name,composer.birth);
}
  • 可以将值从一个拥有者转移给另一个拥有者,这允许你构建重新排列和拆除树形结构
  • 像整数,浮点数和字符这样非常简单的类型.不受所有权规则的约束,这些称为Copy类型
  • 标准库提供了引用计数指针类型Rc和Arc 它们允许值在某些限制有多个拥有者
  • 可以对值进行“借用”(borrow) 以获得值的引用,这种引用使非拥有型指针有着受限的生命周期

move 移动

  • 对于大多数类型来说,像为变量赋值,将其传递给函数或从函数返回这样的操作不会复制值,而是会移动值
  • 源会把所有权转移给目标并变回未初始化状态,改由目标变量来控制值的生命周期
  • rust 会以每次只移动一个值的方式建立和拆除复杂的结构
// 在 rust 中,大多数类型赋值会将值从源转移给目标,而源会回到为初始化状态
let s = vec!["undo".to_string(), "ramen".to_string(), "soba".to_string()];
println!("{:?}", s);
let t = s;
println!("{:?}", t);
// println!("{:?}",s); // s 被移动了不能使用了
// let u = s; // s 被移动了不能使用了

// 如果需要同时访问,就必须显式地要求复制 调用向量的clone()方法
let s = vec!["undo".to_string(), "ramen".to_string(), "soba".to_string()];
let t = s.clone();

let u = s.clone();
println!("{:?}", s);
println!("{:?}", t);
println!("{:?}", u);

更多移动类操作

// 如果你将一个值转移给已初始化的变量.那么Rust 就会丢弃该变量之前值
let mut s = "Govinda".to_string();
s = "Siddhartha".to_string(); // 这里丢弃了值 “Govinda”

let mut s = "Govinda".to_string();
let t = s; // 转移给了t s 没有值
s = "Siddhartha".to_string(); // 所以再赋值什么也没丢弃
// 将参数传递给函数会将所有权转移给函数的参数,从函数返回一个值会将所有权转移给调用者,构建元祖会将值转移给元祖.
struct Person {
    name: String,
    birth: i32,
}

// 从函数中返回值
// 调用Vec::new() 构造一个新向量并返回,返回的不是指向此向量的指针,而是向量本身,它的所有权从Vec::new() 转移给了变量 coomposers,
let mut composers = Vec::new();
// 构造出新值
// 新Person 结构体的name 字短是用 to_string 的返回值初始化的,该结构体拥有这个字符串的所有权
// 将值传递给函数
// 这个Person结构体(不是指向它的指针)被传给了向量的push方法,此方法会将该结构体移动到向量的末尾.
// 向量接管了Persion的所有权,因此也间接接手了name这个String的所有权
composers.push(Person {
    name: "Plaestrina".to_string(),
    birth: 1525,
});
// 移动的永远是值的本身 而不是这些值拥有的堆存储

移动与控制流程

一般性原则是,如果一个变量的值有可能已经移走,并且从那之后尚未明确赋予其新值,那么它就可以被看作是位初始化状态

// 如果一个变量在执行了if表达式中的条件后仍然有值那么就可以在这两个分支中使用它
let x = vec![10,20,30];
let c {
    f(x); // 可以在这里移动x
}else{
    g(x); // 也可以在这里移动x
}
h(x)  // 错误,只要在之前肯定移动了x,x在这里就是未初始化状态
// 所以在循环中禁止在循环中进行变量移动
let x = vec![10,20,30];
while f() {
    g(x); // 错误: x已经在第一次迭代中移动出去了,在第二次移动中就是未初始化状态
}
// 除非在下一次迭代中明确赋予x一个新值,否则就会出错

移动与索引内容

移动会令其来源变成未初始化状态,因为目标将获得该值的所有权. 但并非值的每种拥有者都能变成未初始化状态

//  构建由字符串 "101","102",... "105" 组成的向量
let mut v = Vec::new();
for i in 101..106 {
    v.push(i.to_string());
}
// 从向量中随机抽取元素
let mut third = v[2]; // 不能移动到Vec索引结构之外

建议使用引用,因为你可能只是想访问而不是移动它

// 元素移出向量
// 1 从向量末尾弹出值
let fifth = v.pop();
assert_eq!(fifth, Some("105".to_string()));
// 2 将向量中指定索引处的值与最后一个互换,并把前者移动出来
let second = v.swap_remove(1);
assert_eq!(second, "102");
// 3 把要取的值和另一个互换
let third = std::mem::replace(&mut v[2], "substitute".to_string());
assert_eq!(third, "103");
println!("{:?}", v.len());

let mut compoers = Vec::new();

compoers.push(Person1 {
    name: Some("Palestrina".to_string()),
    birth: 1525,
});

compoers[0].name.take();
// let first_name = compoers[0].name; // 无法移动到索引结构之外
// 将None 留在原处,并将原始值的所有权转移给其调用值
let first_name = std::mem::replace(&mut compoers[0].name, None);
assert_eq!(first_name, Some("Palestrina".to_string()));
assert_eq!(compoers[0].name, None);

println!("{:#?}", compoers);
// take 与之前的replace有相同效果
compoers[0].name.take();

copy 类型,关于移动的例外情况

  • 值的移动涉及向量字符串和其它可能占用大量内存且复制成本比较高的类型
  • 移动能让这些类型的所有权清晰且赋值开销极低
  • 但对于像整数或字符这样的简单类型,就不需要这样处理了
let string1 = "somnambulance".to_string();
let string2 = string1; // 转移给 string2


let mun1:i32 = 36;
let mun2 = mun1; // 制作了完全独立的副本,(复制)
  • 除了那些被指定成Copy类型的类型,大多数类型会被移动,
  • 对于copy类型的值进行赋值会复制这个值,而不会移动它.赋值的源仍会保持已初始化和可用状态,并且具有与之前相同的值.
  • 把copy类型传递给函数和构造器的行为也是如此
  • 标准的copy类型包括所有机器整数类型和浮点数类型,char类型和bool类型,以及某些其它类型.copy 类型的元祖或固定大小的数组本身也是copy类型
  • 任何在丢弃值时需要做一些特殊操作的类型都不能是Copy类型
// 自定义类型
// 默认情况下, struct 类型和 enum 类型不是copy类型
let l = Label{
    number:3,
};
print(l);
println!("my label number is {}",l.number);  // 报错:因为Label 不是copy类型
  • 如果此结构体上的所有字段都是copy类型,那么可以通过 将属性 #[derive(Copy, Clone)] 放到自定义类型上来创建copy类型

  • 如果试图在一个不全是copy类型的结构体上这样做,则仍然不行

  • 为什么符合条件的用户定义类型不能自动成为copy类型呢?

  • 这是因为类型是否为copy 对于在代码中使用它的方式有着重大影响:copy 类型更灵活,因为赋值和相关操作不会把原始值变成未初始化状态.

  • 但对于类型的实现者而言,情况恰恰相反:copy类型可以包含的类型非常有限,而非copy类型可以在堆上分配内存并拥有其它种类的资源.

  • 因此创建一个copy类型代表着实现者的郑重承诺:如果以后确有必要将其非为非copy类型,则使用它的大部分代码可能需要调整

  • rust 中每次移动都是字节级的一对一的拷贝,并让源变成未知状态.复制也是这样,但会保留源的初始化状态

  • rust 原则是:各种开销对程序员来说应该是显而易见的,级别操作必须保持简单,而潜在的昂贵操作应该是显示的

  • 复制和克隆是某个类型具有的特征 ,是特型的示例

Rc 与 Arc: 共享所有权

  • 引用计数指针类型 Rc 和 Arc
  • Rc类型和Arc类型非常相似,唯一区别是Arc 可以安全的在线程之间直接共享,而普通的Rc会使用更快的非线程安全代码来更新其引用计数.
  • rust 的内存和线程安全保证的基石是:确保不会有任何值即是共享又是可变的
let mut s = Rc::new("shirataki".to_string());
let t  = s.clone();
let u  =s.clone();
println!("s is {} t is {} u is {}",s,t,u);
assert!(s.contains("shira"));
assert_eq!(t.find("taki"),Some(5));
println!("{}",u);
s.push_str("  noodles");
println!("{}",u);