You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 10:15:27