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_strs.push_str(&s2)原地追加,不转移所有权,最直观
format!format!("{}{}", s1, s2)不消耗任何输入,返回新String,可读性最好
joinvec.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信息与输出结果,形成肌肉记忆。