rust引用和借用的使用小结

目录
  • 引用和借用
    • 引用的使用
    • 可变引用与不可变引用
    • NLL
    • 总结

引用和借用

如果每次都发生所有权的转移,程序的编写就会变得异常复杂。因此rust和其它编程语言类似,提供了引用的方式来操作。获取变量的引用,称为借用。类似于你借别人的东西来使用,但是这个东西的所有者不是你。引用不会发生所有权的转移

引用的使用

在rust中,引用的语法非常简单。通过&来取引用,通过*来解引用。例如:

fn main() {
    let s1: String = "Hello".to_string();
    let s2: &String = &s1;       // s2引用s1

    println!("{s1}");
    println!("{s2}");
}

这段代码可以正常运行,因为s2引用的s1,不会发生所有权的转移。再来看一个例子,通过引用来传递函数参数。

fn main() {
    let s = "Hello".to_string();
    let len = calculate_length(&s);     // 引用

    println!("{s}");
    println!("{len}");
}

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

在calculate_length中,s是一个引用,它不具备所有权,因此在函数调用结束的时候,s的作用域虽然结束了,但是不会调用drop。

可变引用与不可变引用

在刚才的例子中,只是获取了字符串的长度,相当于我们读取了变量。在rust中,引用默认也是不可变的,如果需要通过引用修改变量,那么必须使用可变引用。可变引用和可变变量一样,都是通过关键字mut来实现的。例如:

fn main() {
    let mut s = String::from("hello");
    change(&mut s);     // 可变引用
    println!("{s}");
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

这段代码输出hello, world,可见我们通过可变引用修改了s的值,但是在这个过程中并没有涉及所有权的转移。

事实上,事情并没有这么简单。可变引用并不是可以随心所欲的被使用。它有一个很大的限制,“同一作用域,一个变量只能有一个可变引用”。例如:

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

    let r1 = &mut s;
    let r2 = &mut s;     // 同一作用域,无法创建两个可变引用。

    println!("{}, {}", r1, r2);
}

两个可变引用,可能会出现“同时写入”这种情况,导致内存不安全的情形发生。如果在不同的作用域,可以有多个可变引用,但是它们不能同时被拥有。例如:

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

    {
        let r1 = &mut s;
        println!("{r1}");
    } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

    let r2 = &mut s;
    println!("{r2}");
}

同时rust也不允许同时存在可变引用和不可变引用。因为不可变引用可能会因可变引用变得失效。下面以一段C++代码来说明这一点。

#include<vector>
#include<string>
#include<iostream>

using namespace std;

int main() {

    // 可读引用因可变引用而变得失效
    vector<string> vs;
    vs.push_back("hello");

    auto & elem = vs[0];

    vs.push_back("world");      // push_back会导致vs指向的整段内存被重新分配并移到了另一个地址,原本迭代器里面的引用就全部变成悬垂指针了。

    cout << vs[0] << endl;
    cout << elem << endl;       // 试图使用悬垂指针

    return 0;
}

这段代码执行之后,结果如下所示:

hello
Segmentation fault (core dumped)

很明显,这里的段错误正是由于试图使用悬垂指针引起的。而rust特殊的可变引用和不可变引用机制避免了这种错误的发生。例如:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}   // 返回s的引用,函数结束,s移出作用域,调用drop函数清理内存,那么返回的引用将会变成悬垂引用,从而引发错误。

这段rust代码无法编译通过,从而避免了像上面C++代码那样的运行时错误。

正如Rust 程序设计语言中所言

这一限制以一种非常小心谨慎的方式允许可变性,防止同一时间对同一数据存在多个可变引用。新 Rustacean 们经常难以适应这一点,因为大部分语言中变量任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:

两个或更多指针同时访问同一数据。
至少有一个指针被用来写入数据。
没有同步数据访问的机制。

Rust 的编译器一直在优化,早期的时候,引用的作用域跟变量作用域是一致的,这对日常使用带来了很大的困扰,你必须非常小心的去安排可变、不可变变量的借用,免得无法通过编译,例如以下代码:

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

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束

    let r3 = &mut s;
    println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
  // 新编译器中,r3作用域在这里结束

在老版本的编译器中(Rust 1.31 前),将会报错,因为 r1 和 r2 的作用域在花括号 } 处结束,那么 r3 的借用就会触发 无法同时借用可变和不可变的规则。但是在新的编译器中,该代码将顺利通过,因为 引用作用域的结束位置从花括号变成最后一次使用的位置,因此 r1 借用和 r2 借用在 println! 后,就结束了,此时 r3 可以顺利借用到可变引用。

