Rust中类型转换在错误处理中的应用小结

 更新时间:2023年09月26日 11:39:20   作者:明天好,会的  
随着项目的进展,关于Rust的故事又翻开了新的一页,今天来到了服务器端的开发场景,发现错误处理中的错误类型转换有必要分享一下,对Rust错误处理相关知识感兴趣的朋友一起看看吧

随着项目的进展,关于Rust的故事又翻开了新的一页,今天来到了服务器端的开发场景,发现错误处理中的错误类型转换有必要分享一下。

Rust抽象出来了Result<T,E>,T是返回值的类型,E是错误类型。只要函数的返回值的类型被定义为Resut<T,E>,那么作为开发人员就有责任来处理调用这个函数可能发生的错误。通过Result<T,E>,Rust其实给开发人员指明了一条错误处理的道路,使代码更加健壮。

场景

  • 服务器端处理api请求的框架:Rocket
  • 服务器端处理数据持久化的框架:tokio_postgres

在api请求的框架中,我把返回类型定义成了 Result<T, rocket::response::status::Custom\<String>> ,即错误类型是 rocket::response::status::Custom\<String>

在tokio_postgres中,直接使用 tokio_postgres::error::Error

即如果要处理错误,就必须将 tokio_postgres::error::Error 转换成 rocket::response::status::Custom\<String> 。那么我们从下面的原理开始,逐一领略Rust的错误处理方式,通过对比找到最合适的方式吧。

原理

对错误的处理,Rust有3种可选的方式

  • 使用match
  • 使用if let
  • 使用map_err

下面我结合场景,逐一演示各种方式是如何处理错误的。

下面的代码中涉及到2个模块(文件)。 /src/routes/notes.rs 是路由层,负责将api请求导向合适的service。 /src/services/note_books.rs 是service层,负责业务逻辑和数据持久化的处理。这里的逻辑也很简单,就是route层调用service层,将数据写入到数据库中。

使用match

src/routes/notes.rs

#[post("/api/notes", format = "application/json", data = "<note>")]
pub async fn post_notes(note: Json<Note>) -> Result<(), rocket::response::status::Custom<String>> {
    insert_or_update_note(&note.into_inner()).await
}

/src/services/note_book.rs

pub async fn insert_or_update_note(
    note: &Note,
) -> Result<(), rocket::response::status::Custom<String>> {
    let (client, connection) = match connect(
        "host=localhost dbname=notes_db user=postgres port=5432",
        NoTls,
    )
    .await
    {
        Ok(res) => res,
        Err(err) => {
            return Err(rocket::response::status::Custom(
                rocket::http::Status::ExpectationFailed,
                format!("{}", err),
            ));
        }
    };
    ...
    match client
        .execute(
            "insert into notes (id, title, content) values($1, $2, $3);",
            &[&get_system_seconds(), &note.title, &note.content],
        )
        .await
    {
        Ok(res) => Ok(()),
        Err(err) => Err(rocket::response::status::Custom(
            rocket::http::Status::ExpectationFailed,
            format!("{}", err),
        )),
    }
}

通过上面的代码我们可以读出一下内容:

  • 在service层定义了route层相同的错误类型
  • 在service层将持久层的错误转换成了route层的错误类型
  • 使用match的代码量还是比较大

使用if let

/src/services/note_book.rs

pub async fn insert_or_update_note(
    note: &Note,
) -> Result<(), rocket::response::status::Custom<String>> {
    if let Ok((client, connection)) = connect(
        "host=localhost dbname=notes_db user=postgres port=5432",
        NoTls,
    )
    .await
    {
        ...
        if let Ok(res) = client
            .execute(
                "insert into notes (id, title, content) values($1, $2, $3);",
                &[&get_system_seconds(), &note.title, &note.content],
            )
            .await
        {
            Ok(())
        } else {
            Err(rocket::response::status::Custom(
                rocket::http::Status::ExpectationFailed,
                format!("{}", "unknown error"),
            ))
        }
    } else {
        Err(rocket::response::status::Custom(
            rocket::http::Status::ExpectationFailed,
            format!("{}", "unknown error"),
        ))
    }
}

src/routes/notes.rs

