Rust 的地基:变量、函数与表达式
在 magpie 的世界里,你在任何要填模型的地方写的都是一行字符串:openai/gpt-5.2 指向某个供应商(provider)的具体模型,group/coder 指向一个路由组(routing group,第 2 章的拓扑里见过)。jackdaw 要用这行字符串,第一步是把它拆开。这一章逐行读 jackdaw 里专门干这件事的函数。读完你能回答:
let raw = raw.trim();里前后两个raw各指什么?为什么不需要mut?pub fn parse(raw: &str) -> Result<Self, String>从左到右每一块是什么?- 撞见
fn main() -> Result<(), Box<dyn Error>>,能不能当场拆开讲给别人听?
主线:一个必须拆开的字符串
用户给的字符串不可信:可能有首尾空白,可能没有斜杠,可能斜杠一边是空的。jackdaw/src/model.rs 的 ModelRef::parse 在路由之前把这些问清楚。拆的过程中要有临时名字:Rust 用 let 造名字——变量绑定(let binding),默认不可变,要原地改值才加 mut(可变性)。注意下面整段没有一个 mut:每次"改"都是造新值再绑定,而不是原地修改。
// ...
pub fn parse(raw: &str) -> Result<Self, String> {
let raw = raw.trim();
let Some((head, rest)) = raw.split_once('/') else {
return Err(format!(
"invalid model reference {raw:?}: no `/` found. \
Expected `<provider>/<model-id>` or `group/<id>`"
));
};
if head.is_empty() || rest.is_empty() {
return Err(format!(
"invalid model reference {raw:?}: neither side of `/` may be empty"
));
}读法: 从 let raw = raw.trim(); 看起。参数 raw 是字符串切片(&str):指向字符串字节的只读视图;trim() 给出去掉首尾空白的新切片,又绑回 raw——这是遮蔽(shadowing),新 let 盖住旧名字,连类型都可以换,和 mut 的原地修改是两回事。这行不用写类型:编译器从 trim() 推出仍是 &str(类型推断)。下一行是 let-else:split_once('/') 返回选项类型(Option),要么 Some((斜杠前, 斜杠后))、要么 None;值是 None 时左边匹配不上,就走 else 提前 return Err(...),括号里那段就是用户原样看到的报错。format! 带 !,是宏——先当作函数用,细节不追。紧跟着的 if 是同一招再用一遍:斜杠任一边是空的(比如 /gpt-5.2),同样提前 return Err,只是换了报错文案。
基本类型(primitive types)顺带认脸熟:u32/i64 整数、f64、bool、char、单元类型 ()——只有一个值(就写作 ())、不携带信息的类型;数字字面量默认推成 i32,要别的写明,如 let port: u16 = 3426;。
签名即契约:一行 fn 怎么读
函数(fn)的签名是写给读者的契约。逐块拆 pub fn parse(raw: &str) -> Result<Self, String>:pub——别的模块能用(第 8 章展开);括号里是 名字: 类型,参数类型必须显式;-> 后是返回类型,同样必须显式。返回结果类型(Result):成功的一半是 Self,即 impl ModelRef 块所属的类型,也就是 parse 造出来的东西;失败的一半是 String,一段直接给人读的文案。Self 长什么样?定义就在同文件:
// ...
/// A parsed model reference.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelRef {
/// Direct reference: name of a `[providers.<name>]` table plus the
/// upstream model id.
Provider { provider: String, model_id: String },
/// Routing-group reference: `group/<id>`.
Group { id: String },
}读法: 从 pub enum 看起。枚举(enum)是"一个类型、几个已知形状之一":要么 Provider(带 provider、model_id 两个命名字段),要么 Group(带 id)。字段类型是堆字符串(String),不是只读的 &str——拆出来的片段要保存下来交给后面的路由;谁拥有这些数据,所有权第 5 章展开。#[derive(...)] 让编译器自动实现"能打印、能比较"这些能力(派生,derive;第 6 章展开)。第一次读带走一句就够:parse 的产物是个二选一的值。
表达式:最后一行没有分号
多数语言里 if 是语句,当不了值;Rust 里 if、match、{} 块都是表达式与语句中的"表达式"——有值。let、return 是语句;块的值等于它最后一个表达式的值;给表达式加 ; 就变成语句、丢掉值(得到 ())。于是函数体最后一行不带分号,它就是返回值:
// ...
if head == RESERVED_PROVIDER_NAME {
// The reserved name routes to groups, so `group/coder` means
// "group with id `coder`", never "provider named group".
Ok(ModelRef::Group {
id: rest.to_string(),
})
} else {
Ok(ModelRef::Provider {
provider: head.to_string(),
model_id: rest.to_string(),
})
}
}读法: 从 if 那行看起。head、rest 是上一节 let-else 绑定的两个名字,在这里被消费。RESERVED_PROVIDER_NAME 是文件顶上的常量 pub const RESERVED_PROVIDER_NAME: &str = "group";——group 这个名字留给了路由组,叫这个名的 provider 永远到不了,配置校验会拒绝它。两个分支的值都是 Ok(...),里面是构造枚举变体的表达式,rest.to_string() 从切片造出新的 String。注意 else 块收尾的 } 后面没有分号、没有 return:整个 if/else 是 parse 的尾表达式,它的值就是函数的返回值。
同一个文件里还有一处模式匹配(pattern matching),把值反向拼回字符串:
// ...
impl fmt::Display for ModelRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ModelRef::Provider { provider, model_id } => {
write!(f, "{provider}/{model_id}")
}
ModelRef::Group { id } => write!(f, "{RESERVED_PROVIDER_NAME}/{id}"),
}
}
}读法: 从 match self 看起:按值的形状分派——Provider 就把两个字段拼回去,Group 就拼成 group/<id>;每个 => 右边是表达式,整个 match 的值又作为 fmt 的尾表达式返回。impl fmt::Display for ModelRef 先当作"定义这个类型怎么打印"的固定句式,特征是什么第 7 章展开;match 必须列全所有变体(穷尽性),第 6 章展开。
拆开 fn main() -> Result<(), Box<dyn Error>>
无数 Rust 项目和 AI 生成的第一版代码,入口都是这一行。现在你能自己拆:
fn main():程序入口,运行时调它,不是你调它;->:后跟返回类型,main也可以有返回值;Result<(), Box<dyn Error>>:成功的一半是()——成功时"没什么可说的";失败的一半是Box<dyn Error>——装着任意错误的盒子(Box是智能指针,dyn Error第 7 章展开)。
整行读作:"成功时无事可报,失败时带着错误退出"。这么写,是为了能在 main 里用问号运算符(? operator):
// 示意代码,不是 jackdaw 的源文件;ModelRef 假定已在作用域内
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let parsed = ModelRef::parse("openai/gpt-5.2")?;
println!("{parsed:?}");
Ok(())
}读法: 从带 ? 的那行看起:parse 返回 Ok,? 把里面的 ModelRef 解出来绑定给 parsed;返回 Err,? 让 main 立刻以 Err 结束——运行时打印错误,进程以非零码退出(退出码第 10 章展开)。{parsed:?} 能打印,靠的正是 #[derive(Debug)]。末行 Ok(()) 是尾表达式:成功、无值。? 还会顺手把 String 错误转成 Box<dyn Error>,这一步第一次读可以跳过。
小结
两个 raw 是遮蔽,不是修改;签名从左到右读:pub fn 名(参数: 类型) -> 返回类型;枚举是几选一的值;不带分号的最后一行就是返回值;Result<(), Box<dyn Error>> 的 main 意思是"成功无事、失败带错"。下一章追问 parse 里 &str 进、String 出的差别——这些数据的所有权归谁。
自检
- 你写 JavaScript 时习惯
raw = raw.trim()。对照let raw = raw.trim();:两个raw是什么关系?为什么这里不需要mut? - 不运行代码:
ModelRef::parse("openrouter/deepseek/deepseek-chat")的Ok里,provider和model_id各是什么?ModelRef::parse("/gpt-5.2")返回什么? - parse 结尾的 if/else 后面没有分号。如果补上一个,会发生什么?这说明函数体的最后一行在 Rust 里是什么?
参考答案
- 第二个
let新建了一个绑定,遮蔽同名参数;trim()返回新切片,原参数没被改写。不需要mut,因为没有变量被原地修改。 provider是openrouter,model_id是deepseek/deepseek-chat——只按第一个斜杠拆,后面原样保留。"/gpt-5.2"斜杠左边为空,触发第二道检查,返回Err,报错文案是 "neither side of/may be empty"。- 分号把 if/else 变成丢弃值的语句,函数尾表达式的值成了
(),与声明的Result<Self, String>不符,编译不通过。最后一行是"函数的值"这个表达式,不是一串动作的收尾。