示例代码解析
// 泛型函数:使用 trait bound 限定类型参数的行为
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
这段代码是 Rust "泛型(<T>)" 与 "特征约束(Trait Bound)" 最经典的例子。
我们依然用最直观的**"先做减法,再做加法"**的方式,分 4 步 把它彻底拆解开:
第一步:先把 <T: PartialOrd> 遮住,看它本来在干嘛?
假设我们只想写一个函数,从一堆整数(i32)里找出最大的那个数。代码会写成这样:
Rust
// 传入一个整数切片 &[i32],返回最大那个整数的引用 &i32
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0]; // 先假设第 0 个元素是最大的(拿着它的借条)
for item in list { // 挨个遍历数组里的每个元素借条 item
if item > largest { // 如果发现有人比当前最大的还要大
largest = item; // 就把 largest 这张借条改指向新的擂主!
}
}
largest // 最后一行不加分号,相当于 return largest
}
逻辑非常清楚,就是个最普通的**"打擂台找最大值"**算法。
第二步:为什么要引入 <T>?(泛型:一份代码,万物通用)
上面的代码有个痛点:它写死了 i32。如果明天你想找一堆小数(f64)里的最大值,或者一堆字符(char,比如 'a', 'z')里的最大值,难道要把上面这段一模一样的代码复制粘贴三遍吗?
于是,我们把具体的 i32 替换成一个类型占位符 T(就像数学里的未知数
XX
):
Rust
fn largest<T>(list: &[T]) -> &T
-
函数名后面的 <T> :向编译器声明"注意啦,接下来函数里出现的 T 不是一个真名,而是一个任意类型的代号"。
-
list: &T:表示传进来的是一个装满 T 类型元素的数组切片引用。
-
-> &T:表示最后返回出去的也是 T 类型元素的引用。
第三步:为什么必须在 T 后面加上 : PartialOrd?(特征约束)
如果你只写 fn largest<T>(list: &T) -> &T,编译器会立刻报错,指着函数里面的这一行骂你:
Rust
if item > largest // ❌ 报错:你怎么敢保证随便传个 T 进来都能用 > 号比大小?!
编译器的顾虑非常合理 :
万一有个人调用 largest,传进来的是你前几题定义的 User 结构体数组呢?两个 User 怎么用 > 比大小?按年龄比还是按名字长度比?根本没法比!
所以,你必须给未知数 T 设一个**"准入门槛"**:
Rust
fn largest<T: PartialOrd>(list: &[T]) -> &T
-
PartialOrd 是 Rust 标准库提供的一个接口(在 Rust 里叫 Trait 特征),专门代表**"可以比较大小(支持 >、< 符号)"**的能力。
-
<T: PartialOrd> 翻译成人话就是:"我这个函数什么类型 T 都接,但有一个硬性条件------传进来的 T 必须是天生能比大小的类型(比如整数、小数、字符、字符串)!不能比大小的类型免谈!"
第四步:两个顺带复习前面知识的精妙小细节
-
为什么是 let mut largest = &list0; 而不是 &mut list0?
-
注意 mut 写在变量名 largest 前面,而 & 后面没有 mut!
-
这是因为我们根本不想修改数组里的任何数字 (所以只需要只读借条 &list0);我们修改的是 largest 这个变量本身手里的借条指向谁(一开始指向第 0 个,后来改指向更大的那个 largest = item;)。
-
-
为什么这里不用像上一题那样写生命周期 <'a>?
-
你可能会问:"这函数也是传进去引用 &T,返回引用 &T,怎么不用写 'a 了?"
-
因为这里只有 list 这一个输入参数 !编译器用脚趾头想都知道,返回的最大值借条 100% 是从 list 里面掏出来的,寿命必然跟 list 一致,所以编译器自动帮你推导了,省去了手写 'a 的麻烦!
-
-
泛型会影响运行速度吗?(依然是零成本抽象!)
- 完全不会! 当你在代码里分别用整数 i32 和小数 f64 调用 largest 时,编译器会在编译期自动在后台帮你生成两份专属的机器码(就像你手写了两个函数一样),运行时没有任何类型判断的性能损耗。*]Adjusting final characters to clean markdown. Let's output the response.这段代码是 Rust "泛型(<T>)" 与 "特征约束(Trait Bound)" 最经典的例子。
上面largest 变量是引用,为什么能比较大小?
你这个问题问到了非常核心的细节!
这里其实藏着两个大疑惑:
-
如果写过 C/C++:两个指针(引用)直接用 > 比较,难道比的不是"内存地址谁大谁小"吗?为什么在 Rust 里比的是"里面的数值"?
-
从类型上看:函数头只规定了 <T: PartialOrd>(T 本体能比大小),为什么手里拿的是引用 &T(item 和 largest 都是 &T),不用写 *item > *largest 就能直接比?
答案在于 Rust 的两个底层设计:
1. Rust 规定:引用比大小,永远比的是"里面的值",绝不比内存地址!
在 C 语言中,如果你拿两个指针 p1 > p2,比的是它们在内存条上的地址编号,这在日常写业务逻辑时经常引发灾难性的 Bug。
Rust 认为:99.99% 的程序员拿两个引用比大小,想比的都是里面装的数据,而不是内存地址。
所以 Rust 直接在语言层面规定:对两个引用使用 ==、!=、>、< 时,一律穿透引用,去比较它们指向的真实数据!
2. 为什么不用手动写 *item > *largest?(标准库的"买一送一"魔法)
为什么我们只写了 T: PartialOrd,连 &T 也能直接用 > 号呢?因为底层发生了两件事:
① 在 Rust 里,> 号本质上是一个"方法调用"
当你写下:
Rust
if item > largest
编译器在后台其实把它翻译成了一个点号 . 的方法调用(gt 代表 Greater Than 大于):
Rust
if item.gt(&largest)
② 标准库自带"连带继承(买一送一)"规则
Rust 的标准库里提前写好了一段通用代码,翻译成人话就是:
"只要某个类型 T 能比大小(PartialOrd),那么它的引用 &T(甚至多层引用 &&T)也自动获得比大小的能力!具体怎么比?编译器会自动在底层帮你加上 * 解开引用去比!"
也就是说,你写的:
Rust
if item > largest
编译器在底层自动帮你展开成了:
Rust
if (*item) > (*largest)
3. 那我如果强迫症发作,非要手动写 *item > *largest 可以吗?
完全可以!
你可以把那行代码改成:
Rust
if *item > *largest {
largest = item;
}
这两种写法 100% 等价,都能正常通过编译,而且最终生成的机器码一模一样。Rust 允许你不写 *,纯粹是为了让你写代码时视觉更清爽、少敲两个星号而已。
let mut largest = &list0把&去掉会有什么区别?
如果把 let mut largest = &list0; 里的 & 去掉 ,写成 let mut largest = list0;,编译器会立刻报出 3 个连锁错误,代码彻底无法通过编译!
而且最妙的是,第一个错误正好就是你前两题刚问过的**"拿着借条想强行搬走别人东西"**的问题!
我们来看看去掉 & 后会发生什么灾难:
错误一(当场暴毙):试图从借来的数组里"把东西搬走"!
看函数参数:list: &T。这说明 list 只是别人借给你看的一个数组切片。
-
有 & 时(&list0) :
意思是"我只拿第 0 个元素的一张借条(&T)",不动数组里的真身。
-
去掉 & 时(list0) :
意思是"我要把第 0 个元素的**真身(T 本体)**直接从数组里掏出来,搬(Move)给 largest 变量"!
编译器当场拦截 :
万一别人传进来的是一个字符串数组 &String 呢?String 在堆上,不支持自动复制(Copy)。如果你把第 0 个 String 搬走了,原来的数组第 0 个位置就空了!
所以编译器在这行会直接报错:
❌ cannot move out of type T, a non-copy slice(无法从借来的数组切片中把元素搬走!)
错误二(连锁反应):后面的 item 和 largest 类型全打架了!
假设去掉了 &,那么此时 largest 的类型变成了 T(值本体) 。
但是往下一行看:
Rust
for item in list { ... }
因为 list 本身是个引用 &T,所以 for 循环每次从里面摸出来的 item 的类型依然是 &T(引用)!
接下来两行直接崩溃:
-
if item > largest 报错:左边 item 是引用 &T,右边 largest 是本体 T,一个穿了马甲一个没穿马甲,类型不一致,根本没法比大小!
-
largest = item; 报错:largest 是 T 本体,而 item 是 &T 引用,没法把一张借条塞给一个本体变量!
错误三(收尾暴毙):返回值类型对不上!
看函数头部的承诺:fn largest<...>(...) -> &T,它承诺最后交出去的是一个引用 &T。
-
如果 largest 去掉了 & 变成了 T 本体,最后一行返回 largest 时,类型就对不上了(承诺给 &T,实际给 T)。
-
你可能会问:那我最后一行写成 &largest 返回它的引用行不行?
更不行! 因为如果 largest 是函数内部的一个局部变量本体,一旦函数执行结束,largest 就会在栈上被当场销毁!你把一个死人的借条 &largest 返回给外面,就会触发 Rust 最痛恨的**"悬垂野指针"**报错!
💡 延伸思考:如果我非要去掉 &,怎么改才能合法?
如果你非要让 largest 不拿借条,而是直接拿值本体 T,你就必须多加一个限制条件 Copy(只允许数字、布尔、字符这种能在栈上随便复印的类型进来),并且把全家桶一起改掉:
Rust
// 1. 加上 + Copy 门槛(禁止 String 这种不能复制的类型入内),且返回值改成 T 本体
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0]; // ✅ 合法了!因为 T 支持 Copy,这里直接复印了一份第 0 个元素
// 2. 用你之前学的"消元法" &item,把遍历出来的引用也顺手抵消成 T 本体
for &item in list {
if item > largest {
largest = item;
}
}
largest // ✅ 直接把复印出来的最大值本体返回出去
}
对比总结 :
为什么原版教程里要保留 &list0?
因为**加上 & 只操作借条,既不需要复制任何内存(哪怕数组里装的是几百兆的大对象也瞬间完成),又能同时支持 String 等所有不能 Copy 的类型!**这就是 Rust 用引用操作数据的精髓所在。
let mut largest = &list0;在调用*largest修改值合法吗
不合法!编译会当场报错!
这是一个极其经典的易混点(相当于 C/C++ 里的常量指针 vs 指针常量,但 Rust 的语法清晰得多)。
报错的原因很简单:还要看 mut 到底写在谁的头上!
1. 为什么 let mut largest = &list0; 不能改 *largest?
我们把这行代码拆成左右两边看:
Rust
let mut largest = &list[0];
// ^^^ ^
// 这里有 mut 注意:这个 & 后面没有 mut!
-
右边 &list0(没有 mut):说明你借出来的是一张**"只读借条"(类型是 &T)**。既然是只读借条,你只被允许看里面的值,绝对没有权力去修改它指向的数据。
-
左边 let mut largest(有 mut):这个 mut 修饰的是 largest 这个变量名本身------意思是**"允许 largest 扔掉旧借条,换拿另一张新借条"**(也就是代码后面的 largest = item;)。
如果你强行写 *largest = 新值;,编译器会直接报错:
❌ cannot assign to *largest, which is behind a & reference(无法给 *largest 赋值,因为它背后只是一个 & 只读引用!)
2. 一张表彻底分清 mut 放在不同位置的区别
你可以把引用変数想象成**"你的手拿着一根指向数据的激光笔"**。在 Rust 里,一共有 3 种组合写法,权限截然不同:
|---------------------------------------------------|---------------------------|------------------------------|----------------------------------------|
| 代码写法 | 手能不能换指别人? (largest = 新引用) | 能不能修改指着的数据? (*largest = 新值) | 形象解释 |
| let mut largest = &list0; (截图里的写法) | ✅ 可以 | ❌ 不可以 | 手可以随便晃动换目标,但手里拿的是只读放大镜,只能看不能改。 |
| let largest = &mut list0; (你之前改字符串那题的写法) | ❌ 不可以 | ✅ 可以 | 手被胶水粘死了不能换目标,但手里拿的是修改画笔,可以改那个目标的值。 |
| let mut largest = &mut list0; (两边都加 mut) | ✅ 可以 | ✅ 可以 | 满配权限:手既可以换指别的目标,又能用画笔修改目标里面的值! |
(⚠️ 补充:如果你想在 largest 函数里用第 2 或第 3 种写法去改数组里的元素,那么函数最开头的参数也必须从只读切片 list: &T 改成可变切片 list: &mut T 才行。)
一句话口诀
-
mut 跟在 let 后面(let mut x) :代表 x 可以换绑别的东西;
-
mut 跟在 & 后面(&mut val) :才代表 可以用 *x 修改里面的值!
fn largest<T: PartialOrd>(list: &T) -> &T 入参能去掉&什么区别
如果把入参 list: &T 里的 & 去掉 ,从"传借条"变成"直接传本体",会引发 3 个层面的重大区别(前两个会直接导致代码无法编译):
区别一(语法底层):直接写成 list: T 会当场报错------因为编译器不知道它占多大内存!
如果你单纯把 & 删掉,写成:
Rust
fn largest<T: PartialOrd>(list: [T]) -> &T // ❌ 编译直接报错!
-
为什么报错?
在 Rust 中,T 代表"一串数量不固定的连续元素"(可能是 3 个,也可能是 100 万个)。而函数在调用时,必须在栈(Stack)上为参数预留固定大小的格子。因为 T 的体积在编译期是个未知数,所以裸的 T 根本不允许直接作为参数传递!
-
为什么加上 &(&T)就可以?
因为 &T 是一张**"胖指针借条"------不管原数组有 3 个元素还是 100 万个元素,这张借条在 64 位电脑上永远只占固定的 16 个字节**(8 字节记录数组开头的内存地址 + 8 字节记录数组有多长)。大小固定,而且传递成本极低!
区别二(生死存亡):哪怕改成传动态数组本体 list: Vec<T>,返回值 -> &T 也会变成"死人借条"而报错!
你可能会问:"那我不写 T,改成传固定头大小的动态数组本体 list: Vec<T>(不加 &,直接把数组所有权传进去)行不行?"
Rust
fn largest<T: PartialOrd>(list: Vec<T>) -> &T { // ❌ 依然编译报错!
// ...
largest // 报错:返回了指向已销毁数据的悬垂引用!
}
来看看根据你前面学的"所有权三大规则"会发生什么:
-
所有权转移(Move) :因为参数没有 &,调用者把整个数组 list 的所有权直接过户给了 largest 函数。
-
离开作用域自动销毁(Drop) :当 largest 函数执行完最后一行大括号 } 时,身为局部变量的 list 寿命到期,整个数组会在函数结束的瞬间被当场销毁、内存被清空!
-
返回了野指针:list 都已经尸骨无存了,你却返回一个指向 list 内部某个元素的引用 -> &T!编译器会当场拦住你:
❌ cannot return value referencing function parameter list(绝不允许返回指向函数内部即将被销毁参数的引用!)
(注:如果不加 & 传参数,那么返回值也必须去掉 & 写成 -> T,把那个最大值从数组里拆出来把本体交出去才行。)
区别三(使用体验):"借看一眼" vs "把原数组吃掉销毁"
就算我们把入参和返回值全都去掉 &(写成 fn largest(list: Vec<i32>) -> i32),站在调用者的角度,体验也是天差地别的:
-
有 & 时(只借用,原数组完好无损):
Rust
let nums = vec![10, 50, 30]; let max = largest(&nums); // 只是把账本借给函数看一眼 println!("最大是: {}, 原数组还在: {:?}", max, nums); // ✅ nums 还能继续用! -
去掉 & 时(夺走所有权,原数组用完即焚):
Rust
let nums = vec![10, 50, 30]; let max = largest(nums); // 把整个账本送给函数了,函数算完顺手把账本烧了! // println!("{:?}", nums); // ❌ 报错!nums 已经被移走销毁了,外面再也不能用了!
总结
在写函数时,如果函数只是想读取/计算一下参数里的数据 (比如求最大值、求和、打印、搜索),入参 100% 都要加上 &。这样:
-
速度快:不管数据多大,只传 8 或 16 字节的借条,零内存拷贝;
-
不毁原数据:调用者手里的原变量在函数跑完后依然能继续使用;
-
能返回内部引用:因为原数据在外面还活着,函数才能安全地返回指向它内部元素的引用 -> &T。
悬垂野指针

