跳转到主内容buffer、slice 和 Vec | 物联网民工

buffer、slice 和 Vec

C 里表示一段二进制数据,最常见的形态是指针加长度:

int parse_packet(const uint8_t *buf, size_t len);

这套写法很灵活,也很危险。buf 是否为空、len 是否可信、函数会不会修改数据、会不会保存这个指针、调用方什么时候释放内存,都不在类型里,只能靠约定、注释和经验。

Rust 没有把这个模式取消,而是拆成几种更明确的类型:

只读视图:&[u8]
可写视图:&mut [u8]
拥有缓冲区:Vec<u8>
固定数组:[u8; N]

这一篇只看二进制 buffer,不讲字符串。String 和 &str 后面单独讲。

C 的指针加长度表达了太多事

先看一个 C 函数:

#include <stdint.h>
#include <stddef.h>

uint16_t checksum(const uint8_t *buf, size_t len)
{
    uint32_t sum = 0;

    for (size_t i = 0; i < len; i++) {
        sum += buf[i];
    }

    return (uint16_t)(sum & 0xffff);
}

从签名看,读者大概能知道它不会修改 buffer,因为参数是 const uint8_t *。但还有很多信息没写出来:

  • buf == NULL 时是否合法
  • len 是否真的对应可访问内存
  • buf + len 是否越界
  • 函数会不会把指针保存到别处
  • 调用期间是否有人同时修改这段内存

C 的类型只表达了一部分事实。剩下的靠调用约定。

&[u8] 是“只读视图”

Rust 里,这个函数可以先写成:

fn checksum(buf: &[u8]) -> u16 {
    let mut sum: u32 = 0;

    for byte in buf {
        sum += *byte as u32;
    }

    (sum & 0xffff) as u16
}

&[u8] 可以先理解成:

指向一段 u8 的只读视图 = 指针 + 长度 + 只读访问权限

它和 C 的 const uint8_t *buf, size_t len 很像,但更紧:

  • 长度跟视图绑定在一起
  • 遍历时知道边界
  • 函数不能通过 &[u8] 修改数据
  • 函数默认只是借用,不拥有这段内存

调用方可以传数组、Vec<u8> 或更大 buffer 的一部分:

let data = [1u8, 2, 3, 4];
let sum = checksum(&data);

let packet = vec![0x10, 0x20, 0x30];
let sum = checksum(&packet);

这里的 &data 和 &packet 都会变成 slice 视图。函数只看见一段连续字节,不需要关心底层来自数组还是 Vec。

&mut [u8] 表示独占可写视图

如果函数要修改 buffer,C 里通常去掉 const:

void xor_in_place(uint8_t *buf, size_t len, uint8_t key)
{
    for (size_t i = 0; i < len; i++) {
        buf[i] ^= key;
    }
}

Rust 里对应的是:

fn xor_in_place(buf: &mut [u8], key: u8) {
    for byte in buf {
        *byte ^= key;
    }
}

&mut [u8] 不是普通“可写指针”。它表示一段 buffer 的独占可写借用。

第一层先记住这个工程含义:

&[u8]      只能读
&mut [u8]  可以写,并且要求这段时间内没有其他访问冲突

这就是 Rust 后面讲 borrow 时会展开的内容。现在不用急着讲生命周期标注,只要知道:Rust 把“这个函数会不会改 buffer”写进了参数类型。

Vec 表示拥有一段可增长缓冲区

C 里如果函数要创建一段数据并交给调用方,常见写法有很多种:

uint8_t *make_packet(size_t *out_len);

或者由调用方提供 buffer:

int make_packet(uint8_t *out, size_t capacity, size_t *out_len);

这些写法都要额外约定:谁分配,谁释放,容量够不够,失败时输出参数是否有效。

Rust 里如果函数要返回自己创建的数据,通常用 Vec<u8>:

fn make_packet(payload: &[u8]) -> Vec<u8> {
    let mut packet = Vec::new();

    packet.push(0x01);
    packet.push(payload.len() as u8);
    packet.extend_from_slice(payload);

    packet
}

Vec<u8> 可以先理解成:

拥有所有权的连续字节缓冲区 = 指针 + 长度 + 容量

它和 C 里一块动态分配的 buffer 有相似处,但释放责任进入了类型。返回 Vec<u8> 的函数把这段内存的所有权交给调用方;调用方不需要手写 free,值离开作用域时会释放。

len 和 capacity 不是一回事

C 开发者写可写 buffer 时,经常同时处理“已经写了多少”和“最多能写多少”:

int encode(uint8_t *out, size_t capacity, size_t *out_len);

Rust 的 Vec 里也有这两个概念:

let mut buf = Vec::with_capacity(1024);

println!("len = {}", buf.len());
println!("cap = {}", buf.capacity());

len 是当前已经有多少有效元素,capacity 是当前分配空间最多能容纳多少元素。

buf.push(1);
buf.push(2);

push 会增加 len。如果容量不够,Vec 会重新分配更大的内存并搬移数据。

这个行为对写库很重要。如果外部还保存着旧的裸指针,重新分配会让旧指针失效。普通 Rust 代码不会让你轻易保留这种危险关系;到 FFI 或 unsafe 边界时要额外小心。

固定长度用数组

如果长度在编译期固定,可以用数组:

let magic: [u8; 4] = [0x12, 0x34, 0x56, 0x78];

[u8; 4] 和 [u8; 8] 是不同类型,因为长度是类型的一部分。

这和 C 数组有相似处,但 Rust 更明确。固定长度字段、magic number、短 key、固定头部,都可以用数组表达。

需要把数组传给只读函数时,它可以变成 slice:

let sum = checksum(&magic);

所以常见关系是:

[u8; N]  固定长度数组,拥有数据
&[u8]    只读 slice,借用一段数据
&mut [u8] 可写 slice,独占借用一段数据
Vec<u8>  可增长 buffer,拥有数据

不要把 &[u8] 当成“弱一点的 Vec”

&[u8] 和 Vec<u8> 最大差异不是能不能增长,而是谁拥有数据。

如果函数只是读取数据,优先接收 &[u8]:

fn parse_packet(input: &[u8]) -> Result<(), &'static str> {
    if input.len() < 2 {
        return Err("packet too short");
    }

    let version = input[0];
    let length = input[1] as usize;

    if input.len() < 2 + length {
        return Err("payload too short");
    }

    Ok(())
}

这样调用方可以传数组、Vec、文件读出来的数据或更大 buffer 的一部分。函数不关心数据从哪里来,也不拿走所有权。

如果函数要创建并返回数据,用 Vec<u8>:

fn encode_packet(payload: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(2 + payload.len());
    out.push(1);
    out.push(payload.len() as u8);
    out.extend_from_slice(payload);
    out
}

这个 API 边界比“所有地方都用 Vec<u8>”更稳。

这一篇先记住什么

C 里的指针加长度,在 Rust 里不是一个类型全包,而是按权限和所有权拆开:

const uint8_t *buf, size_t len  -> &[u8]
uint8_t *buf, size_t len        -> &mut [u8]
malloc 出来的可变长 buffer     -> Vec<u8>
uint8_t buf[N]                  -> [u8; N]

这张表背后的核心变化是:Rust 把长度、读写权限和所有权分开表达。

&[u8] 适合读,&mut [u8] 适合原地改,Vec<u8> 适合拥有和返回数据,[u8; N] 适合固定长度。

下一篇再看字符串。那时会把 C 里的 char *、NUL 结尾、字节串、UTF-8 文本和 Rust 的 String / &str 分开。