ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Rust 错误

2022-08-06 21:04:36  阅读:217  来源: 互联网

标签:错误 list parse field let error line Rust


一个简单的需求:读入一个目录内的所有文本文件,每个文件的格式都是每一行定义一个二维点,例如x=1,y=2;获取所有点的一个列表。这里不使用serde或者现成的parser库,这么简单的格式直接手写就行了

 

没有错误处理

先来一个糙快猛的版本。其中用了一个nightly feature str_split_once,不过这不重要

#![feature(str_split_once)]

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Debug)]
struct Directory(Vec<(&'static str, &'static str)>);

fn parse_file(text: &str) -> Vec<Point> {
    let mut points = Vec::new();
    for line in text.lines() {
        let mut x = None;
        let mut y = None;
        for field in line.split(',') {
            let (k, v) = field.split_once('=').unwrap(); // todo
            match k.trim() {
                "x" => { x = Some(v.trim().parse().unwrap()); }, // todo
                "y" => { y = Some(v.trim().parse().unwrap()); }, // todo
                _ => {}, // todo
            }
        }

        points.push(Point { x: x.unwrap(), y: y.unwrap() }); // todo
    }

    points
}

fn main() {
    let d = Directory(vec![
        ("file1", "x=3,y=4\nx=1,y=2")
    ]);

    for (filename, text) in &d.0 {
        println!("{} {:?}", filename, parse_file(text));
    }
}

  

标准的错误处理

上面的代码里留下了5个todo。想成为一段健壮的程序,它们全部都要处理。这里使用了thiserror库,要换成snafu或者手写也是完全没问题的。代码行数一下就翻倍了,不过看起来是那么的正确

 

#![feature(str_split_once)]

use std::num::ParseIntError;

use thiserror::Error;
use fehler::{throws, throw};

use self::ParseFileError::*;

#[derive(Error, Debug)]
enum ParseFileError {
    #[error("no equal sign found: `{field}` in line `{line}`")]
    NoEqualSign { field: String, line: String },
    #[error("parse x as i32 failed: `{field}` in line `{line}`")]
    XParseFailed { source: ParseIntError, field: String, line: String },
    #[error("parse y as i32 failed: `{field}` in line `{line}`")]
    YParseFailed { source: ParseIntError, field: String, line: String },
    #[error("unknown key: `{field}` in line `{line}`")]
    UnknownField { field: String, line: String},
    #[error("x not found: `{line}`")]
    XNotFound { line: String },
    #[error("y not found: `{line}`")]
    YNotFound { line: String },
}


#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Debug)]
struct Directory(Vec<(&'static str, &'static str)>);

#[throws(ParseFileError)]
fn parse_file(text: &str) -> Vec<Point> {
    let mut points = Vec::new();
    for line in text.lines() {
        let mut x = None;
        let mut y = None;
        for field in line.split(',') {
            let (k, v) = field.split_once('=').ok_or_else(|| NoEqualSign {
                field: field.into(),
                line: line.into(),
            })?;
            match k.trim() {
                "x" => { x = Some(v.trim().parse().map_err(|e| {
                    XParseFailed {
                        source: e,
                        field: field.into(),
                        line: line.into(),
                    }
                })?); },
                "y" => { y = Some(v.trim().parse().map_err(|e| {
                    YParseFailed {
                        source: e,
                        field: field.into(),
                        line: line.into(),
                    }
                })?); },
                _ => {
                    throw!(UnknownField {
                        field: field.into(),
                        line: line.into(),
                    })
                },
            }
        }

        points.push(Point {
            x: x.ok_or_else(|| XNotFound { line: line.into() })?,
            y: y.ok_or_else(|| YNotFound { line: line.into() })?,
        });
    }

    points
}

fn main() {
    let d = Directory(vec![
        ("file1", "x=3,y=4\nx=1,y=2")
    ]);

    for (filename, text) in &d.0 {
        println!("{} {:?}", filename, parse_file(text));
    }
}

  

同时返回多个错误

需求方说,你的程序的错误报告做得非常好,不过如果能够同时报告所有错误,那就完美了。于是我们又给ParseFileError加上了List的variant:

#![feature(str_split_once)]

use std::num::ParseIntError;

use thiserror::Error;
use fehler::{throws, throw};

use self::ParseFileError::*;

