国内某私募中频组的应届工程师周一打开她的小组用 Rust 写了半年的报单 / 定价框架, 第一个点进去的是 50 行的 510300.SH 沪深300 ETF 期权定价器, 里面用 Black-Scholes 闭式公式给一张近月 call 估价。每个绑定都打上她在 C++ 课里见过的整型或浮点类型, 每个函数都用「最后一个表达式不带分号」的方式隐式返回, 没有一个 return 关键字; 唯一的 if 被绑到变量上当表达式用。逐个看, 这些与 C++ 的差别都不大; 加在一起就是把 Rust 与 C++ 区分开的语言味道。本课讲 fn main() 与右花括号之间的那一截——基本类型、控制流、模式匹配、函数定义——刚好是国内 ETF 期权 / 50ETF 期权 / 300ETF 业务对应桌位上 Rust 工程师每天会用到的层级。
基本类型分两簇
整型有: 有符号家族 i8、i16、i32、i64、i128, 无符号家族 u8、u16、u32、u64、u128, 以及指针大小的 isize / usize (32 位目标上是 32 位, 64 位目标上是 64 位——本模块所有生产宿主都是 64 位)。两条反射性规则: usize 是数组、切片、Vec<T> 索引的正确类型, 标准库就这么用, 用别的类型索引就要显式 cast; 价格 (以最小报价单位计)、ID、计数、微秒时间戳的默认类型用 i64 / u64——i32 在微秒级时间戳上会溢出, 一律不要用它存任何与墙钟时间相关的量。
浮点是 f32 与 f64。默认全部 f64, 除非你已经度量过有 SIMD 带宽 / 内存占用上的理由换 f32。其他基本类型: bool 占 1 字节, 字面量 true 与 false。char 是 4 字节的 Unicode 标量——必须强调一次: Rust 的 char 不是 C/C++ 的 1 字节 char, 这一处 C++ 类比会主动误导你。Rust 里 1 字节的整数是 u8。Unit 类型 () 即空元组, 是函数无返回值时的返回类型。
元组 (T1, T2, T3) 用 .0 / .1 / .2 访问字段, 用 let (a, b, c) = my_tuple; 解构。数组 [T; N] 长度编码在类型里、放在栈上, 几乎不是变长数据的合理选择——堆上可增长的版本是 Vec<T>, lesson 3 详细讲。切片 &[T] 与 &mut [T] 是「胖指针」(指针加长度), 替代 C++ 的「指针 + 长度」函数参数惯用法; lesson 3 拆开讲。
绑定: 不可变、可变与遮蔽
下面是 Fenced Rust 块, 展示「不可变 / 可变 / 遮蔽 (shadowing)」三种用法:
let x = 5_i64;
let mut y = 5_i64;
y += 1;
let x = x.to_string(); // shadowing — same name, new type
let x = 5_i64; 绑定一个不可变变量, 不能重新赋值。let mut y = 5_i64; 显式 opt-in 可变性。5_i64 这种后缀强制字面量类型, 避免编译器推为默认的 i32; 分位下划线 1_000_000_u64 完全被词法分析忽略, 只为可读。遮蔽——let x = x.to_string();——把同一个名字在同一作用域内重新绑到一个新值 (可以是新类型), 旧的 x 在新 let 之后不可访问。变量的「角色」发生变化时优先用遮蔽 (典型场景是把字符串解析成整数); 真正在原地改值时才用 mut。
控制流即表达式
来自 Python / C++ 的概念跃升: Rust 中几乎一切皆表达式。{ ... } 块的值就是块内最后一个不带分号的表达式; if 自己就是一个表达式, 可以放在 let 的右侧; loop { ... break value; ... } 的值就是 break value; 返回的值。
let sign = if n > 0 { 1 } else if n < 0 { -1 } else { 0 };
let label = match side {
0 => "buy",
1 => "sell",
_ => "unknown",
};
if 的两个分支必须同类型——1 / -1 / 0 都是 i32, 所以 sign: i32。while cond { ... } 与最常见的 for i in 0..n { ... } 与 Python 直觉一致。半开区间 0..n 不含上界, 0..=n 包含上界。带标签的循环 'outer: loop { ... break 'outer; } 用于多层 break, 数值代码里很少见。
match 是穷尽性 (exhaustiveness) 的多分支选择器。漏写一个分支编译器会报 E0004: non-exhaustive patterns——这条性质在你开始对 enum 做匹配时会承重。_ 通配符匹配「其余一切」; 生产代码通常宁愿显式写出每个分支, 这样以后新增一个 enum 变体时 match 会失配并强迫开发者做出有意识的决定。if let Some(x) = opt { ... } 是「只匹配一个变体」的简写; Option<T> 的完整故事在 lesson 4。
for 循环与半开区间
let mut total: f64 = 0.0;
for i in 0..n {
total += prices[i];
}
区间 0..n 产出 0, 1, ..., n-1; 若要包含 n 就写 0..=n。累加器 total 标 mut 因为它的值要原地更新。迭代器链 (.iter().sum(), .map().collect() 之类) 留到 lesson 3 与 lesson 5; 本课用最朴素的 for i in 0..n, 与 C++ / Python 的对应关系最直接。
函数
函数声明 fn name(arg: T, ...) -> ReturnType { body }。函数体是块表达式, 最后一个不带分号的表达式就是返回值。显式 return x; 也可以, 但只在早期 return (early return) 时才惯用:
fn add(a: i64, b: i64) -> i64 { a + b }
fn add_explicit(a: i64, b: i64) -> i64 { return a + b; }
两种写法都能编、返回值相同; 风格指南推崇隐式返回的第一种。语句以 ; 结束, 表达式不带 ;。参数有三种传递形式——按值 (fn f(x: i64))、共享引用 (fn f(x: &i64))、可变引用 (fn f(x: &mut i64))——本课规则是: Copy 标量 (所有整数、所有浮点、bool、char) 一律按值传; & / &mut 这两种推到 lesson 3 里由借用检查器自己解释为什么。Rust 不存在函数重载——这是特性而非缺失, 每个调用点都能无歧义确定调用的是哪个函数。
实战示例: Black-Scholes call
承接 region_notes 的 510300.SH 沪深300 ETF 期权场景 (现价 4.20, 行权价 4.30, 无风险利率 2.5%, 波动率 22%, 到期 0.25 年——与 3.3.1 Python Monte Carlo 和 3.4.1 C++ closed-form 同一组输入, 三种语言落地的价格可横向对照):
fn black_scholes_call(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
// d1, d2, N(d1), N(d2) 用 f64::ln / f64::sqrt / f64::exp
// 加一段内联的 erf 多项式近似; 细节见辅助模块
let n_d1 = 0.0_f64; // placeholder
let n_d2 = 0.0_f64; // placeholder
s * n_d1 - k * (-r * t).exp() * n_d2
}
然后在 main 里:
let price = black_scholes_call(s, k, r, sigma, t);
println!("{:.4}", price);
签名是 (s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64——五个 Copy 标量入, 一个 Copy 标量出, 全部按值传递。函数体最后一个表达式 s * n_d1 - k * (-r * t).exp() * n_d2 不带分号, 即返回值。方法调用语法 (-r * t).exp() 是对负 r*t 调 f64::exp 的写法; 闭式公式只需要 f64::sqrt、f64::ln、(-x).exp() 四种运算 (标准库不带 erf, 生产代码用 statrs crate 或 Abramowitz & Stegun 7.1.26 的多项式近似——后者中译本《数学手册》对应章节给出参数表)。
println! 里的 "{:.4}" 是格式化规约 (format spec), 把浮点打到四位小数。println! 是宏 (! 表明是宏), 格式串在编译期就检查——写 "{:.4} {}" 却只传一个参数是编译错而不是运行崩溃; 这条 「编译期检查格式串」 也是本模块所有例子都用 println! 和 assert_eq! 的原因。
行业说明: 国内私募 / 券商自营在用 Rust 写新定价 / 撮合相关代码时, 价格通常以 i64 的「分之一分」精度存储 (例如 510300.SH 报价 4.200 元存为 4200, 单位 0.001 元), 浮点只在内部数学计算与对外报告时出现。本课的浮点 worked example 是为教学清晰, 生产里同一段逻辑会显式区分定点 i64 与浮点 f64, 但你正在学的类型系统机制完全一样。
那条承重的概念
除了 let 绑定和「项」(items, 即函数 / 结构体 / 模块这一类) 之外, 一切皆表达式; { ... } 块本身也是一个表达式, 其值是块内最后一个不带分号的表达式。把这一句内化, 你能省下未来十次 「expected ;, found }」 的编译报错。
练习
Exercise
写一个函数 fn put_call_parity_price(call_price: f64, s: f64, k: f64, r: f64, t: f64) -> f64, 用 put-call parity 公式 (P = C - S + K * exp(-r * t)) 返回欧式 put 价。(a) 用隐式返回 (没有 return 关键字, 最后一个表达式不带分号) 写成一行函数体。(b) 在 main 里, 把 black_scholes_call 返回的 call 价以及同样的 s / k / r / t 传入, 用 println!("{:.4}", put_price) 打出 put 价。(c) 手算确认: 当 s == k * exp(-r * t) (at-the-forward) 时, 函数应当返回与 call_price 近似相等的数。(d) 加一段 let mut total: f64 = 0.0; 加一个 for i in 0..5 { total += put_call_parity_price(...); } 循环, 把五份相同的 put 价累加到 total 再打印——目的只是把 mut、for、半开区间 0..5、浮点算术一次性练熟。
提示
call_price - s + k * (-r * t).exp(); 无 return、无分号。最后一个表达式自动作为返回值。