前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >系统编程语言Rust特点介绍(2)—— 所有权系统

系统编程语言Rust特点介绍(2)—— 所有权系统

作者头像
glinuxer
发布2020-02-03 19:08:57
1.2K0
发布2020-02-03 19:08:57
举报

很抱歉,第2篇距离第1篇长达3个月。。。工作繁忙加上家里事多。。。不找客观原因了,咱们开始聊聊Rust的所有权系统。

Rust的所有权系统主要有3个特性组成:Ownership(所有权)、Borrowing(借用)和Lifetimes(生命周期)。其中ownership和borrowing基本上是联合使用,实现了下面3个效果。

  1. 在没有gc的条件下,保证了内存安全。(gc对于系统应用来说,是一个比较可怕的难题。因为你很难控制gc造成的性能抖动。)
  2. 每一个变量的值,有且只有一个owner。
  3. 当owner离开scope时,自动drop(这里的drop可以理解为C++中的析构)。

熟悉C++的同学,看到这3个特性,应该可以想到这基本上就是unique_ptr的特性,而Rust对普通变量(非动态申请)也做了这样的限制。通过这样的限制,rust就就解决了常见的因并发竞争引发的内存问题。因为同一时刻,变量的owner只有一个,在编译阶段保证了不会有并发竞争的问题。

请看下面的代码:

在这段代码中s1为一个字符串变量,也就是“hello”字符串的owner。下面一行声明s2 = s1,就发生了ownership的转移。此时,“hello”字符串的owner不再是s1,而变成了s2。这样,在后面println打印的时候,s1不再拥有值,就无法通过编译。

这里的编译错误很明确,s1的值被move走了,也可以说被s2 borrow“借”走了。

细心的同学可能还发现除了红色的错误外,还有一行蓝色的提示String没有实现Copy特性。在这里可以略微解释一下,对于没有实现Copy特性的类型来说,赋值操作就进行了所有权转移。而实现了Copy特性后,会发生真正的Copy动作。在Rust内置类型中,有的实现了Copy特性,有的没有实现,按照Rust的说法,对于实现代价很小且常用的类型如整数,就拥有Copy特性,而String类型则没有。同时,使用者可以为已有类型如这里的String类型,增加Copy trait(特性),就避免了上面代码中的所有权转移。

对于拥有Copy trait的类型来说,赋值操作执行的是Copy动作,而不是所有权的转移。这样的话,依然是一个值只有一个owner,并没有违反Rust的设计原则。

再回到现在的主题,String没有实现Copy trait,而我们现在又不会扩展内置类型,怎么办呢?我不想一赋值就转移所有权怎么办?针对这种情况,可以显示调用clone方法来实现。

接下来请看下面的代码。

前面几行代码用来展示整数的赋值操作并不会发生所有权转移。在大括号中的代码,s2变量clone了s1的值,而不是borrow了所有权。在后面的代码中,s1又追加了新的字符串。看一下输出结果。

x和y都可以顺利打印,s1的值是“hello, world",而s2仍然是"hello"。这说明:1. 整数的赋值操作不会发生所有权转移;2. s2 clone s1后,实际上是拥有了新的值,与s1完全分道扬镳。

现在大家对Rust的ownership有一定了解了吧。接下来看一个例子,这是从其他语言切换到Rust后,基本上都会感到不适的示例。

// move example2
println!("*********************** example4 *****************************");
let mut s3 = String::from("hello");
show_str(s3);
//s3 = show_str2(s3);
//show_str3(&s3);
println!("s3 is {}", s3);

fn show_str(s:String) {
    println!("s is {}", s);
}

在上面的代码中,定义了一个字符串变量s3,然后将其传入到show_str函数中打印。这在一般的程序语言中,是非常常见的操作。然后在Rust中。。。

因为ownership的关系,导致编译报错。原因是在调用show_str的时候,s3的值的所有权被转移给了show_str函数。因此在调用show_str之后,就不能合法的使用s3了。

解决这个问题的方案也很简单,或者让函数把owership传递回来,或者不改变所有权。下面是两种实现方案。

// move example2
println!("*********************** example4 *****************************");
let mut s3 = String::from("hello");
//show_str(s3);
s3 = show_str2(s3);
show_str3(&s3);
println!("s3 is {}", s3);

