跳过正文

借用:refer/borrow

··3127 字
Rust
rust-lang - 这篇文章属于一个选集。
§ 6: 本文

RAII :Rust 中每一个资源或对象只能有一个所有者,当它离开作用域 scope 时,它的 Drop trait 被调用来释放资源。

Rust 对象有唯一的所有权,可以通过赋值表达式、函数传参、函数返回、添加到 struct/tuple 和集合等来转移所有权, 原来的变量变成 未初始化状态, 不能再使用, 可以避免 dangling pointers。

这种 move 转移的方式,在性能上和安全性上都是非常有效的(避免了栈和堆内存拷贝),Rust 编译器会对转移的变量进行错误检查。

  • 如果对象类型实现了 Copy,则不会进行 move 转移。一般情况下堆上分配的对象,例如 String/Vec 没有实现 Copy。自定义类型,如 struct/enum/union 也没有实现 Copy。
// 所有权转移:可以从栈上返回对象
fn new_person() -> Person {
    let person = Person {
        name : String::from("Hao Chen"),
        age : 44,
        sex : Sex::Male,
        email: String::from("[email protected]"),
    };
    return person;
}

fn main() {
    let p  = new_person();
}

fn main() {
    let _box2 = Box::new(5i32);

    // 嵌套 scope
    {
        let _box3 = Box::new(4i32); // 在堆上分配 i32 内存
        // `_box3` is destroyed here, and memory gets freed
    }

    for _ in 0u32..1_000 {
        create_box();
    }

    // `_box2` is destroyed here, and memory gets freed
}

为了不获得对象所有权的情况下使用对象,Rust 通过借用操作(borrow/mut borrow)来获得对象的引用。或者通过引用计数类型,如 Rc/Arc 来 clone 对象(不会拷贝堆数据)。

Rust borrow checker 对所有权和借用进行检查,违反时编译报错:

  1. 对象可以多次共享借用,但是只能一次可变借用。
  2. 可变借用的对象本身必须是可变的,即只能对 mut 对象进行 &mut 可变借用,或从已有 &mut 借用生成新的&mut 借用;
  3. &mut T 可以自动隐式协变( coerced into )到 &T 类型,所以在需要 &T 的地方可以传入 &mut T 类型值,但是反过来不行;
  4. 对象在存在借用的情况下, 不能被修改或 move ;《== 借用冻结
let mut a = 123;
let ar = &a;
// a = 456; // Error:cannot assign to `a` because it is borrowed,借用冻结
println!("{ar}")

let mut s = String::from_str("new string").unwrap();
// s 本身必须是 mut 类型才能被 &mut,在有 &mut 的情况下,原始值 s 不能再被访问。
let sm = &mut s;
// cannot borrow `s` as immutable because it is also borrowed as mutable
// println!("Result: {s} {sm}");
println!("Result: {sm}"); // OK

let s2 = &mut String::from_str("new string").unwrap();
// let sm2 = &mut s2; // s2 不是 mut 类型,不能 &mut;
s2.push_str(" abc"); // s2 是 &mut 借用类型,所以可以修改值
println!("s2: {s2}");

let s3 = s2; // s3 也是 &mut 类型, 故可以修改
s3.push_str(" def");
println!("s3: {s3}");

// s3 是 &mut 类型,但是不能通过 *s3 解引用来 move 变量值(均不能通过借用来 move 值)
// let s4 = *s3;

// 在已经 &mut T 的情况下,原始值不能再被访问:借用冻结
let mut s = String::from_str("new string").unwrap();

let sm = &mut s; // s 本身必须是 mut 类型,才能被 &mut;
s.push_str("abc"); // Error: 在 sm 后续继续使用的情况下,原来的 s 不能再使用。
// 不能同时使用 s 和 sm。
println!("Result: {s} {sm}");

struct MyStruct(u8, String);
let mut ms = MyStruct(3, "test".to_string());
let msm = &mut ms;
let msm2 = &mut msm.1; // 可以从 &mut 创建出另一个 &mut
msm2.push_str(" def");

Rust borrow checker 将变量视为所有权树的根,所以如果要修改对象的成员(如 struct field)、成员的子对象等,一般是使用 &mut self,这样可以将 &mut 引用从对象的根传递到 最内层对象 。这里的修改包括 3 方面:

  1. 对对象本身进行修改;
  2. 对对象的成员进行修改;
  3. 调用对象或成员的方法,这些方法会改变对象的状态和内部字段等;

Rust 提供了 内部可变性 机制,来让使用共享借用 &self 的方法修改 Cell/RefCell/Mutex/Rwlock 等对象的内部状态。

// data 不可变。
let data = Arc::new(Mutex::new(0));
// MutextGuard 支持内部可变性,故可以获得 mut data。
let mut data = data.lock().unwrap();
*data += 1;

