Rust中insert_str行为异常求助:终端UI标签插入位置不符
我想用Rust写一个带两个标签的终端UI,原本计划先绘制标签边框,再通过该行的字符长度减2来确定标签名称的插入位置,想要实现的效果是:
╭────────╮╭───────────╮ │ Lingon ││ Blueberry │
但执行减法操作时程序直接panic;去掉减法后,打印出的字符长度是4和14,但名称却被错误插入到了位置2和10;把Unicode字符\u{2502}换成小写字母l后,插入位置又恢复正常了。
简化后的代码如下:
fn main() { let tab_names = vec!["Lingon", "Blueberry"]; let win_cols = 100; let mut tab_rows = vec![String::with_capacity(win_cols); 2]; for name in tab_names { tab_rows[0].push_str("\u{256d}"); tab_rows[0].push_str(&std::iter::repeat("\u{2500}").take(name.chars().count()+2).collect::<String>()); tab_rows[0].push_str("\u{256e}"); tab_rows[1].push_str("\u{2502}"); tab_rows[1].push_str(&std::iter::repeat("\u{0020}").take(2).collect::<String>()); tab_rows[1].push_str("\u{2502}"); let name_placement = tab_rows[1].chars().count(); println!("{}", name_placement); tab_rows[1].insert_str(name_placement, name) } for i in 0..tab_rows.len() { println!("{}", tab_rows[i]); } }
我大概猜到了问题所在,但作为非专业开发者,希望有人能验证我的猜想。
问题原因
核心问题在于:Rust中String的insert_str()方法接收的是字节索引**,而不是字符数量或者终端显示的列宽**。
你用chars().count()得到的是字符串中的字符总数,但你使用的Unicode边框字符(比如\u{2502},也就是│)是UTF-8编码的多字节字符(占3个字节),而空格是单字节的ASCII字符。这就导致字符数和字节数不相等——你把字符数当成字节索引传给insert_str(),自然会出现插入位置错误,甚至索引越界导致panic。
比如第一次循环中,tab_rows[1]在插入名称前的内容是│ │:
- 字符数是4(│、空格、空格、│)
- 但字节数是3(第一个│) + 1*2(两个空格) +3(第二个│)=8
你传入的name_placement=4是字符数,但insert_str()把它当成字节索引,对应的位置是第二个空格的字节位置,最终导致名称插入到错误的地方。
而当你把\u{2502}换成单字节的l时,每个字符都只占1字节,字符数等于字节数,所以传入的索引刚好匹配,插入位置就正常了。
解决方法
有两种简单的修正方式:
1. 直接构建完整的标签行(推荐)
不需要事后插入,直接通过format!宏拼接出每个标签的完整行,这样更直观也不容易出错:
fn main() { let tab_names = vec!["Lingon", "Blueberry"]; let win_cols = 100; let mut tab_rows = vec![String::with_capacity(win_cols); 2]; for name in tab_names { // 构建顶部边框行 let top_border = format!("╭{}╮", std::iter::repeat("─").take(name.chars().count() + 2).collect::<String>()); tab_rows[0].push_str(&top_border); // 构建内容行,直接包含名称和边框 let content_line = format!("│ {} │", name); tab_rows[1].push_str(&content_line); } for row in tab_rows { println!("{}", row); } }
运行后就能得到你想要的效果:
╭────────╮╭───────────╮ │ Lingon ││ Blueberry │
2. 计算字符对应的字节索引(如果必须用插入方式)
如果你一定要用insert_str(),需要先找到目标字符位置对应的字节索引:
fn main() { let tab_names = vec!["Lingon", "Blueberry"]; let win_cols = 100; let mut tab_rows = vec![String::with_capacity(win_cols); 2]; for name in tab_names { tab_rows[0].push_str("\u{256d}"); tab_rows[0].push_str(&std::iter::repeat("\u{2500}").take(name.chars().count()+2).collect::<String>()); tab_rows[0].push_str("\u{256e}"); tab_rows[1].push_str("\u{2502}"); tab_rows[1].push_str(&std::iter::repeat("\u{0020}").take(2).collect::<String>()); tab_rows[1].push_str("\u{2502}"); // 找到第1个字符之后的字节索引(也就是│后面的位置) let name_placement = tab_rows[1].char_indices().nth(1).unwrap().0; tab_rows[1].insert_str(name_placement, name); } for row in tab_rows { println!("{}", row); } }
这里用char_indices()迭代器获取每个字符的字节索引,nth(1)就是第二个字符(第一个空格)对应的字节位置,也就是我们要插入名称的地方。
内容的提问来源于stack exchange,提问作者Eduardo Isla Venegas

