专栏首页专注网络研发系统编程语言Rust特点介绍(2)—— 所有权系统

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

很抱歉,第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上查看。

本文分享自微信公众号 - LinuxerPub(LinuxerPub),作者:glinuxer

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • TCP的MTU Probe和MSS(1)

    在前面两篇文章中,我们研究了在TCP三次握手时MSS选项的值:一般情况下,都是由出口路由的MTU大小决定:MTU-40。也就是说,TCP在握手阶...

    glinuxer
  • Linux网络性能优化相关策略

    1. rx-checksumming:校验接收报文的checksum。

    glinuxer
  • DPDK之KNI原理

    DPDK是一个优秀的收发包kit,但它本身并不提供用户态协议栈,因此由将数据报文注入内核协议栈的需求,也就是KNI(Kernel NIC Interface)。...

    glinuxer
  • 分布式数据库企业级功能技术解密与最佳实践

    编辑IT大咖说 阅读字数: 2739用时: 10分钟 本文内容来源于彭旸在OSC源创会上海站上的主题演讲,IT大咖说为与开源中国合作的视频知识分享平台。 ? 内...

    IT大咖说
  • 使用django执行数据更新命令时报错:django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.00

    如果在重新封装更新用户表之前,已经更新了数据表,在数据库中已经有了django相关的依赖表,就会报错:

    玩蛇的胖纸
  • SQL反模式学习笔记9 元数据分裂

    1、将一张很长的表拆分成多张较小的表,使用表中某一个特定的数据字段来给这些拆分出来的表命名。

    张传宁老师
  • Android 性能测试之方向与框架篇

    借项目的开发周期,把思考了一段时间的场景化性能测试框架搭建起来,方案应用于项目的测试,也发现了产品中的不少问题。 接下来将用七八个篇幅详细记录一下心路历程。为分...

    陈帅
  • 【 Android 场景化性能测试专栏】方向与框架篇

    系列文章涉及场景化性能测试,包括耗电性能测试、内存泄漏测试、UI流畅度性能测试、app启动速度测试等。测试方案在实际应用中,已有不少产出,其本身具有可操作性强、...

    腾讯移动品质中心TMQ
  • SAS-一条群消息引发的思考(二)

    恩!又有一条群消息引发了我的思考,后续应该还会有接连不断的群消息引发小编的思考...

    Setup
  • 奇淫异巧之 PHP 后门

    早上看了一位小伙伴在公众号发的文章《php 后门隐藏技巧》,写的挺好,其中有一些姿势是我之前没见到过了,学到很很多。同时,这篇文章也引发了自己的一点点思考:“ ...

    信安之路

扫码关注云+社区

领取腾讯云代金券