跳转到主内容运算符、表达式和类型转换 | 物联网民工

运算符、表达式和类型转换

C 程序员看 Rust 运算符,最容易误判的不是 +、-、*、/ 长得像不像,而是 C 里很多自动发生的转换,在 Rust 里不会自动发生。

C 代码里常见这种写法:

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

uint8_t version = 1;
size_t len = 128;
uint32_t total = version + len;

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

Rust 会要求你把类型关系写清楚:

let version: u8 = 1;
let len: usize = 128;
let total: usize = version as usize + len;

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

从这段代码开始,最先要调整的是类型变化必须显式出现。控制流怎么组织,可以在这个基础上继续往后看。

算术运算要求类型一致

C 会做整数提升和 usual arithmetic conversions:

uint8_t a = 10;
uint32_t b = 20;
uint32_t c = a + b;

a 会参与提升,表达式可以通过。

Rust 不会自动把 u8 变成 u32:

let a: u8 = 10;
let b: u32 = 20;
let c = a + b; // 编译错误

要写清楚转换:

let c = a as u32 + b;

这让代码多了几个字符,但少了很多隐藏规则。尤其在协议、文件格式、长度计算里,隐式转换很容易把有符号、无符号、平台相关宽度混在一起。

usize 不要随手和字段宽度混算

C 里 size_t 和 uint32_t 混用很常见:

uint32_t packet_len = header.length;
size_t offset = 4;
size_t end = offset + packet_len;

Rust 会拦住这种混算:

let packet_len: u32 = 128;
let offset: usize = 4;
let end = offset + packet_len; // 编译错误

你要先决定语义:这是内存下标,还是外部格式字段。

如果要拿来切 slice,转换到 usize:

let end = offset + packet_len as usize;

如果要写回协议字段,保持固定宽度:

let packet_len: u32 = payload.len() as u32;

后一种写法还要考虑长度是否可能超过 u32::MAX。更稳的库代码通常用 try_from:

let packet_len = u32::try_from(payload.len()).map_err(|_| EncodeError::TooLarge)?;

as 是转换,不是边界检查。会不会截断,要由你自己判断。

bool 不能由整数隐式得到

C 里条件接受整数:

if (flags & 0x01) {
    enable();
}

Rust 里 if 条件必须是 bool:

let flags: u32 = 0x01;

if flags & 0x01 {
    // 编译错误
}

要写成比较:

if flags & 0x01 != 0 {
    enable();
}

位运算本身还是 &、|、^、!、<<、>>,但位运算结果是整数,不是条件。

这个差异会影响很多 C 风格判断:

if (len)      -> if len != 0
if (ptr)      -> if !ptr.is_null()       // 裸指针场景
if (flags&m)  -> if flags & m != 0

Rust 要你说清楚判断标准。

逻辑运算只处理 bool

C 里 && 和 || 可以处理整数条件:

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

Rust 的 && 和 || 两边必须是 bool:

let len: usize = 10;
let enabled = true;

if len != 0 && enabled {
    // ...
}

短路规则仍然存在。右侧表达式只在需要时求值:

if !data.is_empty() && data[0] == 0x7e {
    // ...
}

这里 data[0] 不会在空 slice 时执行。

赋值不是一个可拿来链式使用的值

C 里赋值表达式有值:

int a;
int b;

a = b = 0;

if ((n = read(fd, buf, sizeof buf)) > 0) {
    /* ... */
}

Rust 里赋值表达式的值是 (),不适合写成 C 的链式赋值或条件赋值:

let mut a = 1;
let mut b = 2;

a = b = 0; // 编译错误

Rust 通常拆开写:

b = 0;
a = b;

读取结果也会单独绑定:

let n = reader.read(&mut buf)?;
if n > 0 {
    // ...
}

这类写法少了 C 的紧凑,但错误路径和中间值更清楚。

代码块可以产生值

C 里的 { ... } 主要是语句块和作用域。Rust 的代码块也是表达式,可以产生值:

let size = {
    let header = 4;
    let payload = 128;
    header + payload
};

块里最后一行没有分号,就是这个块的值。

这会影响很多 Rust 写法。比如 if 也可以产生值:

let limit = if debug { 64 } else { 1024 };

但两个分支类型必须一致:

let limit = if debug { 64 } else { "large" }; // 编译错误

C 程序员可以把它理解成:Rust 里不少控制结构既能控制流程,也能返回一个值,但类型必须在编译期对齐。

sizeof 对应 size_of

C 里用 sizeof:

size_t n = sizeof(uint32_t);

Rust 里用标准库函数:

use std::mem::size_of;

let n = size_of::<u32>();

size_of::<T>() 返回 usize。这适合检查内存里的类型大小,不适合直接决定外部协议布局。外部格式仍然应该自己规定字段宽度和字节序。

as 要谨慎使用

Rust 的 as 很方便:

let a: u32 = 300;
let b: u8 = a as u8;

但这可能截断。300_u32 as u8 不会变成 300,它会按目标类型规则转换。

边界可能出错时,优先用 try_from:

let b = u8::try_from(a).map_err(|_| Error::OutOfRange)?;

可以把规则简单分成两类:

明确知道不会越界的内部计算 -> as 可以接受
外部输入、文件字段、协议长度 -> 优先 try_from

Rust 不替你做隐式转换,但显式转换以后,责任就回到代码上。

最小迁移表

C a + b 自动提升       -> Rust 先统一类型
C size_t + uint32_t    -> Rust 明确转成 usize 或固定宽度
C if (x)               -> Rust if x != 0 / is_null / is_empty
C flags & mask         -> Rust flags & mask != 0 才是条件
C a = b = 0            -> Rust 分开赋值
C sizeof(T)            -> Rust size_of::<T>()
C 隐式数值转换          -> Rust as / try_from
C 语句块                -> Rust 块也可以是表达式

最重要的习惯变化是:C 把很多转换藏在表达式求值里,Rust 要你把类型变化写出来。写库和工具时,这反而能让长度、字段宽度、下标和错误路径更容易审查。

继续阅读