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 分开。