< 返回版块

ianfor 发表于 2023-10-18 17:23


fn main() {
    let mut v = vec![];
    v.push(1);
    
    let v1 = &v[0];
    
    v.push(2);
    println!("{}", v1);
}

这种c++很经典的内存安全错误,始终没法理解编译器是如何检测的 v1的类型是&i32,为什么v1的借用会影响了v的借用

评论区

写评论
作者 ianfor 2023-10-25 09:53

受教了👍

👇
苦瓜小仔: > 为什么v1的借用会影响了v的借用

首先要知道你的代码中,引用的生命周期是什么样的(不展开方法调用、语法脱糖、宏展开等细节内容)

fn main() {
    let mut v = vec![];
    v.push(1);      // 1: Vec::push(&'1 mut Vec<i32>)
    
    let v1 = &v[0]; // 2: std::ops::Index::index(&'2 Vec<i32>, usize) -> &'2 i32
    
    v.push(2);      // 3: Vec::push(&'3 mut Vec<i32>)
    println!("{}", v1); // 4: std::io::_print(format_args!("{0}\n", v1)) 即使用 &'2 i32
}

引用是一种 primitive 类型,并且:

  1. 每创建一个引用,都附带一个生命周期
  2. 引用与引用之间的关系,是通过函数签名上的生命周期标注来约定的。例如上面的 Index trait 的函数/方法 fn index(&self, index: Idx) -> &Self::Output 约定返回的引用必须与 &self 活得一样长

v1 的类型为 &'2 i32,从地点 2 存活到地点 4,所以根据函数契约,那个 &'2 v 在 2、3、4 三处存活。

但在地点 3 有两种引用存活: &'3 mut v&'2 v,这违反了引用的 两大规则 之一(任何一个给定的时间/地点,要么只有一个 &mut T 存活,要么全部是 &T 存活)。

编译器是怎么实现的

当前借用检查的实现被称为 NLL,所以 RFC 2094 是必看的,因为它是 NLL 的设计文稿。尤其从 RFC 2094 (NLL): 什么是生命周期?它如何与借用检查器交互? 开始。

概括地说,编译器会将 Rust 源码脱糖到 MIR (除去所有语法糖的类 Rust 代码,比如上面我列的所有方法调用变成纯函数调用),然后追踪每个生命周期(生命周期视为控制流图中的一个点集)和约束,最终处理这些约束。

MIRI (MIR 解释器)使用 stack borrows(和新一代的 tree borrows)算法对当前借用检查进行建模,所以它也可以帮助你理解 Rust 的借用检查。

GUO 2023-10-19 09:05

大神👍 ,讲得浅显易懂,学习了,感谢分享!👌

--
👇
苦瓜小仔: > 为什么v1的借用会影响了v的借用

首先要知道你的代码中,引用的生命周期是什么样的(不展开方法调用、语法脱糖、宏展开等细节内容)

fn main() {
    let mut v = vec![];
    v.push(1);      // 1: Vec::push(&'1 mut Vec<i32>)
    
    let v1 = &v[0]; // 2: std::ops::Index::index(&'2 Vec<i32>, usize) -> &'2 i32
    
    v.push(2);      // 3: Vec::push(&'3 mut Vec<i32>)
    println!("{}", v1); // 4: std::io::_print(format_args!("{0}\n", v1)) 即使用 &'2 i32
}

引用是一种 primitive 类型,并且:

  1. 每创建一个引用,都附带一个生命周期
  2. 引用与引用之间的关系,是通过函数签名上的生命周期标注来约定的。例如上面的 Index trait 的函数/方法 fn index(&self, index: Idx) -> &Self::Output 约定返回的引用必须与 &self 活得一样长

v1 的类型为 &'2 i32,从地点 2 存活到地点 4,所以根据函数契约,那个 &'2 v 在 2、3、4 三处存活。

但在地点 3 有两种引用存活: &'3 mut v&'2 v,这违反了引用的 两大规则 之一(任何一个给定的时间/地点,要么只有一个 &mut T 存活,要么全部是 &T 存活)。

