Rust常用特型之From和Into特型

在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。

std::convert::Fromstd::convert::Into特型是一种用于转换的特型,它们会消耗某个类型的值,返回另一个类型的值。相对于AsRefAsMut特型用来从一个类型借出另一个类型的引用,FromInto特型会获取参数值的所有权并对它进行转换,最后返回生成的新类型的值(注意不是引用)。

他们两个特型的定义相当对称:

trait Into<T>: Sized {
fn into(self) -> T;
}
trait From<T>: Sized {
  fn from(other: T) -> Self;
}

标准库自动实现了所有类型到它自己的转换,每个类型都实现了From<T>Into<T>。注意这里的类型必须是Sized类型,否则违反这两个特型的定义,注意,特型定义中,T是否Sized是显式定义,所以默认它就是Sized 类型。

虽然这两个特型提供的函数实现了相同的功能,但是它们却应用于不同的场景。

通常使用Into 类型让你的函数可接受的参数更加灵活,例如,你可以写如下 代码:

use std::net::Ipv4Addr;
fn ping<A>(address: A) -> std::io::Result<bool>
where A: Into<Ipv4Addr>
{
  let ipv4_address = address.into();
  ...
}

这里,ping函数不仅能接收IPV4Addr作为参数,它也能接受u32或者[u8;4]类型的值。因为这些类型都实现了Into<Ipv4Addr>。(从含义上来讲,把一个IPV4地址当成一个32位的值或者长度为4的u8数组也是有意义的)。

ping函数仅需要知道address参数实现了Into<Ipv4Addr>就行了,这里并不需要限定你实际调用时传递进去参数的类型。这里只有一种可能工作的情况,所以类型推断自动为你填充了相应内容。

正向我们先前在学习AsRef时提到的,Into特型可以让函数实现类似C++多态的效果。当我们使用上面的ping函数定义时,你可以做下面的任意调用:

println!("{:?}", ping(Ipv4Addr::new(23, 21, 68, 141))); // pass an Ipv4Addr
println!("{:?}", ping([66, 146, 219, 98]));   // pass a [u8; 4]
println!("{:?}", ping(0xd076eb94_u32));  // pass a u32

然而,From特型,却扮演了一个不同的角色。from函数主要用来作为一个构造器,它从其它类型的值产生一个当前类型的实例。例如,IPV4Addr除了有两个函数from_arrayfrom_u32外,它还实现了From<[u8;4]>From<u32>,因此我们可以写出如下代码:

let addr1 = Ipv4Addr::from([66, 146, 219, 98]);
let addr2 = Ipv4Addr::from(0xd076eb94_u32);

我们可以让类型推断来确定到底适配哪个实现。

给定一个恰当的From实现,Rust标准库会自动为你实现相对应的Into特型。当你定义自己的类型时,如果你的构造器只有单一参数,你应该采用实现From<T>方式来编写它。这时,你会免费得到相应的Into实现。

这里举个简单的例子吧,如A实现了From<B>,那么B就自动实现了Into<A>。过程很简单,直接在Into特型的into函数里调用A::from(B)即可。

因为fromto转换函数会获取传递参数的所有权,转换可以在新构造的值上重用初始值的资源。例如,假设有如下代码:

let text = "Beautiful Soup".to_string();
let bytes: Vec<u8> = text.into();

StringInto<Vec<u8>>实现简单的获得了原字符串的堆缓冲区并且重用它,并没有改变它,而是直接作为向量元素的缓冲区返回。这个转换并不需要重新分配空间或者复制文本。这是move使得实现更高效的另一个场景。

这些转换同时也提供了一个优雅的方法来松绑一个值,它从一个约束型类型转换成一个更灵活的类型, 同时并不削弱约束类型的约束的保证。例如,String类型保证它的内容总是有效的UTF-8字符,它的可变方法会被仔细限制为确保不会引入无效的UTF-8字符。但是这个示例演示了一个字符串可以转化为普通的字节,然后你就可以在它上面做你想做的事了,例如压缩它,或者和其它不是UTF-8字符相链接。由于into函数会获取值的所有权,因此text变量在转换之后就变成未初始化的,意味着我们可以自由的访问前者的字符串缓冲区而不必打破字符串的任何约定。

这里的意思是你可以将一个约束更多的类型转换成一个更灵活的类型,然后你就可以做更多的处理。

