Rust: 借用

前面我们介绍了所有权的相关概念,下面用一段代码简单回顾一下:

fn main() {
    let a = String::from("hello");
    
    // a的所有权转移到了b
    let b = a;
    
    // b的所有权转移到了函数内部
    let len = get_len(b);
    
    // 此时只能访问len变量
    println!("{}", len);
    
    // 报错:a和b所有权已转移
    println!("{} {}", a, b);
}

fn get_len(s: String) -> usize {
    s.len()
}

变量丢掉自己的所有权很容易,可要想重新获得,那就得费点功夫了,比如说在 get_len() 函数执行完毕,必须将参数变量 s 作为结果,原封不动地返回。

要是每个函数都这样写,岂不是要疯了?有没有别的的方式,可以简化这种繁琐的过程呢,答案是 借用

在变量前面加一个 & 符号,就能用来表示借用。借用只会 租借 原变量的所有权,在获得数据访问能力的同时,不会剥夺原变量对数据的所有权。

我们来看下面代码:

fn main() {
    let s1 = String::from("hello");
    
    // &表示借用
    let s2 = &s1;
    
    // 变量s1和s2都能访问
    // 打印结果 "hello" "hello"
    println!("{:?} {:?}", s1, s2);
}

在调用函数时,也是使用同样的手法:

fn main() {
    let s = String::from("hello");
    
    // 传递借用参数
    let len = get_len(&s);

    // 变量s依然能够访问
    // 打印结果 "hello" 5
    println!("{:?} {:?}", s, len);
}

// 这里不会引起所有权变化
fn get_len(s: &String) -> usize {
    s.len()
}

那么通过借用生成的变量,究竟是一种什么样的存在呢?

实际上,借用会产生一个指针,这个指针会指向原变量,通过原变量,最终能访问到指定内存地址的数据。所以,对于借用生成的变量,我们称之为 借用指针

借用指针 有时候也被称为 引用,两者要表达的含义是一致的。为了表述清晰,本文将统一使用 借用借用指针 这两个词。

上面我们看到的借用是 只读 的,也就是说,你只能通过它访问数据,不能通过它对数据进行更改。原因很简单,如果借用指针偷偷修改了数据,原变量对此一无所知,那是要出大问题的,编译器绝不允许这种情况发生。

但如果在某些场景下,确实需要通过借用指针来修改数据,那我们就必须为 原变量借用指针 都加上 mut 关键字修饰,显式地告诉编译器,此处的借用指针可以修改数据,我们称这种借用为 可变 借用,且看下面代码:

fn main() {
    // 1. 源头就必须是mutable的
    let mut s = String::from("hello");

    // 2. 传递参数时必须有mut修饰
    change(&mut s);
    
    // 打印结果 "hello world"
    println!("{:?}", s);
}

// 3. 形参也必须有mut修饰
fn change(s: &mut String) {
    s.push_str(" world");
}

现在我们来对比一下 只读借用可变借用 的不同:

  • 只读借用使用 & 来表示;可变借用使用 &mut 来表示。
  • 只读借用只可访问数据,不能更改数据;可变借用既能访问数据,也可以对数据进行更改。

既然只读借用只可访问数据,不能修改数据,那么多个借用就可以共享一份数据,这是内存安全的:

fn main() {
    let s = String::from("hello");

    let a = &s;
    let b = &s;
    
    println!("{} {} {}", s, a, b);
}

反之,可变借用就不能共享,&mut 型借用是独占的,在指定作用域中,只要存在 &mut 型借用,就不允许其他借用出现,下面两段代码都将会编译失败:

fn main() {
    let mut s = String::from("hello");

    // 报错:同一时刻 两种类型的借用冲突
    let a = &s;
    let b = &mut s;
    
    println!("{} {} {}", s, a, b);
}
fn main() {
    let mut s = String::from("hello");

    // 报错:同一时刻 最多一个可变借用
    let a = &mut s;
    let b = &mut s;
    
    println!("{} {} {}", s, a, b);
}

这当然也是基于安全考虑的,当作用域中存在一个可变借用时,对其他借用来说,都是一种潜在的风险,说不定这家伙偷偷就把数据篡改了呢,所以 Rust 禁止这种情况的发生。

需要注意的是,在其他借用的生命周期已完结、资源被销毁后,这个限制会重新放开,此时我们可以继续在原变量上,定义新的可变借用:

fn main() {
    let mut s = String::from("hello");

    let a = &s;
    
    println!("{}", a);

    let b = &mut s;
    
    println!("{}", b);
    
    let c = &mut s;
    
    println!("{}", c);
}

此外,当可变借用存在时,原变量会转为冻结状态,不可读写,只有在当前借用被销毁后,原变量才能恢复访问和修改:

fn main() {
    let mut s1 = String::from("hello");
    
    let s2 = &mut s1;
    
    s2.push_str(" world");
    
    println!("{}", s2);
    
    // 变量s2销毁后才能访问s1
    println!("{}", s1);
}

Rust 还规定,借用指针不能比原变量存活得更久,因为这样很容易出现 垂悬指针 的问题,就像下面这段代码一样:

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

// 编译报错
fn get_str() -> &String {
    let s = String::from("hello");
    
    &s
}

在上面的 get_str() 方法内部,我们定义了一个变量,然后将它的借用指针作为结果返回,问题在于,这个变量在函数执行完即被销毁,留下一个孤零零的指针,访问不到数据,我们称其为垂悬指针,Rust 不允许这种异常现象发生,所以编译时会导致报错。

那怎么办呢,我们可以对代码稍作改进,不再返回借用指针,而是直接返回变量本身,将变量的所有权转移到外部,这样就可以继续访问了:

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

fn get_str() -> String {
    let s = String::from("hello");
    
    // 交出所有权
    s
}

最后,我们来总结一下借用的特点和规则:

  • 借用只会租借变量的所有权,不能实际拥有所有权。
  • 借用会产生一个指针,我们也可以称之为引用。
  • 多个只读借用可以共享同一份数据。
  • 可变借用在指定作用域中不能和其他借用共享数据,具有独占性。
  • 当可变借用存在时,原变量会转为冻结状态,不可读写,直到可变借用被销毁。
  • 借用指针不能比原变量存活得更久。

以上就是借用的相关内容了,里面的坑还是挺多的,初学者可能得多花点时间去理解,等这些规则都熟练掌握了,就能任性地去写 Rust 了。😃

posted @ 2019-12-01 19:19  liuhe688  阅读(287)  评论(0编辑  收藏  举报