Avoiding unchecked indexing
避免不受检查的下标访问
What you’ll learn: Why
vec[i]is dangerous in Rust because it panics on out-of-bounds, and what the safer alternatives look like:.get()、iterators、and_then()and theentry()-style mindset. The goal is to replace C++’s silent undefined behavior with explicit control flow.
本章将学到什么: 为什么vec[i]在 Rust 里仍然危险,因为越界时会 panic;以及更安全的替代方式有哪些:.get()、迭代器、and_then(),还有entry()这类显式处理思路。核心目标是把 C++ 里那种悄悄掉进未定义行为的写法,替换成可见、可控的分支流程。
- In C++,
vec[i]may become undefined behavior andmap[key]may silently insert a missing key. Rust’s[]does not go that far, but it still panics if the index is invalid.
C++ 里,vec[i]越界会直接掉进未定义行为,而map[key]还会在键不存在时偷偷插入默认值。Rust 的[]没这么离谱,但索引无效时照样会 panic。 - Rule of thumb: use
.get()instead of[]unless the code can clearly prove the index is valid.
经验法则: 除非代码本身已经清楚证明下标一定合法,否则优先用.get(),别硬写[]。
C++ → Rust comparison
C++ 与 Rust 的对照
// C++ — silent UB or insertion
std::vector<int> v = {1, 2, 3};
int x = v[10]; // UB! No bounds check with operator[]
std::map<std::string, int> m;
int y = m["missing"]; // Silently inserts key with value 0!
#![allow(unused)]
fn main() {
// Rust — safe alternatives
let v = vec![1, 2, 3];
// Bad: panics if index out of bounds
// let x = v[10];
// Good: returns Option<&i32>
let x = v.get(10); // None — no panic
let x = v.get(1).copied().unwrap_or(0); // 2, or 0 if missing
}
Real example: safe byte parsing from production Rust code
真实例子:生产代码里的安全字节解析
#![allow(unused)]
fn main() {
// Example: diagnostics.rs
// Parsing a binary SEL record — buffer might be shorter than expected
let sensor_num = bytes.get(7).copied().unwrap_or(0);
let ppin = cpu_ppin.get(i).map(|s| s.as_str()).unwrap_or("");
}
Real example: chained safe lookups with .and_then()
真实例子:用 .and_then() 串联安全查找
#![allow(unused)]
fn main() {
// Example: profile.rs — double lookup: HashMap → Vec
pub fn get_processor(&self, location: &str) -> Option<&Processor> {
self.processor_by_location
.get(location) // HashMap → Option<&usize>
.and_then(|&idx| self.processors.get(idx)) // Vec → Option<&Processor>
}
// Both lookups return Option — no panics, no UB
}
Real example: safe JSON navigation
真实例子:安全地层层取 JSON 字段
#![allow(unused)]
fn main() {
// Example: framework.rs — every JSON key returns Option
let manufacturer = product_fru
.get("Manufacturer") // Option<&Value>
.and_then(|v| v.as_str()) // Option<&str>
.unwrap_or(UNKNOWN_VALUE) // &str (safe fallback)
.to_string();
}
Compared with the familiar C++ style json["SystemInfo"]["ProductFru"]["Manufacturer"], this version makes every possible failure visible in the type. Missing data stops the chain cleanly instead of exploding later in an unexpected place.
和 C++ 里常见的 json["SystemInfo"]["ProductFru"]["Manufacturer"] 相比,这种写法把每一步可能失败的地方都放进了类型里。字段缺失时,链条会安静地中断,而不是在某个更奇怪的地方爆炸。
When [] is acceptable
什么时候 [] 仍然可以接受
- After a bounds check:
if i < v.len() { v[i] }
已经先做过边界检查时:比如if i < v.len() { v[i] }。 - In tests: when panicking is the desired behavior
测试代码里:如果故意要验证 panic 行为,也可以直接用。 - With constants and invariants:
let first = v[0];right afterassert!(!v.is_empty());
有明确不变量时:比如刚写完assert!(!v.is_empty()),随后访问v[0]。
Safe value extraction with unwrap_or
用 unwrap_or 安全提取值
unwrap()panics onNoneorErr. In production code, safer alternatives are usually better.unwrap()在遇到None或Err时会 panic。生产代码里大多数时候都应该优先考虑更稳妥的替代方式。
The unwrap family
unwrap 家族速查
| Method | Behavior on None/Err | Use When 适用场景 |
|---|---|---|
.unwrap() | Panics 直接 panic | Tests or truly infallible paths 测试,或者逻辑上绝不可能失败的地方 |
.expect("msg") | Panics with message 带消息 panic | Panic is acceptable and needs explanation 允许 panic,但想把原因写清楚 |
.unwrap_or(default) | Returns default返回默认值 | Cheap fallback available 有便宜的默认值可用 |
| `.unwrap_or_else( | expr)` | |
.unwrap_or_default() | Returns Default::default()返回默认类型值 | Type implements Default类型实现了 Default |
Real example: parsing with safe defaults
真实例子:带安全默认值的解析
#![allow(unused)]
fn main() {
// Example: peripherals.rs
// Regex capture groups might not match — provide safe fallbacks
let bus_hex = caps.get(1).map(|m| m.as_str()).unwrap_or("00");
let fw_status = caps.get(5).map(|m| m.as_str()).unwrap_or("0x0");
let bus = u8::from_str_radix(bus_hex, 16).unwrap_or(0);
}
Real example: unwrap_or_else with a fallback struct
真实例子:unwrap_or_else 配合后备结构体
#![allow(unused)]
fn main() {
// Example: framework.rs
// Full function wraps logic in an Option-returning closure;
// if anything fails, return a default struct:
(|| -> Option<BaseboardFru> {
let content = std::fs::read_to_string(path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
// ... extract fields with .get()? chains
Some(baseboard_fru)
})()
.unwrap_or_else(|| BaseboardFru {
manufacturer: String::new(),
model: String::new(),
product_part_number: String::new(),
serial_number: String::new(),
asset_tag: String::new(),
})
}
Real example: unwrap_or_default on config deserialization
真实例子:配置反序列化失败时用 unwrap_or_default
#![allow(unused)]
fn main() {
// Example: framework.rs
// If JSON config parsing fails, fall back to Default — no crash
Ok(json) => serde_json::from_str(&json).unwrap_or_default(),
}
The C++ equivalent usually turns into a try/catch around JSON parsing plus a manually constructed fallback object. Rust lets that behavior remain visible, local, and predictable.
对应到 C++,通常就会变成一层 try/catch 再手动构造一个兜底对象。Rust 的版本则把这个行为控制得更局部、更显式,也更好预期。
Functional transforms: map、map_err、find_map
函数式变换:map、map_err、find_map
- These methods let
OptionandResultflow through transformations without being manually unpacked, which often replaces nestedif/elsechains with clearer pipelines.
这些方法能让Option和Result在不手动拆开的前提下持续变换,很多原本会写成层层if/else的东西,都能改造成更直的流水线。
Quick reference
速查表
| Method | On | Does 作用 | C++ Equivalent C++ 里的近似写法 |
|---|---|---|---|
| `.map( | v | …)` | Option / Result |
| `.map_err( | e | …)` | Result |
| `.and_then( | v | …)` | Option / Result |
| `.find_map( | v | …)` | Iterator |
| `.filter( | v | …)` | Option / Iterator |
.ok()? | Result | Convert Result to Option and propagate None把 Result 转成 Option 并在失败时早退 | Manual “if error then return nullopt” |
Real example: .and_then() chain for JSON field extraction
真实例子:用 .and_then() 链式提取 JSON 字段
#![allow(unused)]
fn main() {
// Example: framework.rs — finding serial number with fallbacks
let sys_info = json.get("SystemInfo")?;
// Try BaseboardFru.BoardSerialNumber first
if let Some(serial) = sys_info
.get("BaseboardFru")
.and_then(|b| b.get("BoardSerialNumber"))
.and_then(|v| v.as_str())
.filter(valid_serial) // Only accept non-empty, valid serials
{
return Some(serial.to_string());
}
// Fallback to BoardFru.SerialNumber
sys_info
.get("BoardFru")
.and_then(|b| b.get("SerialNumber"))
.and_then(|v| v.as_str())
.filter(valid_serial)
.map(|s| s.to_string()) // Convert &str → String only if Some
}
Real example: find_map — search plus transform in one pass
真实例子:find_map 把查找和变换合并成一趟
#![allow(unused)]
fn main() {
// Example: context.rs — find SDR record matching sensor + owner
pub fn find_for_event(&self, sensor_number: u8, owner_id: u8) -> Option<&SdrRecord> {
self.by_sensor.get(&sensor_number).and_then(|indices| {
indices.iter().find_map(|&i| {
let record = &self.records[i];
if record.sensor_owner_id() == Some(owner_id) {
Some(record)
} else {
None
}
})
})
}
}
find_map 很适合替换那种“for 循环里先判断,再 break,再把结果包一层”的写法。把“找到谁”和“找到后要怎么变”放进同一步里,代码会短很多。find_map is ideal for the old loop shape where you test each element, stop at the first match, and then transform it. Rust fuses that into one clear operation.
Real example: map_err for error context
真实例子:用 map_err 给错误补上下文
#![allow(unused)]
fn main() {
// Example: main.rs — add context to errors before propagating
let json_str = serde_json::to_string_pretty(&config)
.map_err(|e| format!("Failed to serialize config: {}", e))?;
}
JSON handling: nlohmann::json → serde
JSON 处理:从 nlohmann::json 到 serde
- C++ teams often use
nlohmann::jsonfor runtime field access. Rust usually usesserdeplusserde_json, which moves more schema knowledge into the type system itself.
C++ 团队处理 JSON 时,很常见的是nlohmann::json这种运行时取字段模式。Rust 更常见的是serde加serde_json,把更多“这个 JSON 应该长什么样”的知识前移进类型系统。
C++ (nlohmann) vs Rust (serde) comparison
C++ 的 nlohmann 与 Rust 的 serde 对照
// C++ with nlohmann::json — runtime field access
#include <nlohmann/json.hpp>
using json = nlohmann::json;
struct Fan {
std::string logical_id;
std::vector<std::string> sensor_ids;
};
Fan parse_fan(const json& j) {
Fan f;
f.logical_id = j.at("LogicalID").get<std::string>(); // throws if missing
if (j.contains("SDRSensorIdHexes")) { // manual default handling
f.sensor_ids = j["SDRSensorIdHexes"].get<std::vector<std::string>>();
}
return f;
}
#![allow(unused)]
fn main() {
// Rust with serde — compile-time schema, automatic field mapping
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fan {
pub logical_id: String,
#[serde(rename = "SDRSensorIdHexes", default)] // JSON key → Rust field
pub sensor_ids: Vec<String>, // Missing → empty Vec
#[serde(default)]
pub sensor_names: Vec<String>, // Missing → empty Vec
}
// One line replaces the entire parse function:
let fan: Fan = serde_json::from_str(json_str)?;
}
Key serde attributes
常用 serde 属性
| Attribute | Purpose 作用 | C++ Equivalent C++ 里的近似写法 |
|---|---|---|
#[serde(default)] | Fill missing fields with Default::default()字段缺失时用默认值补上 | if (j.contains(key)) { ... } else { default; } |
#[serde(rename = "Key")] | Map JSON key names to Rust field names 把 JSON 键名映射到 Rust 字段名 | Manual j.at("Key") access |
#[serde(flatten)] | Absorb extra keys into a map 把额外字段摊进映射里 | Manual for (auto& [k, v] : j.items()) |
#[serde(skip)] | Skip this field during ser/de 序列化和反序列化时忽略该字段 | Manual omission |
#[serde(tag = "type")] | Tagged enum dispatch 按类型字段分发枚举变体 | if (j["type"] == "...") chain |
Real example: full config struct
真实例子:完整配置结构体
#![allow(unused)]
fn main() {
// Example: diag.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagConfig {
pub sku: SkuConfig,
#[serde(default)]
pub level: DiagLevel, // Missing → DiagLevel::default()
#[serde(default)]
pub modules: ModuleConfig, // Missing → ModuleConfig::default()
#[serde(default)]
pub output_dir: String, // Missing → ""
#[serde(default, flatten)]
pub options: HashMap<String, serde_json::Value>, // Absorbs unknown keys
}
// Loading is 3 lines (vs ~20+ in C++ with nlohmann):
let content = std::fs::read_to_string(path)?;
let config: DiagConfig = serde_json::from_str(&content)?;
Ok(config)
}
Enum deserialization with #[serde(tag = "type")]
带 #[serde(tag = "type")] 的枚举反序列化
#![allow(unused)]
fn main() {
// Example: components.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")] // JSON: {"type": "Gpu", "product": ...}
pub enum PcieDeviceKind {
Gpu { product: GpuProduct, manufacturer: GpuManufacturer },
Nic { product: NicProduct, manufacturer: NicManufacturer },
NvmeDrive { drive_type: StorageDriveType, capacity_gb: u32 },
// ... 9 more variants
}
// serde automatically dispatches on the "type" field — no manual if/else chain
}
Exercise: JSON deserialization with serde
练习:用 serde 做 JSON 反序列化
- Define a
ServerConfigstruct that can be deserialized from the JSON below
定义一个ServerConfig结构体,让它能从下面这段 JSON 反序列化出来。
{
"hostname": "diag-node-01",
"port": 8080,
"debug": true,
"modules": ["accel_diag", "nic_diag", "cpu_diag"]
}
- Use
#[derive(Deserialize)]andserde_json::from_str()
使用#[derive(Deserialize)]和serde_json::from_str()。 - Add
#[serde(default)]todebugso it becomesfalsewhen missing
给debug加上#[serde(default)],这样缺失时默认就是false。 - Bonus: add
DiagLevel { Quick, Full, Extended }with a default ofQuick
加分项:再补一个DiagLevel { Quick, Full, Extended }字段,默认值设成Quick。
Starter code
起始代码
use serde::Deserialize;
// TODO: Define DiagLevel enum with Default impl
// TODO: Define ServerConfig struct with serde attributes
fn main() {
let json_input = r#"{
"hostname": "diag-node-01",
"port": 8080,
"debug": true,
"modules": ["accel_diag", "nic_diag", "cpu_diag"]
}"#;
// TODO: Deserialize and print the config
// TODO: Try parsing JSON with "debug" field missing — verify it defaults to false
}
Solution 参考答案
use serde::Deserialize;
#[derive(Debug, Deserialize, Default)]
enum DiagLevel {
#[default]
Quick,
Full,
Extended,
}
#[derive(Debug, Deserialize)]
struct ServerConfig {
hostname: String,
port: u16,
#[serde(default)] // defaults to false if missing
debug: bool,
modules: Vec<String>,
#[serde(default)] // defaults to DiagLevel::Quick if missing
level: DiagLevel,
}
fn main() {
let json_input = r#"{
"hostname": "diag-node-01",
"port": 8080,
"debug": true,
"modules": ["accel_diag", "nic_diag", "cpu_diag"]
}"#;
let config: ServerConfig = serde_json::from_str(json_input)
.expect("Failed to parse JSON");
println!("{config:#?}");
// Test with missing optional fields
let minimal = r#"{
"hostname": "node-02",
"port": 9090,
"modules": []
}"#;
let config2: ServerConfig = serde_json::from_str(minimal)
.expect("Failed to parse minimal JSON");
println!("debug (default): {}", config2.debug); // false
println!("level (default): {:?}", config2.level); // Quick
}
// Output:
// ServerConfig {
// hostname: "diag-node-01",
// port: 8080,
// debug: true,
// modules: ["accel_diag", "nic_diag", "cpu_diag"],
// level: Quick,
// }
// debug (default): false
// level (default): Quick