#[derive(Error, Debug)]
enum ParseFileError {
    #[error("no equal sign found: `{field}` in line `{line}`")]
    NoEqualSign { field: String, line: String },
    #[error("parse x as i32 failed: `{field}` in line `{line}`")]
    XParseFailed { source: ParseIntError, field: String, line: String },
    #[error("parse y as i32 failed: `{field}` in line `{line}`")]
    YParseFailed { source: ParseIntError, field: String, line: String },
    #[error("unknown key: `{field}` in line `{line}`")]
    UnknownField { field: String, line: String},
    #[error("x not found: `{line}`")]
    XNotFound { line: String },
    #[error("y not found: `{line}`")]
    YNotFound { line: String },
    #[error("multiple errors")]
    List(Vec<ParseFileError>),
}


#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Debug)]
struct Directory(Vec<(&'static str, &'static str)>);

#[throws(ParseFileError)]
fn parse_file(text: &str) -> Vec<Point> {
    let mut points = Vec::new();
    let mut error_list = Vec::new();
    for line in text.lines() {
        let mut line_error_list = Vec::new();
        let mut x = None;
        let mut y = None;
        for field in line.split(',') {
            let (k, v) = match field.split_once('=') {
                Some((k, v)) => (k, v),
                None => {
                    line_error_list.push(NoEqualSign {
                        field: field.into(),
                        line: line.into(),
                    });
                    continue; // 后面的解析都没有意义了,直接continue。如果parse_line是一个单独的函数,这里相当于无视收集错误可以直接用?返回。懒得展示了……
                }
            };
            match k.trim() {
                "x" => {
                    // 写起来会很麻烦……
                    match v.trim().parse() {
                        Ok(xx) => x = Some(xx),
                        Err(e) => {
                            line_error_list.push(XParseFailed {
                                source: e,
                                field: field.into(),
                                line: line.into(),
                            });
                        },
                    }
                },
                "y" => {
                    // 或者可以这样“简洁”地写
                    y = v.trim().parse().map_err(|e| {
                        line_error_list.push(YParseFailed {
                            source: e,
                            field: field.into(),
                            line: line.into(),
                        });
                        () // 可省略
                    }).ok();
                },
                _ => {
                    line_error_list.push(UnknownField {
                        field: field.into(),
                        line: line.into(),
                    });
                },
            }
        }

        // 可能需求会是如果此行没有找到等号,就不用报告找不到x/y的错误了
        // 或者如果解析x时出错,也不用报告找不到x的错误
        // 这里就不处理那么多了……
        if x.is_none() {
            line_error_list.push(XNotFound {
                line: line.into(),
            });
        }
        if y.is_none() {
            line_error_list.push(YNotFound {
                line: line.into(),
            });
        }

        if line_error_list.is_empty() {
            points.push(Point {
                x: x.unwrap(),
                y: y.unwrap(),
            });
        } else {
            error_list.extend(line_error_list);
        }
    }

    if error_list.is_empty() {
        points
    } else {
        throw!(List(error_list));
    }
}

fn main() {
    let d = Directory(vec![
        ("file1", "x=3,y=4t\nx=1,y=2,z=3")
    ]);

    for (filename, text) in &d.0 {
        println!("{} {:?}", filename, parse_file(text));
    }
}

  

 

支持警告

甲方爸爸又来了,先是惯例地夸赞了一番,然后说,某些数据里除了x、y之外,还可能有z、w等等字段,它们直接忽略即可,就不用报错了。不过呢,如果能够输出一句警告,说明哪个文件的哪一行有这些字段,那就能让这个完美的工具更完美了。

 

@@ -3,7 +3,7 @@
 use std::num::ParseIntError;

 use thiserror::Error;
-use fehler::{throws, throw};
+use w_result::WResult;

 use self::ParseFileError::*;

