RustInPractice(16):测试设计,写出可信代码
cargo test 很容易运行,但真正的难点是选择输入和断言行为。测试不是把 demo 输出再打印一遍,而是为边界、失败路径和不变量建立自动化证据。 RustInPractice 第 16 篇。对应 demo:testing_demo,并为后续算法和服务文章建立测试标准。 一、单元测试验证一个行为1234#[test]fn divide_by_zero_is_an_error() { assert!(Calculator::divide(1.0, 0.0).is_err());} 测试名说清行为而非实现。#[cfg(test)] 模块只在测试时编译,适合测试私有实现细节;公开 API 的行为还应有 integration test 覆盖。 二、优先覆盖边界每个集合和算法都至少应考虑:空输入、单元素、重复值、最大最小值、无效格式和错误分支。例如排序必须覆盖空 slice;动态规划应拒绝负容量和长度不一致输入;字符串算法必须声明它返回字节偏移还是字符位置。 123456#[test]fn sort_handles_empty_input() ...
RustInPractice(15):集合选型,Vec、VecDeque 与 HashMap
先选对标准集合,再考虑自定义数据结构。Rust 标准库的 Vec、VecDeque、HashMap、BTreeMap 已覆盖绝大多数工程需求;手写链表和哈希表更适合作为理解所有权与复杂度的练习。 RustInPractice 第三季第 15 篇。对应 demo:stack_queue_list、hash_table。下一篇将这些容器放进可信的测试体系。 一、按访问模式选容器 需求 首选 原因 末尾追加、按下标访问 Vec<T> 连续内存,缓存友好 两端进出队 VecDeque<T> push_back / pop_front 高效 键值快速查找 HashMap<K, V> 平均常数时间访问 需要稳定有序遍历 BTreeMap<K, V> 按 key 排序 去重 HashSet<T> / BTreeSet<T> 只保存 key demo 的栈直接用 Vec:push 与 pop 都在末尾进行。队列则使用 VecDeque,避免从 Vec 头部删除时搬移所...
YOLO 目标检测实战(01):Docker GPU 环境搭建
目标:在隔离的 Docker 镜像里跑通 YOLO 所需的 CUDA / PyTorch / Ultralytics / TensorRT,并准备好权重与样例数据。这一步做扎实了,后面训练、导出、部署才不会被环境问题反复拖住。 这是「YOLO 目标检测实战」的第 01 篇。系列共 8 篇,完整目录见 00 · 系列导读。文中路径统一用容器内 /workspace 或项目相对路径。 一、为什么用 Docker 宿主机只保留驱动与 NVIDIA Container Toolkit,训练依赖全部锁在镜像里。 新架构(如 Blackwell)需要较新的 CUDA wheel(本实验用 CUDA 12.8 + cu128),镜像便于复现。 同一镜像既做训练,也起 FastAPI,避免「本机能跑、服务环境缺包」这种最耗时的问题。 二、仓库结构(相关部分)12345678├── docker/ # Dockerfile + compose + entrypoint├── scripts/ # 分步脚本├── api/...
多模态微调实战(02):用 Docker 固化 ms-swift 训练环境
微调最怕两件事:依赖打架 和 驱动 / CUDA 不匹配。这一篇把训练环境固定在镜像里,宿主机只负责三件事:显卡驱动、Docker、项目目录。目标是让训练环境一次搭好、随处复现。 这是「多模态微调实战」的第 02 篇。上一篇讲了路线图,本篇解决「环境从哪来、怎么复现」。 一、硬件与软件前提 项目 建议 GPU 单卡,显存约 16GB 或以上 驱动 能跑你选用的 CUDA 容器即可(较新的消费卡通常需要较新的 PyTorch) 系统 Linux(本文以 Ubuntu 为例) Docker 已安装,并配置好 NVIDIA Container Toolkit 磁盘 预留 50GB+(模型 + 全量数据 + checkpoint) 自检: 12nvidia-smidocker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu22.04 nvidia-smi 第二行能打印 GPU 信息,说明容器 GPU 通路正常。 二、项目目录约定把下面当作「项目根」: 1234567~/projects...
RustInPractice(14):泛型、Trait 与静态/动态分发
Rust 不以 class 继承为核心。它把数据放进 struct 或 enum,把可替换行为写成 trait,再根据场景选择泛型的静态分发或 trait object 的动态分发。结果是组合关系清楚,抽象成本也更可见。 这是 RustInPractice 第二季收官篇。对应 demo:oop_demo、pimpl_demo、design_patterns。下一季进入集合、测试与并发工程实践。 一、泛型消除重复123fn swap<T, U>(pair: (T, U)) -> (U, T) { (pair.1, pair.0)} T、U 是类型参数。编译器会为实际使用的具体类型生成对应实现,这通常称为单态化。泛型函数不必事先知道具体类型,只需声明它需要的能力。 二、trait 定义能力而非继承层级1234567891011trait Compress { fn compress(&self, data: &str) -> String;}struct Plain;impl Com...
RustInPractice(13):RefCell 与内部可变性
默认情况下,只有拿到 &mut T 才能修改 T。RefCell<T> 是一个刻意的例外:它允许通过 &self 修改内部值,但把借用规则从编译期挪到运行时检查。它很有用,也因此需要更克制地使用。 这是 RustInPractice 的第 13 篇。对应 demo:refcell_demo、design_patterns。下一篇用泛型与 trait 组织可组合行为。 一、内部可变性是什么?demo 的计数器方法接收 &self,却能增加内部数值: 1234567891011use std::cell::RefCell;struct Counter { value: RefCell<i32>,}impl Counter { fn increment(&self) { *self.value.borrow_mut() += 1; }} borrow() 返回只读 guard,borrow_mut() 返回可写 guard。gua...
RustInPractice(12):Rc、Arc 与 Weak
默认的唯一所有权最简单,但有些图结构、缓存和只读共享数据确实需要多个拥有者。Rust 不会隐式开启垃圾回收,而是要求你选择引用计数类型:单线程用 Rc<T>,跨线程用 Arc<T>,反向或可选关联用 Weak<T>。 这是 RustInPractice 的第 12 篇。对应 demo:rc_demo、weak_demo。下一篇:RefCell<T> 与内部可变性。 一、Rc<T>:单线程共享所有权123456use std::rc::Rc;let config = Rc::new(String::from("shared"));let first = Rc::clone(&config);let second = Rc::clone(&config);println!("{} {}", first, second); Rc::clone 不会深拷贝 String,只增加强引用计数。最后一个强引用离开作用域时,值被释放...
RustInPractice(11):Box、Drop 与递归数据结构
Box<T> 常被误解为“Rust 的手动堆内存”。实际上它仍然是普通拥有者:它拥有 T,离开作用域时自动释放 T。Box 的价值在于把值放到堆上,或让递归类型拥有一个编译期确定的大小。 这是 RustInPractice 的第 11 篇。对应 demo:box_demo、tree、stack_queue_list。下一篇讲共享所有权 Rc、Arc 与 Weak。 一、Box<T> 仍是唯一所有权12let number = Box::new(42);println!("{number}"); Box<T> 自身通常在栈上,内部指针指向堆上的 T。它不可隐式复制,移动 Box 就是移动唯一所有权。绝大多数场景不需要显式调用释放函数:作用域结束时自动执行 Drop。 二、Drop 统一管理各种资源demo 的 Resource 实现 Drop: 12345impl Drop for Resource { fn drop(&mut self) { pr...
RustInPractice(10):生命周期与引用关系
生命周期不是对象在运行时倒计时,也不是必须到处手写的神秘标记。它描述的是:一个引用至少要和它所指向的数据一样久。 大多数函数依靠省略规则自动推断;只有返回引用或在 struct 中保存引用时,才需要把关系写出来。 这是 RustInPractice 的第 10 篇。下一篇进入 Box<T> 和递归数据结构。 一、悬垂引用为何被禁止?1234// fn broken() -> &str {// let text = String::from("temporary");// &text// } text 在函数结束时被销毁,返回的引用会指向无效内存,所以编译器拒绝它。正确方案通常是返回拥有的数据: 123fn make_text() -> String { String::from("owned result")} 二、标注的是关系,不是具体时长从两个切片返回较长者: 123fn longer<'a>(left...
RustInPractice(09):借用与可变借用
按值传参会移动所有权,但多数函数只需要读取或修改调用者已有的数据。Rust 用引用表达这种临时访问,并用一条核心规则消除数据竞争:任意时刻,要么有多个只读引用,要么恰好有一个可变引用。 这是 RustInPractice 的第 09 篇。下一篇讲生命周期,解释编译器如何判断引用不会比它指向的数据活得更久。 一、不可变借用 &T1234567fn length(text: &str) -> usize { text.len()}let name = String::from("Rust");println!("{}", length(&name));println!("{name}"); length 得到的是只读视图,而不是 String 的所有权。函数结束后借用失效,原值仍由 name 拥有。对于文本参数,&str 比 &String 更通用:既能接收 String,也能接收字符串字面量。 二、可变...