NLL

对于这种编译器优化行为,Rust 专门起了一个名字 —— Non-Lexical Lifetimes(NLL),专门用于找到某个引用在作用域(})结束前就不再被使用的代码位置。

总结

  • 总的来说,借用规则如下:
  • 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用引用必须总是有效的 参考资料

Rust 程序设计语言
Rust单线程下为什么还是只能有一个可变引用呢?
Rust语言圣经

到此这篇关于rust引用和借用的文章就介绍到这了,更多相关rust引用和借用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Rust如何进行模块化开发技巧分享

    目录 Rust如何进行模块化开发? Package和Create Cargo的惯例 Create的作用 定义module来控制作用域和私有性 路径Path 私有边界(private boundary) pub关键字 super关键字 pub struct pub enum Use关键字 use的习惯用法 as关键字 使用 pub use 重新导出名称 导入外部包 如何将模块放入其他文件? 类似es6的模块化,Rust通过package.create.module来实现代码的模块化管理 Rust如

  • Rust应用调用C语言动态库的操作方法

    目录 外部功能接口FFI UDP套接字的读超时 Rust调用C语言动态库中的函数 避免重复造轮子,使用Rust官方C语言库 外部功能接口FFI 虽然高级(脚本)编程语言的功能丰富,表达能力强,但对底层的一些特殊操作的支持并不完善,就需要以其他编程语言来实现.调用其他编程语言的接口,被称为Foreign Function Interface,直译为外部功能接口.该接口通常是调用C语言实现的外部功能模块,因为C语言接近于全能,几乎任何功能都能够实现:正如同使用汇编语言也可以实现很多功能一样,但开发效

  • 向Rust学习Go考虑简单字符串插值特性示例解析

    目录 fmt.Printf 或 fmt.Sprintf 写拼装字符串业务 简单字符串插值 其他语言例子 Swift Kotlin C Rust 争论矛盾点 总结 fmt.Printf 或 fmt.Sprintf 写拼装字符串业务 在日常开发 Go 工程中,我们经常会用 fmt.Printf 或 fmt.Sprintf 去写类似的拼装字符串的业务. 如下代码: fmt.Printf("Hello Gopher %s, you are %d years old and you're favorite

  • 从迷你todo 命令行入门Rust示例详解

    目录 一个迷你 todo 应用 需要安装的依赖 文件目录组织 主文件 读取文件 状态处理工厂函数 Trait(特征) Create trait Get trait Delete trait Edit trait 导出 trait 为 struct 实现 trait Pending Done 导出 struct Process 输入处理 最后 一个迷你 todo 应用 该文章将使用 Rust 从零去做一个入门级别的 TODO 命令行应用 你将学会什么? 基本的命令行操作 文件读写和文件结构组织 我

  • Rust Atomics and Locks内存序Memory Ordering详解

    目录 Rust内存序 重排序和优化 happens-before Relexed Ordering Release 和 Acquire Ordering SeqCst Ordering Rust内存序 Memory Ordering规定了多线程环境下对共享内存进行操作时的可见性和顺序性,防止了不正确的重排序(Reordering). 重排序和优化 重排序是指编译器或CPU在不改变程序语义的前提下,改变指令的执行顺序.在单线程环境下,重排序可能会带来性能提升,但在多线程环境下,重排序可能会破坏程序

  • Rust 所有权机制原理深入剖析

    目录 what's ownership? Scope (作用域) ownership transfer(所有权转移) move clone copy References and Borrowing(引用和借用) Mutable References(可变引用) Dangling References(悬垂引用) what's ownership? 常见的高级语言都有自己的 Garbage Collection(GC)机制来管理程序运行的内存,例如 Java.Go 等.而 Rust 引入了一种全

  • rust引用和借用的使用小结

    目录 引用和借用 引用的使用 可变引用与不可变引用 NLL 总结 引用和借用 如果每次都发生所有权的转移,程序的编写就会变得异常复杂.因此rust和其它编程语言类似,提供了引用的方式来操作.获取变量的引用,称为借用.类似于你借别人的东西来使用,但是这个东西的所有者不是你.引用不会发生所有权的转移. 引用的使用 在rust中,引用的语法非常简单.通过&来取引用,通过*来解引用.例如: fn main() { let s1: String = "Hello".to_string()

  • vue引用json文件的方法小结

    相信大家都有被后端数据支配过 废话不多说 直接上代码 1.解决怎么从控制台把数据 移到json文件中 直接右击复制值 var getData = require("./taifeng.json"); // 直接引入json文件 console.log(getData); vue中引用Json文件 我们用import引用文件的时候,被引用的文件都会用export暴漏,比如js,而有一些文件不需要暴漏,如Json.img(图片).css; import 引用Json文件 import aa

  • 详解Rust中的方法

    目录 Rust中的方法 方法的简单概念 定义方法 Rust自动引用和解引用 带参数的方法 小结 Rust中的方法 方法其实就是结构体的成员函数,在C语言中的结构体是没有成员函数的,但是Rust毕竟也是一门面向对象的编程语言,所以给结构体加上方法的特性很符合面向对象的特点. 方法的简单概念 方法(method)与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码.不过方法与函数是不同的,因为它们在结构体的上下文中被定义,并且它们第一个参数总是

  • 深入了解Rust的切片使用

    目录 为什么要有切片 字符串切片 其它类型的切片 为什么要有切片 除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice),切片允许我们引用集合中某一段连续的元素序列,而不是整个集合. 考虑这样一个小问题:编写一个搜索函数,它接收字符串作为参数,并将字符串中的首个单词作为结果返回.如果字符串中不存在空格,那么就意味着整个字符串是一个单词,直接返回整个字符串作为结果即可. 让我们来看一下这个函数的签名应该如何设计: fn first_word(s: &String) -> ?

  • 深入了解Rust的生命周期

    目录 楔子 生命周期标注语法 结构体中的生命周期标注 生命周期的省略 方法中的生命周期标注 同时指定生命周期和泛型 楔子 Rust 的每个引用都有自己的生命周期,生命周期指的是引用保持有效的作用域.大多数情况下,引用是隐式的.可以被推断出来的,但当引用可能以不同的方式互相关联时,则需要手动标注生命周期. fn main() {     let r;     {         let x = 5;         r = &x;     }  // 此处 r 不再有效     println!(

  • Rust Atomics and Locks并发基础理解

    目录 Rust 中的线程 线程作用域 所有权共享 借用和数据竞争 内部可变 rust 中的线程安全 Send 和 Sync 线程阻塞和唤醒 Rust 中的线程 在 Rust 中,线程是轻量级的执行单元,可以并行执行多个任务.Rust 中的线程由标准库提供的 std::thread 模块支持,使用线程需要在程序中引入该模块.可以使用 std::thread::spawn() 函数创建一个新线程,该函数需要传递一个闭包作为线程的执行体.闭包中的代码将在新线程中执行,从而实现了并发执行.例如: use

  • rust生命周期详解

    目录 rust生命周期 借用检查 函数中的生命周期 手动声明生命周期 结构体中的生命周期 生命周期消除 三条消除原则 生命周期约束 静态生命周期 rust生命周期 生命周期是rust中用来规定引用的有效作用域.在大多数时候,无需手动声明,因为编译器能够自动推导.当编译器无法自动推导出生命周期的时候,就需要我们手动标明生命周期.生命周期主要是为了避免悬垂引用. 借用检查 rust的编译器会使用借用检查器来检查我们程序的借用正确性.例如: #![allow(unused)] fn main() {

  • C++关于const与引用的分析讲解

    目录 一.关于 const 的疑问 二.关于引用的疑问 三.小结 一.关于 const 的疑问 const 什么时候为只读变量?什么时候是常量? const 常量的判别准则 只有用字面量初始化的 const 常量才会进入符号表 使用其他变量初始化的 const 常量仍然是只读变量 被 volatile 修饰的 const 常量不会进入符号表 注:在编译期间不能直接确定初始值的 const 标识符,都被作为只读变量处理. const 引用的类型与初始化变量的类型 如果相同,则初始化变量成为只读变量

  • Python数组定义方法

    本文实例讲述了Python数组定义方法.分享给大家供大家参考,具体如下: Python中没有数组的数据结构,但列表很像数组,如: a=[0,1,2] 这时:a[0]=0, a[1]=1, a[[2]=2,但引出一个问题,即如果数组a想定义为0到999怎么办?这时可能通过a = range(0, 1000)实现.或省略为a = range(1000).如果想定义1000长度的a,初始值全为0,则 a = [0 for x in range(0, 1000)] 下面是二维数组的定义: 直接定义: a

  • c++智能指针的超详细讲解

    目录 1.什么是智能指针 2.原始指针的问题 3.unique_ptr 4.shared_ptr 5.shared_ptr使用需要注意的点 5.1 不能将一个原始指针初始化多个shared_ptr 5.2.循环引用问题 6.智能指针小结 总结 1.什么是智能指针 从比较简单的层面来看,智能指针是RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制对普通指针进行的一层封装.这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方

随机推荐