惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
MyScale Blog
MyScale Blog
有赞技术团队
有赞技术团队
V
Visual Studio Blog
T
The Blog of Author Tim Ferriss
爱范儿
爱范儿
Vercel News
Vercel News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
D
DataBreaches.Net
美团技术团队
Microsoft Security Blog
Microsoft Security Blog
大猫的无限游戏
大猫的无限游戏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
GbyAI
GbyAI
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
The Cloudflare Blog
宝玉的分享
宝玉的分享
V
V2EX
Microsoft Azure Blog
Microsoft Azure Blog

smallyu的博客

为什么买的加密货币一定要去中心化 RISC-V 虚拟机无法代替 EVM 虚拟机 Polkadot 的 Existential Deposit 机制错在哪里 利息,套利,交易策略,金融市场 LayerZero 从来不挑战比特币的地位 默认参数引起的以太坊节点运行错误 硬件钱包与资产安全 预测市场比币圈更赌场 为什么去中心化的跨链桥不可能实现 我对于 AI 时代的答案 不要投资任何隐私币 区块链技术世界的三大真理 以太坊 AA 钱包的致命问题是什么 基于 AI 语义执行的 MCP 区块链的设计 一个集成 Geth 和 CometBFT 的兼容层 我的加密货币定投策略(二) DeFi 基础: 理解 AMM 定价机制 Go 语言 GMP 调度器的原理是什么 Web3 项目分析计划 对 Psyche Network 项目的分析 continuation 教程: 理解 CPS 如何开发一个比特币符文(Runes)协议 比特币脚本开发教程 我从王垠的计算机科学视频班学到了什么 区块链技术面试题(2025年版) Solana 智能合约开发教程 (1) 尝试开发一个最小 EVM 虚拟机 基于 ZK 的链上身份系统设计 一个 Web3 打赏系统的设计 鼓吹 Cursor 的人技术能力都差
Rust 语言容易让新手困惑的一个“过度优化”
2025-06-29 · via smallyu的博客

2025-06-30

假如我们现在要写一些代码,随便用 cargo new 一个项目就行,然后写一个函数 append,函数的功能很好理解,就是把两个传入的字符串给拼接起来,第一个参数是字符串(的引用类型),第二个参数也是字符串,假如我们的参数是 Hello, world,函数调用后会返回 Hello, world 给我们。函数具体这样写:

fn append(s1: &String, s2: &String) -> String {
    return s1.clone() + s2.clone().as_str();
}

不需要关心 return 后面的语句写法,这不是我们关注的重点。在入口函数 main 里调用这个 append,运行一下,输出的内容会和我们预期一样,打印出拼接后的字符串 Hello, world

fn main() {
    let s1: String = String::from("Hello");
    let s2: String = String::from(", world");
    println!("{}", append(&s1, &s2));
}

那么现在,保持 append 函数完全不变,在 main 函数里修改两个字符串的定义,整个 main 函数变成这样,猜一下输出结果会是什么?注意 Rust 是静态类型的语言,编译器对于变量类型往往具有严格的定义和判断:

fn main() {
    let s1: Box<String> = Box::new(String::from("Hello"));
    let s2: Box<String> = Box::new(String::from(", world"));
    println!("{}", append(&s1, &s2));
}

我们首先的直觉是应该编译报错,因为 s1 的类型是 Box<String>,调用 append 函数的时候,传入的参数为 &s1,对应的类型为 &Box<String>,而显然 append 函数的定义是没有修改的,接收的参数类型仍然是 &String。那么这种情况下,为什么编译器没有报错,而且代码还能正常运行,输出了 Hello, world 的结果?(先别管这里的 Box 是什么,反正是一种类型)

我们接着再修改一下 main 函数的内容,把字符串的定义改为这样:

fn main() {
    use std::rc::Rc;

    let s1: Rc<String> = Rc::new(String::from("Hello"));
    let s2: Rc<String> = Rc::new(String::from(", world"));
    println!("{}", append(&s1, &s2));
}

代码能通过编译吗?能正常运行吗?append 函数的定义仍然没有变,这里 main 函数中 s1 的类型变成了 Rc<String>,相应的传入 append 函数做参数的时候,类型变为了 &Rc<String>。但是为什么,编译器没有报错,而且还能正常运行出结果,输出 Hello, world?(同样别管 Rc 是什么,也是一种类型)

