为什么在Rust中按值返回是惯用写法(而非使用输出参数)?
作为有C经验的开发者,刚接触Rust时对「大对象直接按值返回」感到困惑太正常了——毕竟在C里,我们早就习惯了用输出参数来避免昂贵的拷贝,生怕编译器没做RVO导致性能损失。但Rust的设计和生态选择这种写法,背后是几个关键的原因,既涉及语言特性,也关乎代码的可读性和长期可维护性:
1. Rust的所有权与移动语义让「返回值」的开销几乎可以忽略
C++里的RVO是一种可选优化,编译器不保证一定会做(虽然现在主流编译器大多会支持,但标准里没强制要求),所以大家不敢放心依赖。但Rust不一样:
- 首先,Rust的移动语义是语言原生特性:对于像
String、自定义struct这类类型,「返回值」的本质是所有权转移,而不是深拷贝。比如你例子里的Config,返回它的时候只是把query和file_path的所有权从函数里转移到调用者,没有复制任何字符串数据——这操作的成本和传递一个指针差不多,完全没有性能负担。 - 其次,Rust的编译器对返回值优化(RVO/NRVO)的支持更可靠,而且因为有借用检查器的存在,编译器能更确定变量的生命周期和别名情况,更容易触发优化。很多时候,编译器会直接把对象构造在调用者的栈空间里,连移动的步骤都省了,和你用输出参数手动做的优化效果完全一样。
2. 按值返回的代码更符合直觉,可读性拉满
从代码语义上看,一个像Config::new这样的构造函数,它的核心目的就是生成一个新的Config实例。直接返回这个实例的写法:
let config = Config::new(&args);
比用输出参数的写法:
let mut config = Config::default(); // 还得先搞个默认值占位 Config::new(&args, &mut config);
要清晰太多。你不用再跟踪「这个函数会不会修改我传入的引用?」「我得先初始化一个变量传进去?」这类问题,函数的签名直接告诉了你它的作用——输入参数,返回新对象。这种写法的心智负担小很多,团队协作时也不容易出错。
3. Rust的设计鼓励「最小化可变性」,输出参数反而违背这一点
Rust非常强调最小化可变状态,因为可变状态是很多bug的来源。输出参数需要你传入一个可变引用,这意味着函数内部可以修改这个变量的状态,而调用者的代码里多了一个可变变量,这会让代码的可预测性下降。
而按值返回的写法,你得到的是一个不可变的变量(除非你显式加mut),这更符合Rust的设计哲学:默认不可变,需要修改时再显式声明。
4. 输出参数在Rust里反而可能更低效
你可能觉得「手动用输出参数更高效」,但在Rust里这往往是反直觉的:
- Rust不允许未初始化的变量,所以你必须先创建一个合法的实例(比如用
default()或者空值)才能传给函数,这一步可能会有额外的初始化开销; - 输出参数需要可变引用,这会引入借用检查的额外约束,比如你不能同时有多个可变引用指向这个变量,反而可能限制代码的灵活性。
总结
回到你的问题——为什么Rust idiom是按值返回?因为Rust的语言特性(所有权、移动语义、严格的借用检查)让这种写法既安全又高效,同时还能让代码更清晰、更符合直觉。编译器的优化不是「碰运气」,而是有语言规则做保障的,你完全可以放心依赖它。
内容来源于stack exchange