然而,一个便宜的转换并不是Into或者From的一部分。相对的,AsRefAsMut 转换被期望为便宜的(因为他们只是借出引用),FromInto转换有可能分配空间,进行复制或者包含对值内容的其它处理。例如,String实现了From<&str>,它会将字符串切片复制到一个新分配的堆上的缓冲区。std::collections::BinaryHeap<T>实现了From<Vec<T>>,它会根据具体的算法来比较和重新排序元素。

注意,有时候会在FromInto上应用?操作符。它一般用于错误转换,将各种不同的错误转换成一个更广泛的统一的错误。使用?操作符可以让你少写很多代码。这个经常和thiserror::error搭配在一起使用,此时使用一个枚举定义一个错误。枚举的每个变量代表不同的错误。例如 枚举MyError作为函数返回值,而函数类的语句返回OtherError时,在其后面使用?操作符会调用相应的into方法,转换成MyError

例如,假定有一个需求,需要读取二进制数据,然后转成10进制数字,最后输出UTF-8文本。这就意味着使用std::str::from_utf8和在i32上使用FromStr实现,它们会返回不同的错误类型。假设我们使用第七章定义的GenericErrorGenericError类型,?操作符可以为你做这种自动转换。

type GenericError = Box<dyn std::error::Error + Send + Sync +'static>;
type GenericResult<T> = Result<T, GenericError>;
fn parse_i32_bytes(b: &[u8]) -> GenericResult<i32> {
	Ok(std::str::from_utf8(b)?.parse::<i32>()?)
}

像绝大多数error类型一样,Utf8ErrorParseIntError都实现了Error特型,标准库增加了一个空From实现,它可以将任意实现了Error特型的对象转化为一个Box<dyn Error>,我们可以使用?自动来转换。

impl<'a, E: Error + Send + Sync + 'a> From<E>
for Box<dyn Error + Send + Sync + 'a> {
  fn from(err: E) -> Box<dyn Error + Send + Sync + 'a> {
    Box::new(err)
  }
}

如果不使用?操作符,而是使用match匹配的话,你需要写很多代码。通过使用两次?操作符,你只需要一行代码。

其实这里的?操作符的作用是错误传递,如果一个Result结果返回的是Ok(v),则?操作符直接得到v的值,如果返回的是Err(e),则会终止函数运行并把Err(e)作为函数结果返回。由于函数的返回类型是一个更广泛的类型Result<T,U>,因此Err(e)会自动转化为Err(u)。

这里书中的示例和常用的thiserror示例基本是相同的,只不过一个是返回了自定义的枚举类型(实现了Error特型),一个是返回Error特型对象(当然特型对象更广泛了)。

虽然Result定义中的E并没有约定E:Error,但一般都用于E:Error。

FromInto特型被添加到Rust标准库之前,Rust代码充斥着临时热转换特型和构造方法,每个都定义了一个单独的类型。FromInto特型统一化了这种转换,只要你遵循它,你可以很容易的使用,你的用户对此也会很熟悉。语言本身和其它库也可以依赖这些特型来实现一个标准的,范例性的转换封装。

FromInto是不可失败特型,他们的API需要这种转换一定是成功的。不幸的是,许多转换复杂的多,例如,转换一个很大的i64整数到i32会丢失一些信息,转换后的结果可能并不是我们想要的。例如:

let huge = 2_000_000_000_000i64;
let smaller = huge as i32;
println!("{}", smaller); // -1454759936

有好几种方式来处理这种情况,根据代码中的上下文环境,一个wrapping转换可能更合适。另一方面,数字信息和处理系统经常会使用saturating转换,这里超过最大值的数会限制为最大值。

这里的说的意思是由于FromInto不可能失败,有时直接进行转换并不合适,例如上面的例子,直接转换得到了负数。因此要根据实际使用情况选择合适的方法,而不是简单的使用FromInto直接进行转换。

总结

本章重点讲了如下几点:

  1. From和Into将一种类型转换成另一种类型,他们是对称的,实现其中一种,Rust会自动帮你实现另外一种。
  2. From和Into会消耗转换的值,同时有可能重新分配空间和进行复制,因此不是便宜的转换。但某些场景下,Move方式更高效。
  3. From和Into可以通过?操作符自动转换,一般这种场景用于错误传递。
  4. From和Into不可失败,并且也不是万能的(有些场景不适用直接转换)。
  5. From和Into可以将一种约束类型转换为更广泛的类型而不打破这种约束。
  6. Into特型可以让你的函数参数更灵活,而From特型主要用于构造器。
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AiMateZero

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值