写给前端看的Rust教程(16)生命周期

1,996 阅读5分钟

原文:24 days from node.js to Rust

前言

Rust的生命周期概念主要是避免空引用,对用户来说空引用会导致程序崩溃,对于恶意攻击者来说这是个漏洞。

注意:空引用 是大多数软件错误的根源。据Chromium团队的调查,70%的严重事故都是内存安全类的(详见),如果你对此感兴趣,可以读一下 这篇文章 ,该文章介绍了浏览器内存安全问题

内存安全问题对于没有使用过类似C语言进行内存管理的开发人员来说有点陌生,JavaScriptJava会利用垃圾回收机制帮你管理内存,避免空引用问题发生,垃圾回收器会追踪你对数据的引用,直到数据的引用数量是0

在63年前,垃圾回收机制还非常具有非常革命性,但它是有代价的,在极端情况下,垃圾回收会使程序失去几秒钟响应

Rust不采用垃圾回收机制来避免空引用,你可以获得类似C语言一般的高效同时具有JavaScript语言般的安全性

注意:网上有许多介绍Rust生命周期的文章,本教程不会面面俱到,只会告诉你一些容易引起困惑的问题,教程后面的相关阅读是阅读本文的先决条件,请仔细阅读

正文

生命周期 & 生命周期注解

你经常会在一些文章中遇到将“生命周期注解”简写为“生命周期”,但这只是会让人感到困惑

  • 生命周期:Rust借用检查器borrow checker)中涉及到的一个概念,每个变量都有一个创建和销毁的时间点,这段时间就是它的生命周期
  • 生命周期注解:一种Rust的语法,开发人员可以向引用添加一个命名标记以赋予其生命周期,使用场景是多个引用的时候才有意义

当你读到“你必须指定生命周期”或“给引用一个生命周期”的时候,这里的“生命周期”实际上就是“生命周期注解”

生命周期注解省略

每个引用都有一个生命周期,这和有没有注解无关,有时候不写注解并不意味着在逃避生命周期这回事

例如这个函数:

fn omits_annotations(list: &[String]) -> Option<&String> {
  list.get(0)
}

与下面这个行数是等效的:

fn has_annotations<'a>(list: &'a [String]) -> Option<&'a String> {
  list.get(1)
}

使用起来也是一致的:

fn main() {
  let authors = vec!["Samuel Clemens".to_owned(), "Jane Austen".to_owned()];
  let value = omits_annotations(&authors).unwrap();
  println!("The first author is '{}'", value);
  let value = has_annotations(&authors).unwrap();
  println!("The second author is '{}'", value);
}

输出结果:

The first author is 'Samuel Clemens'
The second author is 'Jane Austen'

之所至可以省略注解是因为这里的入参和返回值都只有一个,此时Rust会自动推断他们的生命周期是一致的

静态声明周期('static)

Rust社区里经常会涉及到'static,有很多解释'static的内容,不过这会让人觉的更复杂了,下面我们尽可能简洁的介绍

有两种'static的经典使用场景:

(一) 作为引用的显式生命期注释

fn main() {
  let mark_twain = "Samuel Clemens";
  print_author(mark_twain);
}
fn print_author(author: &'static str) {
  println!("{}", author);
}

(二)作为泛型参数的生命周期限制

fn print<T: Display + 'static>(message: &T) {
  println!("{}", message);
}

&'static的出现意味着这个引用在程序的剩余部分也是有效的,其所指向的数据不能move也不能被改变。字符串字面量都是&'static的,

&'static针对的不是保存引用的变量,我们以一个例子来做说明。get_memory_location()函数返回一个字符串字面量的指针和长度,get_str_at_location()函数以指针和长度作为参数,并读取内存中指定位置的内容

use std::{slice::from_raw_parts, str::from_utf8_unchecked};

fn get_memory_location() -> (usize, usize) {
    let string = "Hello World!";
    let pointer = string.as_ptr() as usize;
    let length = string.len();
    (pointer, length)
    // `string` is dropped here.
    // It's no longer accessible, but the data lives on.
}

fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
    // Notice the `unsafe {}` block. We can't do things like this without
    // acknowledging to Rust that we know this is dangerous.
    unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}

fn main() {
    let (pointer, length) = get_memory_location();
    let message = get_str_at_location(pointer, length);
    println!(
        "The {} bytes at 0x{:X} stored: {}",
        length, pointer, message
    );
    // If you want to see why dealing with raw pointers is dangerous,
    // uncomment this line.
    // let message = get_str_at_location(1000, 10);
}

输出结果是:

The 12 bytes at 0x562037200057 stored: Hello World!

另一方面,添加'static作为限制也就是在告诉Rust你希望这个类型可以一直存在,而不是数据一直存在

你需要明白&'static  不等于 T: 'static

下面的代码证实了第二块中的String是如何满足static_bound()函数的'static约束,但不保留第一个块中&'static的相同性质

use std::fmt::Display;

fn main() {
    let r1;
    let r2;
    {
        static STATIC_EXAMPLE: i32 = 42;
        r1 = &STATIC_EXAMPLE;
        let x = "&'static str";
        r2 = x;
    }
    println!("&'static i32: {}", r1);
    println!("&'static str: {}", r2);
    let r3;
    {
        let string = "String".to_owned();

        static_bound(&string); // This is *not* an error
        r3 = &string; // *This* is
    }
    println!("{}", r3);
}

fn static_bound<T: Display + 'static>(t: &T) {
    println!("{}", t);
}

结果:

error[E0597]: `string` does not live long enough
  --> crates/day-16/static/src/main.rs:21:10
   |
21 |     r3 = &string;
   |          ^^^^^^^ borrowed value does not live long enough
22 |   }
   |   - `string` dropped here while still borrowed
23 |   println!("{}", r3);
   |                  -- borrow later used here

For more information about this error, try `rustc --explain E0597`.

虽然两种用法是有关联,但背后的精神却不一样

如果你想通过添加&'static来解决问题,那么你需要重新思考看看这么做对不对,如果你需要添加'static解决问题,那通常是没问题的

相关阅读

总结

本文历经五次迭代,让人最头痛的地方在于泛型、生命周期、引用在第三方库及异步代码中最佳实践,这些我们还未涉及。

后面我们会介绍到如何用Rust提升JavaScript程序的性能,如何建立一个web服务器,如何实现文件读写操作以及序列化反序列化JSON