#[post("/api/notes", format = "application/json", data = "<note>")]
pub async fn post_notes(note: Json<Note>) -> Result<(), rocket::response::status::Custom<String>> {
    insert_or_update_note(&note.into_inner()).await
}

使用了 if let ... ,代码更加的别扭,并且在else分支中,拿不到具体的错误信息。

其实,不难看出,我们的目标是将api的请求,经过route层和service层,将数据写入到数据中。但这其中的错误处理代码的干扰就特别大,甚至要有逻辑嵌套现象。这种代码的已经离初衷比较远了,是否有更加简洁的方式,使代码能够最大限度的还原逻辑本身,把错误处理的噪音降到最低呢?答案肯定是有的。那就是map_err

map_err

map_err是Result上的一个方法,专门用于错误的转换。下面的代码经过了map_err的改写,看上去是不是清爽了不少啊。/src/services/note_book.rs

pub async fn insert_or_update_note(
    note: &Note,
) -> Result<(), rocket::response::status::Custom<String>> {
    let (client, connection) = connect(
        "host=localhost dbname=notes_db user=postgres port=5432",
        NoTls,
    )
    .await
    .map_err(|err| {
        rocket::response::status::Custom(
            rocket::http::Status::ExpectationFailed,
            format!("{}", err),
        )
    })?;
    ...
    let _ = client
        .execute(
            "insert into notes (id, title, content) values($1, $2, $3);",
            &[&get_system_seconds(), &note.title, &note.content],
        )
        .await
        .map_err(|err| {
            rocket::response::status::Custom(
                rocket::http::Status::ExpectationFailed,
                format!("{}", err),
            )
        })?;
    Ok(())
}

src/routes/notes.rs

#[post("/api/notes", format = "application/json", data = "<note>")]
pub async fn post_notes(note: Json<Note>) -> Result<(), rocket::response::status::Custom<String>> {
    insert_or_update_note(&note.into_inner()).await
}

经过map_err改写后的代码,代码的逻辑流程基本上还原了逻辑本身,但是map_err要额外占4行代码,且错误对象的初始化代码存在重复。在实际的工程项目中,service层的处理函数可能是成百上千,如果再乘以4,那多出来的代码量也不少啊,这会给后期的维护带来不小的压力。

那是否还有改进的空间呢?答案是Yes。

Rust为我们提供了From<T> trait,用于类型转换。它定义了从一种类型T到另一种类型Self的转换方法。我觉得这是Rust语言设计亮点之一。

但是,Rust有一个显示,即实现From<T> trait的结构,必须有一个在当前的crate中,也就是说我们不能直接通过From<T>来实现从 tokio_postgres::error::Error rocket::response::status::Custom<String> 。也就是说下面的代码编译器会报错。

impl From<tokio_postgres::Error> for rocket::response::status::Custom<String> {}

报错如下:

32 | impl From<tokio_postgres::Error> for rocket::response::status::Custom<String> {}
   | ^^^^^---------------------------^^^^^----------------------------------------
   | |    |                               |
   | |    |                               `rocket::response::status::Custom` is not defined in the current crate
   | |    `tokio_postgres::Error` is not defined in the current crate
   | impl doesn't use only types from inside the current crate

因此,我们要定义一个类型 MyError 作为中间类型来转换一下。/src/models.rs

pub struct MyError {
    pub message: String,
}
impl From<tokio_postgres::Error> for MyError {
    fn from(err: Error) -> Self {
        Self {
            message: format!("{}", err),
        }
    }
}
impl From<MyError> for rocket::response::status::Custom<String> {
    fn from(val: MyError) -> Self {
        status::Custom(Status::ExpectationFailed, val.message)
    }
}

/src/services/note_book.rs

pub async fn insert_or_update_note(
    note: &Note,
) -> Result<(), rocket::response::status::Custom<String>> {
    let (client, connection) = connect(
        "host=localhost dbname=notes_db user=postgres port=5432",
        NoTls,
    )
    .await
    .map_err(MyError::from)?;
    ...
    let _ = client
        .execute(
            "insert into notes (id, title, content) values($1, $2, $3);",
            &[&get_system_seconds(), &note.title, &note.content],
        )
        .await
        .map_err(MyError::from)?;
    Ok(())
}

src/routes/notes.rs