编译器是怎么实现的

当前借用检查的实现被称为 NLL,所以 RFC 2094 是必看的,因为它是 NLL 的设计文稿。尤其从 RFC 2094 (NLL): 什么是生命周期?它如何与借用检查器交互? 开始。

概括地说,编译器会将 Rust 源码脱糖到 MIR (除去所有语法糖的类 Rust 代码,比如上面我列的所有方法调用变成纯函数调用),然后追踪每个生命周期(生命周期视为控制流图中的一个点集)和约束,最终处理这些约束。

MIRI (MIR 解释器)使用 stack borrows(和新一代的 tree borrows)算法对当前借用检查进行建模,所以它也可以帮助你理解 Rust 的借用检查。

dr12730 2023-10-19 07:44

回答太棒了,深受启发,学习了~~

苦瓜小仔 2023-10-18 20:18

为什么v1的借用会影响了v的借用

首先要知道你的代码中,引用的生命周期是什么样的(不展开方法调用、语法脱糖、宏展开等细节内容)

fn main() {
    let mut v = vec![];
    v.push(1);      // 1: Vec::push(&'1 mut Vec<i32>)
    
    let v1 = &v[0]; // 2: std::ops::Index::index(&'2 Vec<i32>, usize) -> &'2 i32
    
    v.push(2);      // 3: Vec::push(&'3 mut Vec<i32>)
    println!("{}", v1); // 4: std::io::_print(format_args!("{0}\n", v1)) 即使用 &'2 i32
}

引用是一种 primitive 类型,并且:

  1. 每创建一个引用,都附带一个生命周期
  2. 引用与引用之间的关系,是通过函数签名上的生命周期标注来约定的。例如上面的 Index trait 的函数/方法 fn index(&self, index: Idx) -> &Self::Output 约定返回的引用必须与 &self 活得一样长

v1 的类型为 &'2 i32,从地点 2 存活到地点 4,所以根据函数契约,那个 &'2 v 在 2、3、4 三处存活。

但在地点 3 有两种引用存活: &'3 mut v&'2 v,这违反了引用的 两大规则 之一(任何一个给定的时间/地点,要么只有一个 &mut T 存活,要么全部是 &T 存活)。

编译器是怎么实现的

当前借用检查的实现被称为 NLL,所以 RFC 2094 是必看的,因为它是 NLL 的设计文稿。尤其从 RFC 2094 (NLL): 什么是生命周期?它如何与借用检查器交互? 开始。

概括地说,编译器会将 Rust 源码脱糖到 MIR (除去所有语法糖的类 Rust 代码,比如上面我列的所有方法调用变成纯函数调用),然后追踪每个生命周期(生命周期视为控制流图中的一个点集)和约束,最终处理这些约束。

MIRI (MIR 解释器)使用 stack borrows(和新一代的 tree borrows)算法对当前借用检查进行建模,所以它也可以帮助你理解 Rust 的借用检查。

作者 ianfor 2023-10-18 19:09

--
👇
ianfor: 额 我的意思是v1类型是&i32, 他是如何让v不能在借用的 编译器是怎么实现的

为什么v1会导致v的借用,不是很理解这个

--
👇
ribs: i32 是编译器推断出来的,没有任何地方表明他是其他类型,那他默认就是 i32

push 接受的是可变引用&mut self,可变引用不能跟不可变引用共存,这是借用规则定的

作者 ianfor 2023-10-18 19:03

额 我的意思是v1类型是&i32, 他是如何让v不能在借用的 编译器是怎么实现的

--
👇
ribs: i32 是编译器推断出来的,没有任何地方表明他是其他类型,那他默认就是 i32

push 接受的是可变引用&mut self,可变引用不能跟不可变引用共存,这是借用规则定的

ribs 2023-10-18 18:10

i32 是编译器推断出来的,没有任何地方表明他是其他类型,那他默认就是 i32

push 接受的是可变引用&mut self,可变引用不能跟不可变引用共存,这是借用规则定的

1 共 7 条评论, 1 页