这段程序之所以报错,是因为它触犯了 Rust 借用检查的核心铁律,并且在底层会引发极度危险的**"悬垂野指针(Use-After-Free)"**。
我们可以从**"规则层面"和"底层内存真相"**两个角度来彻底理解:
一、 规则层面:读写互斥(不能一边借来看,一边偷偷改)
我们给代码标上生命线:
Rust
fn main() {
let mut s = String::from("runoob");
let slice = &s[0..3]; // 1. slice 借出了一个【只读借条】(&str)
// ┌─── 只读借条 slice 的生命期开始 ───┐
s.push_str("yes!"); // 2. ❌ 试图获取【可变借用】(&mut) 进行修改! │
// │ │
println!("slice = {}", slice); // 3. 借条最后在这里使用 ───────────────┘
}
-
借条还在用 :最后一行 println! 用到了 slice,说明第 1 行创建的只读借用一直存活到了最后一行。
-
中间却想改:还记得前面讲的吗?s.push_str 在底层需要的参数是 &mut self(可变借用)。
-
触犯铁律:Rust 严厉禁止**"在只读借用存活期间,同时去获取可变借用(改写数据)"**。
-
编译器会直接报错:
❌ cannot borrow 's' as mutable because it is also borrowed as immutable(无法将 s 作为可变借出,因为此时它已经被作为不可变借出了!)
-
二、 底层内存真相:如果不报错,会导致内存崩溃!
Rust 为什么要这么严苛?这绝不是为了刁难你,而是为了拯救你的程序不被踩内存崩溃:
-
String 在堆内存上的特性 :
字符串在堆上有一个预先分配的容量(Capacity)。
-
追加内容可能触发"内存搬家(Reallocation)" :
当调用 s.push_str("yes!") 时,如果原有的堆空间装不下了,操作系统就会在堆内存的其他空闲位置重新申请一块更大的新内存 ,把老数据拷贝过去,然后直接把老内存释放销毁(free)!
-
致命灾难来了:
-
你的 slice 是一个切片指针,死死指向老内存的起始地址。
-
如果允许 push_str 执行,老内存一旦被销毁,slice 瞬间就变成了指向垃圾荒地的野指针(Dangling Pointer)!
-
等到最后一行 println! 去读 slice 时,程序就会读取到不可预料的乱码数据,甚至当场崩溃!
-
在 C / C++ 里,这种 Bug 叫 "迭代器/指针失效(Iterator Invalidation)" ,每年引发无数高危漏洞;而 Rust 在编译期就直接掐死了这种可能。
三、 怎么改才能正确?
解决方法 1:调整代码顺序(先用完借条,再修改)
Rust 拥有先进的 NLL(非词法作用域生命周期) 。只要你在修改前把借条彻底用完,借条就会提前失效,此时就可以随意修改了:
Rust
fn main() {
let mut s = String::from("runoob");
let slice = &s[0..3];
println!("slice = {}", slice); // ✅ 先把借条用完!到此为止,只读借条的使命结束!
s.push_str("yes!"); // ✅ 此时没有任何只读借条存在了,可以放心修改!
}
解决方法 2:如果两个都要用,复制一份新数据(脱离借用关系)
如果不借条,而是把切片真正克隆/转换为独立拥有的 String,它们之间就没有利益纠葛了:
Rust
fn main() {
let mut s = String::from("runoob");
let slice = s[0..3].to_string(); // ✅ 自己在堆上重新开辟内存复制一份,成为独立所有者
s.push_str("yes!"); // 随便改 s,跟 slice 没有任何关系
println!("slice = {}", slice); // ✅ 正常运行!
}