#[allow(dead_code)]
fn show_str2(s:String) -> String {
    println!("s is {}", s);
    s
}

#[allow(dead_code)]
fn show_str3(s: &String) {
    println!("s is {}", s);
}

在上面的代码中,show_str2通过参数s获得了String的ownership,但是又通过返回值将ownership归还。而show_str3则将参数定义为引用类型,这样在调用show_str3时,就不会发生ownership的转移。

下面是上述代码的运行结果。

在show_str3中,我们见到了Rust中“引用”的语法。这时,我们要提出一个疑问,既然Rust支持引用,那岂不是又可以有多个变量拥有同一个值的所有权了?这样岂不是违反了Rust的设计原则和安全限制,可能会导致对同一个值的并发修改?

答案自然是否定的。请看下面的示例代码:

    // double mut referenc
    println!("*********************** example5 *****************************");
    let r1 = &s3;
    let r2 = &s3;
    println!("r1 is {}, r2 is {}", r1, r2);

s3还是上面定义的可变String类型,这里的r1和r2都是对s3的引用,准确的说是常量引用(记住Rust中变量类型默认都是常量的)。引用呢,实际上就是指向了s3值的内存。编译运行,结果如下:

既没有编译错误,运行结果也如预期。为什么呢?因为这样的语法是安全的,这里的r1和r2都是常量引用,只能读取不能更改。自然这里就没有并发竞争的逻辑,因此Rust允许这样的编码逻辑。

如果我们在定义了常量引用之后,又企图使用s3修改其值,会怎么样呢?

    // double mut referenc
    println!("*********************** example5 *****************************");
    let r1 = &s3;
    let r2 = &s3;
    s3.push_str("aaa");
    //let r3 = &mut s3;
    println!("r1 is {}, r2 is {}", r1, r2);

错误提示,因为有r1和r2两个常量引用,那么就不能再修改s3了,即使之前s3被定义为mutable的变量。如果没有尝试修改s3的值,代码编译运行就没有问题,这说明了Rust的安全检查不仅仅是简单的语法限制,同时还做了上下文分析。

现在,我们再定义一个可变的引用类型r3来测试。

    // double mut referenc
    println!("*********************** example5 *****************************");
    let r1 = &s3;
    let r2 = &s3;
    let r3 = &mut s3;
    println!("r1 is {}, r2 is {}", r1, r2);

使用cargo build编译。

出现编译错误。提示r1进行了immutable borrow,就不能使用r3再进行mutable borrow。我们也可以这样理解,已经有了只读的引用,为了保证安全性,自然不能再进行可写的引用指向了。

目前这里的错误是因为前面有只读的引用,如果我们把r1和r2的代码注释掉,逻辑是否就正确了呢?

    // double mut referenc
    println!("*********************** example5 *****************************");
    //let r1 = &s3;
    //let r2 = &s3;
    let r3 = &mut s3;
    //println!("r1 is {}, r2 is {}", r1, r2);
    println!("r3 is {}", r3);

编译没有错误,运行也正常。也许与某些同学的预期不符。因为r3既然是mut 类型的引用,岂不是有两个变量(加上s3)可以同时更改s3的值了呢?

让我们再加上s3的打印。

    println!("*********************** example5 *****************************");
    //let r1 = &s3;
    //let r2 = &s3;
    let r3 = &mut s3;
    //println!("r1 is {}, r2 is {}", r1, r2);
    println!("r3 is {}, s3 is {}", r3, s3);

符合预期出现编译错误,再次验证Rust的所有权系统,是不能允许有两个变量有同时修改一个值的可能性。

至此,对于Rust的ownership,我想大家已经有了一定的认识了。Rust的所有权系统中的lifetime,只能等待另外一篇文章介绍了。另外,一些好奇的同学可能会想到,截止到目前为止,这些都是单线程程序。Rust如何在多线程,真正的并发编程下,保证的内存安全呢?我争取很快再写两篇,一篇介绍lifetime,另外一篇介绍Rust的并发安全性。

PS:关于Rust系列文章的示例代码,大家可以在https://github.com/gfreewind/RustTraining上查看。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 LinuxerPub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档