Rust返回str与&str时值类型大小的编译判定差异问题
str与&str的编译大小判定差异问题 问题背景
学习Rust语言数周,在查阅文档中借用机制相关示例时,遇到了编译行为差异问题。
可正常编译运行的示例代码
fn main() { let mut z:String = String::from("Hi there"); let copy = first_word(&z); z.clear(); } fn first_word(s:&String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[..i] } } &s[..] }
修改后无法编译的代码
将上述代码中first_word函数返回值类型前的&移除,把返回类型从&str改为str,对应代码如下:
fn main() { let mut z:String = String::from("Hi there"); let copy = first_word(&z); z.clear(); } fn first_word(s:&String) -> str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return s[..i] } } s[..] }
修改后的代码无法通过编译,编译器抛出错误:
编译期无法确定
str类型值的大小。
现有认知:&str返回的是借用引用,直接返回str属于所有权转移场景。核心疑问:为什么返回所有权值和返回借用引用两种场景,会在编译期值大小已知性判定上存在差异?
问题解答
首先明确Rust的硬性规则:所有在栈上存储、作为函数参数/返回值按值传递的类型,必须在编译期有固定的已知大小。Rust默认会给所有这类场景加上Sized约束,不满足要求的代码直接无法通过编译。
&str可以正常返回的核心原因:&str本质是字符串切片引用,属于固定大小的胖指针,结构非常明确:一个指向字符串数据起始地址的裸指针,一个记录字符串长度的整数。在64位系统上它总共占16字节,32位系统上占8字节,编译阶段完全能确定它的大小,自然可以作为返回值正常在栈上传递。它指向的实际字符串内容存在堆上或者程序静态内存段,返回的时候不需要拷贝整个字符串内容,只需要传递这个固定大小的胖指针即可,和是否转移所有权没有关系,本身大小固定就符合值传递的要求。- 直接返回
str编译报错的核心原因:str是典型的动态大小类型(DST),它的实际长度完全跟着字符串内容走:可能是1字节的单个字符,也可能是上万字节的长文本,编译阶段根本没法提前预判某一段str到底占多少字节,既没法给它在栈上分配对应大小的内存,也没法生成移动/拷贝返回值的汇编代码,自然过不了编译。
这里纠正一个常见认知偏差:不是“返回借用就不检查大小,返回所有权才检查”,而是只要是作为独立值在栈上传递,不管是转移所有权还是按值拷贝,都必须满足编译期大小已知的要求。没法直接返回str,和没法直接返回[i32](不带长度的数组切片)是完全一样的逻辑:这类类型本身大小不固定,根本没法作为独立的值在栈上传递。
所有权转移的前提是编译器知道要移动多少字节的数据,动态大小类型连需要移动的字节数都算不出来,自然不可能支持直接返回做所有权转移。如果确实要返回字符串的所有权,应该使用String类型:String内部是固定大小的结构(包含数据指针、长度、容量三个字段),持有堆上字符串数据的所有权,返回的时候只需要移动栈上的这个固定大小结构,就能把堆数据的所有权完整转移给接收方,完全符合编译期固定大小的要求。
内容的提问来源于stack exchange,提问作者Sentient Platypus

