Rust-类型转换进阶

这篇文章收录于Rust 实战专栏。这个专栏中的相关代码来自于我开发的笔记系统。它启动于是2023年的9月14日。相关技术栈目前包括:Rust,Javascript。关注我,我会通过这个项目的开发给大家带来相关实战技术的分享。


关于Rust的类型转换,我在之前的已经感受到了Rust类型的一等公民地位中做了阐述。随着项目进度的推进,对类型转换的使用场景也变得丰富起来。

场景

说到数据转换,在程序开发中是一件很平常的事情,比如,我们下面要讨论的场景
Rust-类型转换进阶_第1张图片
models::User定义如下:

#[derive(Debug, Deserialize, Serialize)]
pub struct User {
    pub id: String,
    pub user_name: String,
    pub gender: Gender,
    pub alias: Option<String>,
    pub birthday_year: Option<u8>,
    pub birthday_month: Option<u8>,
    pub birthday_day: Option<u8>,
}

在数据集中,gender的类型是postgres的int not nulltokio_posgres库会将其转换成Rust的i32类型。birthday_year的类型是postgres的int nulltokio_postgres库会自动将其转换成Rust的Option类型。

因此,我们的目标是

  1. i32转换成models::Gender
  2. Option转换成Option

先睹为快,最终的转换应用代码如下

    let row= client.query_one("select id, user_name, alias, gender, birthday_year, birthday_month, birthday_day from users where id=$1", &[id]).await.map_err(MyError::from)?;
    Ok(User {
        ...
        gender: SqlGender(row.get("gender")).into(),
        birthday_year: Sqlu8(row.get("birthday_year")).into(),
    })

实现步骤

i32 to models::Sex

在这个转换过程中,我们会使用到std::convert::From trait

  1. 声明一个临时类型SqlSex
pub struct SqlGender(pub i32);

之所以要声明这个临时类型,是因为我们要告诉编译器这个tuple接收的数据类型是i32。上面的row.get("gender")的返回实现了FromSql的trait。tokio_postgres通过FromSql实现了对i32的转换。
2. 在models::Gender上实现From

impl From<SqlGender> for Gender {
    fn from(val: SqlGender) -> Self {
        if let Ok(val1) = u8::try_from(val.0) {
            if let Ok(result) = Gender::try_from(val1) {
                return result;
            }
        }
        Sex::NotSet
    }
}

上面的代码,实际上先将i32数据类型转换成u8类型,然后再将u8类型转换成Gender。上面的两个转换过程我们都使用了try_fromtry_from来源于TryFrom trait。其实,如果我们看Rust关于u8的文档,会看见一串FromTryFrom的实现。

Option to Option

和上面的转不同,这个转换是结果是Option,但由于输入的数据也是一个Option,因此,这里我们还是使用的From。如果转换可能存在失败的情况,且我们要处理失败,那么我们应该使用std::convert::TryFrom

  1. 声明临时类型Sqlu8
pub struct Sqlu8(pub Option<i32>);

声明这个临时类型和上面的原因是一样的,告诉编译器这里使用的是Option类型。
2. 在Option上实现From

impl From<Sqlu8> for Option<u8> {
    fn from(val: Sqlu8) -> Self {
        if let Some(val1) = val.0 {
            if let Ok(result) = u8::try_from(val1) {
                return Some(result);
            }
        }
        None
    }
}

上面的代码先拿到有效的i32数据,然后再将i32转换成u8类型,任何失败都将返回None

转换的应用

实现了上面的步骤,我们通过下面的方式来使用转换。

    let row= client.query_one("select id, user_name, alias, gender, birthday_year, birthday_month, birthday_day from users where id=$1", &[id]).await.map_err(MyError::from)?;
    Ok(User {
        ...
        gender: SqlGender(row.get("gender")).into(),
        birthday_year: Sqlu8(row.get("birthday_year")).into(),
    })

我们先用声明的临时类型来包裹数据,然后通过调用.into()来实现转换。
Rust本身也有类似的用法,例如将字符串切片转换成String类型。

let msg :String = "hello".into();

这里看起来有没有一点魔幻的感觉,反正我是有的。实现转换的代码和应用转换的代码感觉没啥关联。我查了一下,这是Rust的“关注点分离“设计模式的一种体现。这样设计到好处显而易见,即我们可以无限扩展其类型的转换而不会对已有的代码造成任何影响。例如,我们上面就对i32, u8的转换进行了相关的扩展。

关于临时类型

我们在这里使用临时类型的原因是要告诉编译器,以pub struct Sqlu8(pub Option);为例,我们要接受一个类型为Option的值。即相当于一个中间变量。
还有一种情况需要使用临时类型,即如果你要转换的两个类型,都是从第三方模块中引入的,这个时候也需要加入一个临时类型过度。因为Rust不允许转换的两个类型都不在当前模块内。

小结

我们描述了转换的场景,具体的转换步骤和转换的应用方式。这里的场景是数据集和本地类型之间的数据转换。类似的场景还有很多,只要涉及到不同的上下文,数据转换的需求就会出现。使用Rust的std::convert::From trait,实际上就是在实践“关注点分离”的设计模式,它会大大提升我们代码的可维护性和可扩展性,个人认为这是写好Rust代码的重要方法之一。

如有问题,欢迎大家留言交流。关注我,后面会给大家带来更多关于Rust开发实战技术的分享。

你可能感兴趣的:(rust,前端)