4. 切片与字符串面试题精讲
在Rust面试中,字符串与切片相关的问题几乎必考。它们既是语言设计的核心特色,也是区分候选人是否真正理解所有权、借用与内存布局的试金石。本章围绕四个高频考点展开:String与&str的本质区别、切片越界与UTF-8边界、字符串拼接与格式化、字节与字符操作。每一部分都配有可直接运行的代码推演,帮助你在面试中既能说清原理,又能写出正确代码。
一、String与&str:所有权视角下的本质区别
面试官常问:“String和&str有什么区别?”标准回答应从三个维度展开:所有权、内存布局、可变性。
String是拥有所有权的、可增长的、堆分配的UTF-8字符串,内部由ptr、len、capacity三个字段组成(24字节,64位平台)。&str是字符串切片,是对某段UTF-8字节序列的借用,内部是ptr和len组成的胖指针(16字节),不拥有数据,因此不可变(除非配合&mut str且仅限ASCII安全操作)。
let s: String = String::from("hello");
let slice: &str = &s[..]; // 借用整个字符串
let literal: &'static str = "hi"; // 字面量本身即&str
println!("String size: {}", std::mem::size_of::<String>()); // 24
println!("&str size: {}", std::mem::size_of::<&str>()); // 16
面试延伸问题:“为什么函数参数推荐用&str而不是&String?”因为&str可以接受String的借用、字面量、以及其它&str,而&String只能接受String的引用,灵活性差。这体现了Rust的Deref强制转换:&String可自动转为&str,反之不行。
二、切片越界与UTF-8边界:面试中最容易踩的坑
字符串切片语法&s[a..b]中的a、b是字节索引,不是字符索引。这是面试高频陷阱。若索引落在UTF-8字符的中间字节,程序会panic。
let s = "你好世界";
// 每个汉字占3字节,总长12字节
println!("{}", &s[0..3]); // 输出"你",合法
// println!("{}", &s[0..1]); // panic: byte index 1 is not a char boundary
面试中常要求写出“安全截取前N个字符”的函数。正确做法是使用char_indices或chars:
fn take_chars(s: &str, n: usize) -> &str {
match s.char_indices().nth(n) {
Some((idx, _)) => &s[..idx],
None => s,
}
}
fn main() {
let s = "你好世界";
println!("{}", take_chars(s, 2)); // 输出"你好"
}
判断依据:char_indices()返回每个字符的起始字节位置,nth(n)取第n个字符的起始索引,用它作为切片终点必然落在字符边界上。若n超过字符总数,返回整个字符串。这个函数在面试中能体现你对UTF-8边界的敏感度。
三、字符串拼接与格式化:性能与可读性的权衡
Rust提供多种拼接方式,面试官常问“哪种最高效”。常见方式及适用场景如下:
| 方式 | 示例 | 特点 |
|---|---|---|
+运算符 | s1 + &s2 | 消耗s1所有权,可能复用其缓冲区,适合链式拼接 |
push_str | s.push_str(&s2) | 原地追加,不转移所有权,最直观 |
format! | format!("{}{}", s1, s2) | 不消耗任何输入,返回新String,可读性最好 |
join | vec.join(",") | 适合集合拼接,带分隔符 |
let mut s = String::from("Hello");
s.push_str(", ");
s.push_str("world");
let s2 = format!("{}!", s);
println!("{}", s2); // Hello, world!
// + 运算符示例
let a = String::from("foo");
let b = String::from("bar");
let c = a + &b; // a被移动,b仍可用
// println!("{}", a); // 编译错误
println!("{}", c); // foobar
面试要点:+运算符实际调用add(self, other: &str),会消耗左操作数。若在循环中用+拼接,每次都可能重新分配,性能差;此时应使用push_str或format!。而format!虽然可读性好,但每次都会分配新String,在热路径中需谨慎。
四、字节与字符操作:chars、bytes、char_indices的选择
面试官常给出一道题:“统计字符串中每个字符出现的次数”或“反转字符串”。这里的关键是区分字节与字符。
use std::collections::HashMap;
fn count_chars(s: &str) -> HashMap<char, usize> {
let mut map = HashMap::new();
for ch in s.chars() {
*map.entry(ch).or_insert(0) += 1;
}
map
}
fn reverse_chars(s: &str) -> String {
s.chars().rev().collect()
}
fn main() {
let s = "a你b好a";
let counts = count_chars(s);
println!("{:?}", counts); // {'a': 2, '你': 1, 'b': 1, '好': 1}
println!("{}", reverse_chars(s)); // 好a b你a 实际输出"好b你a"
}
注意:s.bytes()返回u8迭代器,适合处理ASCII或二进制数据;s.chars()返回char迭代器,按Unicode标量值遍历;s.char_indices()同时给出字节位置和字符。若用bytes()反转,会破坏UTF-8编码,得到乱码。面试中若被要求“反转字符串”,务必确认是按字符还是按字节。
另一个高频题:“如何判断一个字符串是否为回文?”正确做法是使用chars()收集后双指针比较,或直接s.chars().eq(s.chars().rev())。后者简洁但会遍历两次,前者更高效。
fn is_palindrome(s: &str) -> bool {
let chars: Vec<char> = s.chars().collect();
let (mut i, mut j) = (0, chars.len().saturating_sub(1));
while i < j {
if chars[i] != chars[j] { return false; }
i += 1; j -= 1;
}
true
}
五、面试实战推演:综合案例
假设面试官给出任务:“实现一个函数,将字符串中每个单词首字母大写,其余小写,单词以空格分隔,返回新String。”这题综合考察切片、字符操作与拼接。
fn capitalize_words(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut new_word = true;
for ch in s.chars() {
if ch.is_whitespace() {
new_word = true;
result.push(ch);
} else if new_word {
result.extend(ch.to_uppercase());
new_word = false;
} else {
result.extend(ch.to_lowercase());
}
}
result
}
fn main() {
let s = "hello 世界 rust";
println!("{}", capitalize_words(s)); // Hello 世界 Rust
}
推演要点:使用with_capacity预分配,避免多次扩容;to_uppercase可能返回多个字符(如德语ß→SS),因此用extend而非push;new_word标志位处理空格后的首字母。这个案例在面试中能同时展示性能意识与Unicode正确性。
六、高频面试题速查清单
- String与&str区别:所有权、内存布局、可变性、Deref转换。
- 切片越界:字节索引,非字符索引;用
char_indices安全截取。 - UTF-8边界:汉字3字节,emoji 4字节;
&s[0..1]可能panic。 - 拼接性能:循环中用
push_str,少量用format!,+会移动左值。 - 字符操作:
chars()按字符,bytes()按字节,char_indices()两者兼顾。 - 反转字符串:必须用
chars().rev(),不能用bytes().rev()。
掌握以上内容,你在面试中面对字符串与切片问题时,既能准确回答概念,又能写出符合Rust所有权与UTF-8规范的代码。建议在本地用cargo new创建练习项目,将上述代码逐一运行,观察panic信息与输出结果,形成肌肉记忆。