根据刚才的代码片段,我们观察到一个现象:当函数的参数类型是 &String 的时候,既可以接受 &String 类型的参数,也可以接收 &Box<String> 类型的参数,还可以接收 &Rc<String> 类型的参数。

再疯狂一点,如果把 main 函数改成这样呢?

fn main() {
    let s1: Box<Box<Box<Box<String>>>> = Box::new(Box::new(Box::new(Box::new(String::from("Hello")))));
    let s2: Box<Box<Box<Box<String>>>> = Box::new(Box::new(Box::new(Box::new(String::from(", world")))));
    println!("{}", append(&s1, &s2));
}

如果把 main 函数改成这样呢?

fn main() {
    use std::rc::Rc;
    
    let s1: Rc<Rc<Rc<Rc<String>>>> = Rc::new(Rc::new(Rc::new(Rc::new(String::from("hello")))));
    let s2: Rc<Rc<Rc<Rc<String>>>> = Rc::new(Rc::new(Rc::new(Rc::new(String::from(", world")))));
    println!("{}", append(&s1, &s2));
}

结果是 main 函数都可以正常运行,输出 Hello, world 的结果。

为了进一步观察关于类型的问题,现在新写两个 append 函数,append2 函数接收的类型是 &Box<String>,而 append3 函数接收的类型是 &Rc<String>

fn append2(s1: &Box<String>, s2: &Box<String>) -> Box<String> {
    let mut result = (**s1).clone();
    result.push_str(s2);
    Box::new(result)
}

use std::rc::Rc;
fn append3(s1: &Rc<String>, s2: &Rc<String>) -> Rc<String> {
    let mut result = (**s1).clone();
    result.push_str(s2);
    Rc::new(result)
}

接下来分析一下,对于下面的 main 函数代码,编译器会在哪一行报错?

fn main() {
    let s1: Box<Box<Rc<Rc<String>>>> = Box::new(Box::new(Rc::new(Rc::new(String::from("hello")))));
    let s2: Box<Box<Rc<Rc<String>>>> = Box::new(Box::new(Rc::new(Rc::new(String::from(", world")))));
    println!("{}", append(&s1, &s2));
    println!("{}", append2(&s1, &s2));
    println!("{}", append3(&s1, &s2));
}

这样呢,字符串的类型再扩展一下,编译器还会报错吗,在哪一行?

fn main() {
    let s1: Box<Box<Rc<Rc<Box<Box<String>>>>>> = Box::new(Box::new(Rc::new(Rc::new(Box::new(Box::new(String::from("hello")))))));
    let s2: Box<Box<Rc<Rc<Box<Box<String>>>>>> = Box::new(Box::new(Rc::new(Rc::new(Box::new(Box::new(String::from(", world")))))));
    println!("{}", append(&s1, &s2));
    println!("{}", append2(&s1, &s2));
    println!("{}", append3(&s1, &s2));
}

Rust 把这种语言特性叫做人体工学设计,为了减轻开发人员的负担。但是 Rust 在设计动不动会把变量给 move 掉、不得不使用 ' 单引号写法的时候,却放弃了人体工学,把内存安全放在了更重要的地位……倒是也没什么错,毕竟 Rust 只有内存安全是绝不能放松的。

最后再来个进阶难度的,假如在实际的业务场景中,有一个叫 do_something 的函数,接收泛型类型的参数,我们需要对这个函数基于原有逻辑做一些改动,原本的函数逻辑是这样:

fn do_something<T1, T2>(t1: T1, t2: T2) {
    println!("{}", append(&t1, &t2));
}

现在新增加一些处理:

fn do_something<T1, T2>(t1: T1, t2: T2) {
    // 增加一个函数来处理 t1
    handle_t1(&t1);  

    println!("{}", append(&t1, &t2));
}

那么问题来了,参数 t1 的类型是什么?handle_t1 函数的参数类型应该如何定义?在原有逻辑中,t1 作为参数对 append 函数进行了调用,是否意味着 t1 的类型是 &String?如果不是 &Stringt1 的类型可能是什么?