Rust中解析多行字符串时,如何应用带行列索引的闭包?
问题背景
需要解析每行长度一致的多行字符串:将每个字符映射为u8类型的二维数组,同时记录字符串中标记为P的点的位置。
最初的实现思路是遍历整个字符串,用char_parse闭包处理每个字符:该闭包捕获point变量的可变引用,在映射字符为数值的同时提取P的点位。但传递行列索引给闭包时,编译器抛出了以下错误:
第一次编译错误
error: captured variable cannot escape `FnMut` closure body --> src/question.rs:40:17 | 24 | let mut char_parse = |(row, col), chr| match chr { | -------------- variable defined here ... 39 | .flat_map(|(row, line)| { | - inferred to be a `FnMut` closure 40 | / line.chars() 41 | | .enumerate() 42 | | .map(|(col, chr)| char_parse((row, col), chr)) | |_______________________________________----------_________________^ returns a reference to a captured variable which escapes the closure body | | | variable captured here | = note: `FnMut` closures only have access to their captured variables while they are executing... = note: ...therefore, they cannot allow references to captured variables to escape error[E0373]: closure may outlive the current function, but it borrows `row`, which is owned by the current function --> src/question.rs:42:26 | 42 | .map(|(col, chr)| char_parse((row, col), chr)) | ^^^^^^^^^^^^ --- `row` is borrowed here | | | may outlive borrowed value `row` | note: closure is returned here --> src/question.rs:40:17 | 40 | / line.chars() 41 | | .enumerate() 42 | | .map(|(col, chr)| char_parse((row, col), chr)) | |__________________________________________________________________^ help: to force the closure to take ownership of `row` (and any other referenced variables), use the `move` keyword | 42 | .map(move |(col, chr)| char_parse((row, col), chr)) | ++++ For more information about this error, try `rustc --explain E0373`.
添加move关键字后的编译错误
error[E0507]: cannot move out of `char_parse`, a captured variable in an `FnMut` closure --> src/question.rs:42:26 | 24 | let mut char_parse = |(row, col), chr| match chr { | -------------- captured outer variable ... 39 | .flat_map(|(row, line)| { | ------------- captured by this `FnMut` closure ... 42 | .map(move |(col, chr)| char_parse((row, col), chr)) | ^^^^^^^^^^^^^^^^^ ---------- | | | | | variable moved due to use in closure | | move occurs because `char_parse` has type `[closure@src/question.rs:24:30: 24:47]`, which does not implement the `Copy` trait | move out of `char_parse` occurs here For more information about this error, try `rustc --explain E0507`.
原始代码
use ndarray::{Array1, Array2}; use std::str::FromStr; #[derive(PartialEq, Eq, Copy, Clone, Debug)] struct Point2D { x: usize, y: usize, } #[derive(PartialEq, Debug)] struct Field { heights: Array2<u8>, point: Point2D, } impl FromStr for Field { type Err = String; fn from_str(s: &str) -> Result<Self, Self::Err> { let mut lines = s.lines().peekable(); let cols = lines.peek().ok_or("No input")?.len(); let mut point: Option<Point2D> = None; let mut char_parse = |(row, col), chr| match chr { c if c.is_ascii_lowercase() => Ok((c as u8) - 97), 'P' => match point { Some(_) => Err("Duplicate point"), None => { point = Some(Point2D { x: col, y: row }); Ok(0) } }, _ => Err("Unsupported symbol"), }; // This does not compile: let flat_heights: Array1<u8> = lines .enumerate() .flat_map(|(row, line)| { line.chars() .enumerate() .map(|(col, chr)| char_parse((row, col), chr)) }) .collect::<Result<_, _>>()?; let rows = flat_heights.len() / cols; let heights = flat_heights.into_shape((rows, cols)).map_err(|_| "Invalid shape")?; let point = point.ok_or("No point found")?; Ok(Field { heights, point }) } } #[cfg(test)] mod tests { use super::*; use ndarray::array; #[test] fn parse_heights() { let field: Result<Field, _> = "cab\nabP\nacc".parse(); assert_eq!( field, Ok(Field { heights: array![[2, 0, 1], [0, 1, 0], [0, 2, 2],], point: Point2D { x: 2, y: 1 }, }) ); } }
疑问点
- 如何解决这些编译错误?
- 这种实现方式是否符合Rust的惯用写法?
char_parse闭包为什么必须声明为mut?
解答
1. 解决编译错误
问题根源在于闭包的所有权与借用规则冲突:
- 添加
move关键字后,map闭包会尝试获取char_parse的所有权,但char_parse被外层flat_map的FnMut闭包捕获,FnMut仅允许可变借用捕获变量,无法转移所有权。 char_parse闭包修改了捕获的point变量,调用时需要可变借用自身,进一步加剧了所有权问题。
推荐方案:内联处理逻辑,避免嵌套闭包传递
直接在遍历流程中处理每个字符,无需单独定义char_parse闭包,彻底规避所有权问题:
use ndarray::{Array1, Array2}; use std::str::FromStr; #[derive(PartialEq, Eq, Copy, Clone, Debug)] struct Point2D { x: usize, y: usize, } #[derive(PartialEq, Debug)] struct Field { heights: Array2<u8>, point: Point2D, } impl FromStr for Field { type Err = String; fn from_str(s: &str) -> Result<Self, Self::Err> { let mut lines = s.lines().peekable(); let cols = lines.peek().ok_or("No input")?.len(); let mut point: Option<Point2D> = None; let flat_heights: Array1<u8> = lines .enumerate() .flat_map(|(row, line)| { line.chars() .enumerate() .map(move |(col, chr)| match chr { c if c.is_ascii_lowercase() => Ok((c as u8) - b'a'), 'P' => match point { Some(_) => Err("Duplicate point".to_string()), None => { point = Some(Point2D { x: col, y: row }); Ok(0) } }, _ => Err(format!("Unsupported symbol: {}", chr)), }) }) .collect::<Result<_, _>>()?; let rows = flat_heights.len() / cols; let heights = flat_heights.into_shape((rows, cols)).map_err(|_| "Invalid shape")?; let point = point.ok_or("No point found")?; Ok(Field { heights, point }) } } #[cfg(test)] mod tests { use super::*; use ndarray::array; #[test] fn parse_heights() { let field: Result<Field, _> = "cab\nabP\nacc".parse(); assert_eq!( field, Ok(Field { heights: array![[2, 0, 1], [0, 1, 0], [0, 2, 2],], point: Point2D { x: 2, y: 1 }, }) ); } }
备选方案:用RefCell共享可变状态(不推荐)
若必须保留char_parse闭包,可使用RefCell包装point,实现共享可变访问(会引入运行时借用检查):
use ndarray::{Array1, Array2}; use std::cell::RefCell; use std::str::FromStr; #[derive(PartialEq, Eq, Copy, Clone, Debug)] struct Point2D { x: usize, y: usize, } #[derive(PartialEq, Debug)] struct Field { heights: Array2<u8>, point: Point2D, } impl FromStr for Field { type Err = String; fn from_str(s: &str) -> Result<Self, Self::Err> { let mut lines = s.lines().peekable(); let cols = lines.peek().ok_or("No input")?.len(); let point = RefCell::new(None::<Point2D>); let char_parse = |(row, col), chr| match chr { c if c.is_ascii_lowercase() => Ok((c as u8) - b'a'), 'P' => { let mut p = point.borrow_mut(); match *p { Some(_) => Err("Duplicate point".to_string()), None => { *p = Some(Point2D { x: col, y: row }); Ok(0) } } } _ => Err(format!("Unsupported symbol: {}", chr)), }; let flat_heights: Array1<u8> = lines .enumerate() .flat_map(|(row, line)| { line.chars() .enumerate() .map(move |(col, chr)| char_parse((row, col), chr)) }) .collect::<Result<_, _>>()?; let rows = flat_heights.len() / cols; let heights = flat_heights.into_shape((rows, cols)).map_err(|_| "Invalid shape")?; let point = point.into_inner().ok_or("No point found")?; Ok(Field { heights, point }) } }
2. 是否符合Rust的惯用写法?
原始方案的核心思路没问题,但嵌套闭包捕获可变状态的方式不符合Rust惯用写法:
- Rust推崇直接、显式的状态处理,闭包嵌套捕获可变变量容易引发所有权混淆,降低代码可读性。
- 推荐方案将字符处理逻辑内联到遍历流程,或提取为普通函数(通过参数传递
&mut Option<Point2D>),更符合Rust的设计哲学。
3. 为什么char_parse闭包必须声明为mut?
因为char_parse闭包捕获了mut point,并且调用时会修改该捕获变量(遇到P时为point赋值)。在Rust中:
- 仅读取捕获变量的闭包,无需标记为
mut; - 会修改捕获变量的闭包,必须声明为
mut——因为闭包自身需要被可变借用,才能修改内部保存的捕获状态,这是Rust借用规则的强制要求。
内容的提问来源于stack exchange,提问作者xyl0o
相关产品推荐
相关产品推荐