在转移对象的所有权时可以改变它的可变性(因为转移到的变量完全拥有该对象,所以可以决定它的可见性):

let immutable_box = Box::new(5u32);
println!("immutable_box contains {}", immutable_box);

// Mutability error
//*immutable_box = 4;

// 转移对象时,可以改变它的可变性
let mut mutable_box = immutable_box;
println!("mutable_box contains {}", mutable_box);
*mutable_box = 4; // 修改可变对象值
println!("mutable_box now contains {}", mutable_box);

不能通过借用(无论是可变还是共享借用)来对对象的所有权进行转移,例如 let v2 = *V ,解决办法:

  1. V 如果实现 Copy/Clone trait,则可以调用对应的方法;
  2. 使用 std::mem::replace(&dest, src) 将 src 值替换 dest,同时返回 dest 的值:
// Error 的例子
struct Buffer {
    buffer : String,
}
struct Render {
    current_buffer : Buffer,
    next_buffer : Buffer,
}
impl Render {
    fn update_buffer(&mut self, buf : String) {
        // error[E0507]: cannot move out of `self.next_buffer` which is behind a mutable reference
        // move occurs because `self.next_buffer` has type `Buffer`, which does not implement the
        // `Copy` trait
        self.current_buffer = self.next_buffer;
        self.next_buffer = Buffer{ buffer: buf};
    }
}

// OK:使用 std::mem::replace() 替换
fn update_buffer(&mut self, buf: String) {
    self.current_buffer = std::mem::replace(&mut self.next_buffer, Buffer{buffer : buf});
}

// OK 的例子:使用 std::ptr::read/write 来临时转义借用对象的内容
//
// ptr::read(src: *const T) -> T
//
// 从 src 指针处浅复制(类似于 Copy)获取 T 对象,它共享 src 指向的内存区域数据。
unsafe {
    let result = ::std::ptr::read(dest); // result 共享 dest 指向的内存区域数据
    std::ptr::write(dest, src); // 向 dest 写入 src 对象
    result
}

// Ok 的例子
#[derive(Debug)]
struct Person {
    name: String,
    email: String,
}
fn main() {
    let mut p = Person{name: "zzz".to_string(), email: "fff".to_string()};

    let _name = p.name; // struct 允许部分 move
    println!("{} {}", _name, p.email); // 可以访问没有 move 的 struct 成员

    // println!("{:?}", p); //出错:不允许访问部分 move 的 struct 整体

    p.name = "Hao Chen".to_string();  // OK
    println!("{:?}", p); // OK
}

Reborrow:如果以前的 &mut 变量 r 不再使用,则可以使用 &*r 来获取新的 reborrow;

  • 如果借用后续不再使用,则允许再次借用,该特性称为 NLLNon-Lexical Lifetime
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

impl Point {
    fn move_to(&mut self, x: i32, y: i32) {
        self.x = x;
        self.y = y;
    }
}

fn main() {
    let mut p = Point { x: 0, y: 0 };
    let r = &mut p;
    // let p2 = *r // 错误:Point 未实现 Copy,不能通过解引用 *r 来转移值

    let rr: &Point = &*r; // 重新借用
    println!("{:?}", rr); // Reborrow ends here, NLL introduced

    // 由于借用 rr 不再使用,所以 r 可以重新被借用
    r.move_to(10, 10);
    println!("{:?}", r);
}

在解构 struct/tuple 对象时, 可以 by-move(默认)或 by-refer:

  • by-move :可能造成 struct/tuple 的 field 被 partial move ,这些 field 后续不能再访问,但是未被 partial move 的字段 还是可以访问的
  • by-refer :需要添加 ref/ref mut ,表示获得对象的借用,对应的变量是 &/&mut 类型。

Vec/Array 等 容器类型不支持元素的 partial move, 元素需要实现 Copy 或者被 std::mem::replace。

// https://practice.course.rs/ownership/ownership.html
#[derive(Debug)]
struct Person {
    name: String,
    age: Box<u8>,
}

let person = Person {
    name: String::from("Alice"),
    age: Box::new(20),
};

// `name` is moved out of person, but `age` is referenced
let Person { name, ref age } = person;
println!("The person's age is {}", age);
println!("The person's name is {}", name);

// Error! borrow of partially moved value: `person` partial move occurs
//println!("The person struct is {:?}", person);

// `person` cannot be used but `person.age` can be used as it is not moved
println!("The person's age from person struct is {}", person.age);


// Vec/array 中的元素不支持 partial-move
struct Person {
    name: Option<String>,
    birth: i32
}
let mut composers = Vec::new();
composers.push(
    Person {
        name: Some("Palestrina".to_string()),
        birth: 1525
    }
);
// Vec/array 没有实现 Copy 时,不支持 partial-move,所以编译出错
// let first_name = composers[0].name;
let first_name = &composers[0].name; // OK,借用而非 move