@@ -35,10 +35,10 @@ struct Point {
 #[derive(Debug)]
 struct Directory(Vec<(&'static str, &'static str)>);

-#[throws(ParseFileError)]
-fn parse_file(text: &str) -> Vec<Point> {
+fn parse_file(text: &str) -> WResult<Vec<Point>, ParseFileError, ParseFileError> {
        let mut points = Vec::new();
        let mut error_list = Vec::new();
+       let mut warning_list = Vec::new();
        for line in text.lines() {
                let mut line_error_list = Vec::new();
                let mut x = None;
@@ -80,7 +80,7 @@ fn parse_file(text: &str) -> Vec<Point> {
                                        }).ok();
                                },
                                _ => {
-                                       line_error_list.push(UnknownField {
+                                       warning_list.push(UnknownField {
                                                field: field.into(),
                                                line: line.into(),
                                        });
@@ -113,18 +113,18 @@ fn parse_file(text: &str) -> Vec<Point> {
        }

        if error_list.is_empty() {
-               points
+               WResult::WOk(points, warning_list)
        } else {
-               throw!(List(error_list));
+               WResult::WErr(List(error_list))
        }
 }

  

 

这次的改动比较小,主要就是引入WResult。继放弃?之后,fehler也被放弃了。

MyResult

现在的代码已经非常别扭了。如果一开始就知道有这些需求,应该怎么设计呢?我们已经用过了标准库里的Result,第三方的WResult,试试自己定义的MyResult。下面是经过了几次整理的最终代码,自我感觉思路应该是对了。命名等比较随意就不要追究了。

 

#![feature(str_split_once)]
#![feature(try_trait)]

#[macro_use] extern crate shrinkwraprs;

use std::num::ParseIntError;
use std::fmt::{self, Debug};
use std::error::Error as _;
use std::ops::Try;

use thiserror::Error;


use self::ParseLineError::*;
use self::MyResult::{MyOk, MyErr};
use self::MyError::{Collected, Fatal};

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Debug)]
struct Directory(Vec<(&'static str, &'static str)>);

#[derive(Debug, Shrinkwrap)]
#[shrinkwrap(mutable)]
struct Warnings<E>(Vec<E>);

impl<E> Warnings<E> {
    fn new() -> Self {
        Self(Vec::new())
    }
}

impl<E: Debug> From<Warnings<E>> for MyError<E> {
    fn from(warn: Warnings<E>) -> Self {
        Collected(warn.0)
    }
}

#[derive(Debug, Error)]
enum MyError<E: Debug> {
    Collected(Vec<E>),
    Fatal(E),
}

impl<E: Debug> fmt::Display for MyError<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

#[must_use]
#[derive(Debug)]
enum MyResult<T, E: Debug> {
    MyOk {
        ok: T,
        warn: Warnings<E>,
    },
    MyErr(MyError<E>),
}

impl<T, E: Debug> Try for MyResult<T, E> {
    type Ok = T;
    type Error = E;

    fn into_result(self) -> Result<T, E> {
        unimplemented!()
    }

    fn from_ok(v: T) -> Self {
        MyOk {
            ok: v,
            warn: Warnings::new(),
        }
    }

    fn from_error(v: E) -> Self {
        MyErr(Fatal(v))
    }
}

impl<T, E: Debug> MyResult<T, E> {
    fn drain_warning(self, f: &dyn Fn(E)) -> MyResult<T, E> {
        match self {
            MyOk {ok, mut warn} => {
                for w in warn.drain(..) {
                    f(w);
                }
                MyOk {
                    ok,
                    warn: Warnings::new(),
                }
            },
            MyErr(e) => MyErr(e),
        }
    }
}

#[derive(Error, Debug)]
enum ParseLineError {
    #[error("no equal sign found: `{field}`")]
    NoEqualSign { field: String },
    #[error("parse x as i32 failed: `{field}`")]
    XParseFailed { source: ParseIntError, field: String },
    #[error("parse y as i32 failed: `{field}`")]
    YParseFailed { source: ParseIntError, field: String },
    #[error("unknown key: `{field}`")]
    UnknownField { field: String},
    #[error("x not found")]
    XNotFound,
    #[error("y not found")]
    YNotFound,
}

fn ok_wrapping<T, E: Debug>(ok: impl FnOnce() -> T, warning_list: Warnings<E>, error_list: Vec<E>) -> MyResult<T, E> {
    if error_list.is_empty() {
        MyOk {
            ok: ok(),
            warn: warning_list,
        }
    } else {
        MyErr(Collected(error_list))
    }
}

fn parse_line(line: &str) -> MyResult<Point, ParseLineError> {
    let mut warning_list = Warnings::new();
    let mut error_list = Vec::new();
    let mut x = None;
    let mut y = None;
    for field in line.split(',') {
        let (k, v) = match field.split_once('=') {
            Some((k, v)) => (k, v),
            None => {
                return MyErr(Fatal(NoEqualSign {
                    field: field.into(),
                })); // 为MyResult实现了Try,也可以使用?:Err(NoEqualSign { field: field.into(), })?
            }
        };
        match k.trim() {
            "x" => {
                // 写起来会很麻烦……
                match v.trim().parse() {
                    Ok(xx) => x = Some(xx),
                    Err(e) => {
                        error_list.push(XParseFailed {
                            source: e,
                            field: field.into(),
                        });
                    },
                }
            },
            "y" => {
                // 或者可以这样“简洁”地写
                y = v.trim().parse().map_err(|e| {
                    error_list.push(YParseFailed {
                        source: e,
                        field: field.into(),
                    });
                    () // 可省略
                }).ok();
            },
            _ => {
                warning_list.push(UnknownField {
                    field: field.into(),
                });
            },
        }
    }

    // 可能需求会是如果此行没有找到等号,就不用报告找不到x/y的错误了
    // 或者如果解析x时出错,也不用报告找不到x的错误
    // 这里就不处理那么多了……
    if x.is_none() {
        error_list.push(XNotFound);
    }
    if y.is_none() {
        error_list.push(YNotFound);
    }

    ok_wrapping(|| Point {
        x: x.unwrap(),
        y: y.unwrap(),
    }, warning_list, error_list)
}

#[derive(Error, Debug)]
enum ParseFileError {
    #[error("parse line failed: `{line}`")]
    Line {line: String, source: MyError<ParseLineError>},
}

fn parse_file(text: &str) -> MyResult<Vec<Point>, ParseFileError> {
    let mut points = Vec::new();
    let mut warning_list = Warnings::new();
    let mut error_list = Vec::new();
    for line in text.lines() {
        match parse_line(line) {
            MyOk { ok, warn } => {
                points.push(ok);
                if !warn.is_empty() {
                    warning_list.push(ParseFileError::Line {
                        line: line.into(),
                        source: warn.into(),
                    });
                }
            },
            MyErr(e) => {
                error_list.push(ParseFileError::Line {
                    line: line.into(),
                    source: e,
                });
            }
        }
    }

    ok_wrapping(|| points, warning_list, error_list)
}

#[derive(Error, Debug)]
enum ParseDirectoryError {
    #[error("parse file failed: `{file}`")]
    File {file: String, source: MyError<ParseFileError>},
}

fn parse_directory(directory: &Directory) -> MyResult<Vec<Point>, ParseDirectoryError> {
    let mut points = Vec::new();
    let mut warning_list = Warnings::new();
    let mut error_list = Vec::new();

    for (filename, text) in &directory.0 {
        match parse_file(text) {
            MyOk { ok, warn } => {
                points.extend(ok);
                if !warn.is_empty() {
                    warning_list.push(ParseDirectoryError::File {
                        file: (*filename).into(),
                        source: warn.into(),
                    });
                }
            },
            MyErr(e) => {
                error_list.push(ParseDirectoryError::File {
                    file: (*filename).into(),
                    source: e,
                });
            }
        }
    }

    ok_wrapping(|| points, warning_list, error_list)
}

fn main() {
    let d = Directory(vec![
        ("file1", "x=3,y=4\nx=1,y=2,z=3")
    ]);

    let r = parse_directory(&d).drain_warning(&|e| {
        println!("{}", e);
        if let Some(s) = e.source() {
            println!("caused by: {}", s);
        }
    });

    println!("{:?}", r);
}

  

 

  • 确实每个函数都应该有一个错误类型,甚至可以有多个,例如ParseLineError可以再抽一个ParseFieldError出来,反复出现的field字段就是征兆
  • 错误的来源可以是多个,最终是树状结构。正如rfc2965里说的那样(RustConf 2020的报告里也提到了):(Pain points) (TheErrortrait) can only represent singly-linked lists for chains of errors。现在的状态Error Reporter并不能太好地工作
  • 函数独立地决定某个错误是Error还是Warning,但调用者可以把自己对应的错误放到自己的error_list或warning_list里,甚至直接丢弃。例如需求可能是,file1可以有xy之外的字段不用管,其它文件则需要警告。比起把额外的信息传入parse_line函数,在parse_directory里处理UnknownField会更清晰
  • Warning的类型和Error一致,不要再单独弄个枚举。如果参考log的术语,似乎还应该有info/debug/trace等等,但我暂时认为它们不需要返回给调用者
  • 如果Rust支持匿名枚举,也许不需要特意定义MyError了

错误处理真是太累了……总之暂时先用这个思路试试看效果如何。当然绝大部分情况下我肯定都会偷懒,但至少需要返回错误列表时我会统一使用MyResult而不是一会Vec<Result<()>>一会Vec<Error>了

 

转自:

作者: 

juu wiio  https://zhuanlan.zhihu.com/p/234820782

 

标签:错误,list,parse,field,let,error,line,Rust
来源: https://www.cnblogs.com/pythonClub/p/16557863.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有