这篇文章收录于Rust 实战专栏。这个专栏中的相关代码来自于我开发的笔记系统。它启动于是2023年的9月14日。相关技术栈目前包括:Rust,Javascript。关注我,我会通过这个项目的开发给大家带来相关实战技术的分享。
关于Rust的类型转换,我在之前的已经感受到了Rust类型的一等公民地位中做了阐述。随着项目进度的推进,对类型转换的使用场景也变得丰富起来。
场景
说到数据转换,在程序开发中是一件很平常的事情,比如,我们下面要讨论的场景
models::User
定义如下:
rust
#[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 null
,tokio_posgres
库会将其转换成Rust的i32
类型。birthday_year
的类型是postgres的int null
,tokio_postgres
库会自动将其转换成Rust的Option<i32>
类型。
因此,我们的目标是
- 将
i32
转换成models::Gender
- 将
Option<i32>
转换成Option<u8>
先睹为快,最终的转换应用代码如下
rust
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<T>
trait
- 声明一个临时类型
SqlSex
rust
pub struct SqlGender(pub i32);
之所以要声明这个临时类型,是因为我们要告诉编译器这个tuple接收的数据类型是i32
。上面的row.get("gender")
的返回实现了FromSql
的trait。tokio_postgres
通过FromSql
实现了对i32
的转换。
- 在
models::Gender
上实现From<SqlGender>
rust
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_from
,try_from
来源于TryFrom
trait。其实,如果我们看Rust关于u8
的文档,会看见一串From<T>
和TryFrom<T>
的实现。
Option<i32>
to Option<u8>
和上面的转不同,这个转换是结果是Option
,但由于输入的数据也是一个Option
,因此,这里我们还是使用的From<T>
。如果转换可能存在失败的情况,且我们要处理失败,那么我们应该使用std::convert::TryFrom<T>
。
- 声明临时类型
Sqlu8
rust
pub struct Sqlu8(pub Option<i32>);
声明这个临时类型和上面的原因是一样的,告诉编译器这里使用的是Option<i32>
类型。
- 在
Option<u8>
上实现From<Sqlu8>
rust
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
。
转换的应用
实现了上面的步骤,我们通过下面的方式来使用转换。
rust
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
类型。
rust
let msg :String = "hello".into();
这里看起来有没有一点魔幻的感觉,反正我是有的。实现转换的代码和应用转换的代码感觉没啥关联。我查了一下,这是Rust的"关注点分离"设计模式的一种体现。这样设计到好处显而易见,即我们可以无限扩展其类型的转换而不会对已有的代码造成任何影响。例如,我们上面就对i32
, u8
的转换进行了相关的扩展。
关于临时类型
我们在这里使用临时类型的原因是要告诉编译器,以pub struct Sqlu8(pub Option<i32>);
为例,我们要接受一个类型为Option<i32>
的值。即相当于一个中间变量。
还有一种情况需要使用临时类型,即如果你要转换的两个类型,都是从第三方模块中引入的,这个时候也需要加入一个临时类型过度。因为Rust不允许转换的两个类型都不在当前模块内。
小结
我们描述了转换的场景,具体的转换步骤和转换的应用方式。这里的场景是数据集和本地类型之间的数据转换。类似的场景还有很多,只要涉及到不同的上下文,数据转换的需求就会出现。使用Rust的std::convert::From<T>
trait,实际上就是在实践"关注点分离"的设计模式,它会大大提升我们代码的可维护性和可扩展性,个人认为这是写好Rust代码的重要方法之一。
如有问题,欢迎大家留言交流。关注我,后面会给大家带来更多关于Rust开发实战技术的分享。