#[post("/api/notes", format = "application/json", data = "<note>")]
pub async fn post_notes(note: Json<Note>) -> Result<(), rocket::response::status::Custom<String>> {
    insert_or_update_note(&note.into_inner()).await
}

MyError rocket::response::status::Custom<String> 之间的转换是隐式的,由编译器来完成。因此我们的错误类型的转换最终缩短为 map_err(|err|MyError::from(err)) ,再简写为 map_err(MyError::from)

关于错误处理中的类型转换应用解析就到这里。通过分析这个过程,我们可以看到,在设计模块时,我们应该确定一种错误类型,就像tokio_postgres库一样,只暴露了tokio_postgress::error::Error一种错误类型。这种设计既方便我们在设计模块时处理错误转换,也方便其我们的模块在被调用时,其它代码进行错误处理。

到此这篇关于Rust中类型转换在错误处理中的应用解析的文章就介绍到这了,更多相关Rust错误处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Rust遍历 BinaryHeap的示例代码

    Rust遍历 BinaryHeap的示例代码

    Rust 的 BinaryHeap 结构体实现了迭代器接口,因此你可以遍历它,如果你想要遍历 BinaryHeap 中的所有元素,你可以使用 .into_iter() 方法将其转换为迭代器,并遍历其中的元素,本文通过实例介绍Rust遍历 BinaryHeap的相关知识,感兴趣的朋友一起看看吧
    2024-04-04
  • Rust语言中的String和HashMap使用示例详解

    Rust语言中的String和HashMap使用示例详解

    这篇文章主要介绍了Rust语言中的String和HashMap使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • 使用systemd部署r-nacos的操作方法

    使用systemd部署r-nacos的操作方法

    r-nacos是一个用rust实现的nacos服务,我们用它平替java nacos以降低服务占用内存,提升服务的稳定性,这篇文章主要介绍了使用systemd部署r-nacos,需要的朋友可以参考下
    2024-03-03
  • 一步到位,教你如何在Windows成功安装Rust

    一步到位,教你如何在Windows成功安装Rust

    一步到位:轻松学会在Windows上安装Rust!想快速掌握Rust编程语言?别再为复杂教程头疼!这份指南将手把手带你顺利完成Windows平台上的Rust安装全过程,从此编码之旅更加顺畅无阻,立即阅读,开始你的Rust编程旅程吧!
    2024-01-01
  • Rust 累计时间长度的操作方法

    Rust 累计时间长度的操作方法

    在Rust中,如果你想要记录累计时间,通常可以使用标准库中的std::time::Duration类型,这篇文章主要介绍了Rust如何累计时间长度,需要的朋友可以参考下
    2024-05-05
  • Rust包和Crate超详细讲解

    Rust包和Crate超详细讲解

    这篇文章主要介绍了Rust包管理和Crate,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-12-12
  • Rust开发WebAssembly在Html和Vue中的应用小结(推荐)

    Rust开发WebAssembly在Html和Vue中的应用小结(推荐)

    这篇文章主要介绍了Rust开发WebAssembly在Html和Vue中的应用,本文将带领大家在普通html上和vue手脚架上都来运行wasm的流程,需要的朋友可以参考下
    2022-08-08
  • 深入理解Rust中Cargo的使用

    深入理解Rust中Cargo的使用

    本文主要介绍了深入理解Rust中Cargo的使用,Cargo简化了项目的构建过程,提供了依赖项管理,以及一系列方便的工作流程工具,下面就来具体的介绍一下如何使用,感兴趣的可以了解一下
    2024-04-04
  • 深入了解Rust的生命周期

    深入了解Rust的生命周期

    生命周期指的是引用保持有效的作用域,Rust的每个引用都有自己的生命周期。本文将通过示例和大家详细说说Rust的生命周期,需要的可以参考一下
    2022-12-12
  • 详解Rust调用tree-sitter支持自定义语言解析

    详解Rust调用tree-sitter支持自定义语言解析

    使用Rust语言结合tree-sitter库解析自定义语言需要定义语法、生成C解析器,并在Rust项目中集成,具体步骤包括创建grammar.js定义语法,使用tree-sitter-cli工具生成C解析器,以及在Rust项目中编写代码调用解析器,这一过程涉及到对tree-sitter的深入理解和Rust语言的应用技巧
    2024-09-09

最新评论