解引用操作符 * 用于返回引用类型对象的值。由于 Rust 编译器会自动解引用和生成引用,所以实际很少直接通过 * 操作符来显式解引用。

. 操作符 自动按需借用或解引用变量:

  1. 通过 . 操作符访问对象的成员时,自动解引用: ref.filed 等效于 (*ref).field ;
  2. 通过 . 操作符调用方法时,如果方法的第一个参数式 &self&mut self, 则 自动借用对象来生成引用=,然后传递给对应的方法:v.sort() 等效为 (&v).sort(),称为 =method call deref coercion
// 使用 . 访问 引用变量的成员时,自动解引用
struct Anime { name: &'static str, bechdel_pass: bool };
let aria = Anime { name: "Aria: The Animation", bechdel_pass: true };
let anime_ref = &aria;
assert_eq!(anime_ref.name, "Aria: The Animation");
// 等效于
assert_eq!((*anime_ref).name, "Aria: The Animation");

// 对象方法是 &self 或 &mut self 时,自动借用变量,生成它的引用
let mut v = vec![1973, 1968];
v.sort(); // 自动借用 v,生成 &v 来调用 sort() 方法
// 等效于
(&mut v).sort();

let x = 5;
let y = &x;
assert_eq!(5, *y); // 但直接使用 ref 变量时需要 显式解引用

其它自动解引用场景(都支持 多级自动解引用 ):

  1. 比较操作,因为默认比较的是引用的值,而非引用本身(指针);
  2. index 操作符;
  3. 算术运算操作;
  4. println!()/assert* 等宏函数自动解引用传入的引用参数;
struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;
assert_eq!(rrr.y, 729); // . 操作支持多级解引用

let x = 10;
let y = 10;
let rx = &x;
let ry = &y;
let rrx = &rx;
let rry = &ry;
assert!(rrx <= rry);  // 比较操作也自动多级解引用
assert!(rrx == rry);

fn factorial(n: usize) -> usize {
    (1..n+1).product()
}
let r = &factorial(6);
assert_eq!(r + &1009, 1729); // 算术运算自动解引用

let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1); // x 是 &i32 类型,算术运算自动解引用

Rust 引用操作可以是任意表达式,如字面量,Rust 会自动进行转换(coercion)Type coercions - The Rust Reference

r + &1009; // 算术操作自动解引用

let _: &i8 = &mut 42;

fn bar(_: &i8) { }

fn main() {
    bar(&mut 42);

    let x = 5;
    let y = &x;
    assert_eq!(5, *y);
}

对于 T,&T,&mut T,Box<T> 在进行 Display 时显示的都是 T 的值。

&T, &mut T, Box<T> 是 指针类型 ,可以使用 {:p} 修饰符来 显示它们的地址而非值

  • Rc::clone() 只是增加了引用计数,所以产生的对象与以前的对象是 相同的地址
  • 如果要比较引用地址本身,需要使用 std::ptr::eq() 函数,使用 {:p} 来打印指针地址;
let mut t = 123;
let tp = &mut t;
let tpp = &tp; //  从 &mut T 变量中可以再借用出共享引用 &T
println!("{:p} {:p}",  tp, tpp); // tp 和 tpp 是两个不同类型的变量,所以地址不一致

let rc = Rc::new(String::from("abc"));
let rc2 = rc.clone();
// rc: abc, rc2: abc, rc pointer:0x600001bdc2b0, rc2 pointer 0x600001bdc2b0
// 可见 rc 和 rc2 内存的地址都是一样的,说明 Rc clone 没有发生堆内存拷贝。
println!("rc: {}, rc2: {}, rc pointer:{:p}, rc2 pointer {:p}", rc, rc2, rc, rc2);

let five = 5;
let other_five = 5;
let five_ref = &five;
let same_five_ref = &five;
let other_five_ref = &other_five;
assert!(five_ref == same_five_ref); // 比较操作时,自动多级解引用,所以比较的是值。
assert!(five_ref == other_five_ref);

assert!(std::ptr::eq(five_ref, same_five_ref)); // std::ptr::eq 比较内存地址
assert!(!std::ptr::eq(five_ref, other_five_ref));
rust-lang - 这篇文章属于一个选集。
§ 6: 本文

相关文章

不安全:unsafe
··824 字
Rust
Rust
函数、方法和闭包:function/method/closure
··7032 字
Rust
Rust 函数、方法和闭包
包和模块:package/crate/module
··2066 字
Rust
Rust 项目的包和模块组织结构
变量:variable
··1275 字
Rust
Rust 变量介绍