跳转到主内容基本类型、变量和常量 | 物联网民工

基本类型、变量和常量

C 程序员进入 Rust,最先要过的不是所有权,而是能不能顺手写出普通变量、常量和基本类型。

C 里常见代码大概是这样:

#include <stdint.h>
#include <stdbool.h>

#define MAX_PACKET_SIZE 1024

int retry = 3;
uint32_t flags = 0;
bool enabled = true;
char tag = 'A';

Rust 也能表达这些东西,但默认习惯不完全一样:

const MAX_PACKET_SIZE: usize = 1024;

let mut retry: i32 = 3;
let flags: u32 = 0;
let enabled: bool = true;
let tag: u8 = b'A';

结构体布局、数组、指针和字符串都建立在这些基础类型之上。变量、常量和宽度先分清,后面的内存和 API 问题才不会混在一起。

变量默认不能改

C 里局部变量默认可修改:

int retry = 3;
retry = retry - 1;

Rust 里 let 绑定默认不可变:

let retry = 3;
retry = retry - 1; // 编译错误

需要修改时要写 mut:

let mut retry = 3;
retry = retry - 1;

这不是语法洁癖。C 代码里一个变量是否会被后续修改,通常要靠人读完整个作用域判断。Rust 把这个意图写在绑定处:默认不改,要改就显式 mut。

这会影响库代码的可读性。看到:

let len = data.len();

就知道 len 后面不会被重新赋值。看到:

let mut offset = 0;

就知道它是一个会推进的游标或累加状态。

类型可以标,也可以推断

C 声明变量时必须写类型:

int count = 0;
uint32_t flags = 0;

Rust 可以显式写:

let count: i32 = 0;
let flags: u32 = 0;

也可以让编译器从上下文推断:

let count = 0;
let flags = 0_u32;

没有上下文时,整数默认通常会落到 i32。写库、协议字段、文件格式时,不要依赖读者猜默认类型,关键位置应该把类型写清楚。

例如协议版本字段是 1 字节:

let version: u8 = 1;

长度来自 slice:

let len: usize = data.len();

数据格式字段和内存长度不是同一种语义。前者应该用明确宽度,后者通常用 usize。

整数优先写清宽度

C 里有两套整数习惯。

一套是传统类型:

int count;
long total;
unsigned flags;

另一套是 stdint.h:

uint8_t version;
int32_t temperature;
uint64_t timestamp;

Rust 的整数类型直接带符号和宽度:

uint8_t   -> u8
uint16_t  -> u16
uint32_t  -> u32
uint64_t  -> u64

int8_t    -> i8
int16_t   -> i16
int32_t   -> i32
int64_t   -> i64

写二进制协议、文件格式、寄存器字段、跨语言结构体时,优先用固定宽度整数。

usize 和 isize 更像 C 里的 size_t 和 ssize_t 这类平台相关大小:

let index: usize = 0;
let len: usize = data.len();

它们适合内存里的长度、下标和对象大小,不适合直接作为网络协议或文件格式字段。

bool 不是整数

C 在很长时间里没有内建 bool,后来通过 <stdbool.h> 使用 _Bool:

#include <stdbool.h>

bool enabled = true;

if (enabled) {
    /* ... */
}

C 里很多代码还会把整数当条件:

if (flags) {
    /* non-zero means true */
}

Rust 的 bool 是独立类型,条件表达式必须是 bool:

let enabled: bool = true;

if enabled {
    // ...
}

整数不能直接当条件:

let flags: u32 = 1;

if flags != 0 {
    // ...
}

这会让一些 C 写法变啰嗦,但意图更明确。if ptr、if len、if flags 这种压缩写法,在 Rust 里要写成具体判断。

Rust 的 char 不是 C 的 char

C 的 char 通常是一字节整数类型:

char tag = 'A';

Rust 的 char 表示一个 Unicode scalar value,大小是 4 字节:

let ch: char = '中';

如果你要表达一个字节,用 u8:

let tag: u8 = b'A';

如果你要表达一段文本,用 &str 或 String。如果你要表达 C 字符串,用 CStr 或 CString。不要把 Rust 的 char 当成 C 的 char 直接替换。

第一层判断可以这样记:

C char          -> 常常是一个字节
Rust u8         -> 一个字节
Rust char       -> 一个 Unicode 标量值
Rust &str/String -> UTF-8 文本

const 不是 C 的 const

C 里常见两种“常量”写法。

一种是宏:

#define MAX_PACKET_SIZE 1024

另一种是 const 限定对象:

const int max_packet_size = 1024;

Rust 的 const 不等于 C 的 const。它更像“有类型的编译期常量值”:

const MAX_PACKET_SIZE: usize = 1024;

Rust const 有类型,不是文本替换,也不是一个有固定唯一地址的只读对象。它适合表达数组容量、默认上限、协议常量、状态值等。

如果需要一个有固定存储位置的全局值,用 static:

static APP_NAME: &str = "iot-tool";

第一层先记住区别:

const  -> 编译期常量值
static -> 有固定存储位置的全局值

对应关系不要写成 C const -> Rust const。更稳的是:

C #define 数值常量        -> Rust const
C enum 常量               -> Rust const / Rust enum
C const 局部只读变量       -> Rust let
C static const 全局对象    -> Rust const 或 static,看是否需要固定地址

大多数从 #define 迁移过来的数值常量,先考虑 Rust const。

shadowing 不是重新赋值

C 里同一个作用域不能重复定义同名变量:

int len = 10;
int len = len + 1; // 错误

Rust 允许 shadowing:

let len = "1024";
let len: usize = len.parse().unwrap();

第二个 len 不是修改第一个 len,而是创建了一个新的绑定,遮蔽了旧绑定。

这在处理输入时很常见:

let port = "1883";
let port: u16 = port.parse().unwrap();

变量名表达的是同一个业务概念,类型从文本变成了数字。不要把 shadowing 当成 C 的重新赋值,它更像“同名的新变量”。

最小迁移表

核心对应关系如下:

C int              -> Rust i32 或按需求选择 i16/i64
C unsigned int     -> Rust u32 或按需求选择 u16/u64
C uint8_t          -> Rust u8
C size_t           -> Rust usize
C bool/_Bool       -> Rust bool
C char as byte     -> Rust u8
C char as text     -> Rust char / &str,看语义
C #define value        -> Rust const
C const local object   -> Rust let
C global object        -> Rust static
C mutable local        -> Rust let mut
C default local        -> Rust let

这里最重要的不是把名字背下来,而是先区分三类东西:

  • 外部格式字段:用明确宽度类型
  • 内存长度和下标:用 usize
  • 文本、字节、字符:不要混成 char *

运算符和类型转换会继续沿着这条线处理 C 里的隐式转换、整数提升和 Rust 的显式 as。

继续阅读