深入Rust中的类型转换

发布时间:2022-11-01 08:30
最后更新:2024-06-20 22:40
所属分类:
Rust

其实类型转换在各种强类型的语言中的使用都非常的广泛,而在实现某个具体业务的时候,所绘制的数据流图也是由一种数据转换成另一种数据来体现业务流程的流转和变化的。所以其实设计程序的工作更多的就是处理和转换不同的数据类型。但是对与Rust这种对于数据类型要求更高的语言中,数据类型之间的转换就看起来变得更加复杂了。

不定长类型

在Rust中看到最多的泛型约束可能就是Sized?Sized两个了。它们分别表示定长类型和不定长类型,其中不定长类型又可以被称为动态大小类型。动态大小类型的大小在编译期是无法知晓的,必须在程序实际运行的时候才能够确定。这对于其他大多数语言来说,并不是什么难以处理的问题,但是到了Rust中,就变成了一个令人头疼的问题。

Rust编译器要求
Rust的编译器要求所有代码中所声明使用的类型都必须是定长的,或者说在编译期其长度是确定的。

根据Rust编译器的要求,如果在代码中直接使用了不定长类型,那么代码是不能通过编译的。例如类型str和切片。str作为存储和表示字符串的底层类型,其长度是根据所存储的字符串确定的,所以其长度不可能在编译期确定;切片也是如此。所以这就能解释为什么我们在代码中看到的字符串一般都是&str类型,切片一般都是&[]类型,它们都是引用的形式。

这是因为引用在内存中占据的长度是已知的,在代码中使用引用是能够满足Rust编译器要求的。

所以,如果在代码中的泛型约束中看到了?Sized标记,请放心大胆的使用引用来访问,这表示编译器并不限制泛型类型的大小。

FromInto

From是Rust标准库中提供的一个用于显式类型转换的特征。从它的签名可以看出它的功能。

1
2
3
pub trait From<T> {
  fn from(T) -> Self;
}

所有实现了From特征的类型可以通过From中规定的方法from()消耗目标值,并生成一个自身类型的实例。在大部分情况下,Rust都推荐我们利用这个特征来进行显式的类型转换,From特征带来的特点就是我们能够明确的知道我们要将什么类型的值转换成什么类型的值。

From特征还常常被用于错误处理中,在返回Result<T, E>类型的实例时,实现了From特征的错误可以被自动转换为函数返回值中所需要返回的错误类型。

Into是Rust标准库中提供的另一个特征,这个特征的是From的对立面,它所提供的功能是将当前的实例转换为指定类型的实例。所以Into的转换是主动的显式类型转换。所以根据Into的这个特点,Rust针对实现了From特征的类型会自动生成Into特征的实现。

在使用Rust自动实现Into的这个特性的时候,需要注意Rust的特殊限制,这一点需要参考Rust标准库的文档,但总结起来就是编译向旧版本的Rust或者需要转换成当前crate以外定义的类型时。

优先选择实现Into特征而不是From特征的情况,主要是用在泛型约束上,实现了Into特征的类型实例可以被用作对应类型的参数。在作为函数实参传入函数的时候,Rust会自动调用Into特征提供的方法进行自动类型转换。如果在自己编写的函数中需要手动调用Into特征,那么可以使用Into::<TargetType>::into(instance)的形式,而不是使用instance.into::<TargetType>()的形式;而且前一种形式在应用在.map()这类接受一个闭包的函数中,是非常好用的,例如.map(Into::<TargetType>::into),可以不必全新定义一个闭包。

调用其他的特征中的方法也可以使用这种形式,但是需要注意源类型必须已经实现了这个特征。
From特征和Into特征都是不允许出现任何错误的,如果在进行类型转换的过程中可能会出现错误,那么应该选择使用TryFromTryInto来代替。

AsRefAsMut

FromInto用来执行实例到实例之间的转换不同的是,AsRefAsMut是用来执行引用到引用之间的转换的。AsRefAsMut两个特征中的核心方法分别是as_ref()as_mut(),它们分别用来实现不同类型引用之间的转换。这个转换规则是十分简单的。

  • 如果类型S实现了AsRef<T>,那么就可以通过调用as_ref()完成从&S&T的转换。
  • 如果类型S实现了AsMut<T>,那么就可以通过调用as_mut()完成从&S&mut T的转换。

对于Rust默认给AsRefAsMut实现的规则,对于实现了这两个特征的类型还存在着以下的规则。

  • 如果类型S实现了AsRef<T>,那么类型&S也就实现了AsRef<T>
  • 如果类型S实现了AsRef<T>,那么&mut S也就实现了AsRef<T>
  • 如果类型S实现了AsMut<T>,那么&mut S也就实现了AsMut<T>

AsRef一般主要用在不会失败的不同类型引用之间的转换,或者是对于其内部包含元素之间的转换。如果转换比较复杂,则最好为&T类型实现From特征来进行转换。

如果使用AsRef特征作为函数的参数,例如fn f<T: AsRef<Path>>(p: T),可以使函数能够接收任意实现了AsRef特征的类型的参数,在函数调用的时候,会自动的对传入的参数进行转换。

Box

Box是Rust中比较常见的智能指针包装类型,智能指针一般都是一个结构体,但是通过其内部携带的元数据,可以提供比引用更加强大的功能。智能指针与引用之间的区别主要在于引用只是借用了数据,而智能指针可以拥有它们所指向的数据。

到这里是不是想到了什么,是的,Box这类智能指针可以作为函数的返回值。类似的只能指针还有RcArcCellRefCell

实际上智能指针结构体在Rust中还有很多。

Box<T>允许将一个值分配到堆内存上,而不是像往常一样分配在栈内存上。使用Box将值分配在堆上以后,会在栈上留下一个指向堆上数据的智能指针结构。在Rust中,堆上的数据也都是拥有一个所有者的,所以在发生所有权转移的时候,实际上只是转移了栈上的引用或者智能指针。Box只是一个非常简单的包装类型,处了用于将值分配在堆上以外,没有任何其他的功能,非常的单一。所以其常常被使用在以下场景中。

  • 特意将值分配在堆内存上。
  • 避免在转移所有权的时候对数据进行复制,此时可以直接使用&T来接收参数,但是返回值还是需要使用Box<T>或者impl AsRef<T>
  • 无法在编译期确定的类型的大小,但又需要使用固定大小的类型,比如从函数返回了一个切片,或者定义递归类型。
  • 用于说明实例实现了一个特征,但并不指定是某一个特定的类型,例如Vec<Box<dyn Unit>>可以在Vec里持有任何实现了Unit特征的元素,但并不需要指定具体的类型名称。
  • 用于在运行期产生一个全局有效的值,可以借用Box::leak()方法来实现,例如将一个在函数中生成的值转换成一个生命期是'static的值。
其实其他语言中的对象也是依靠Box来实现的,只是它们不需要我们手动管理内存。

关于Deref

任何实现的Deref特征的类型,都可以像智能指针那样工作,可以允许写出同时支持智能指针和引用的代码。引用实际上就是指针,其中存储的是其所指向的数据所在的内存地址,对引用使用*操作符解引用可以获取到其指向位置的数据。智能指针也可以被*解引用,效果与对引用解引用是一样的,但是智能指针是一个结构体,对于Rust来说,智能指针就是一个实现了Deref特征的结构体。我们可以通过为结构体实现Deref特征来实现自己的智能指针。

Deref特征中所需要实现的只是一个名为deref()的方法,所以在对实现了Deref特征的智能指针类型进行解引用的时,实际上Rust执行的是*(instance.deref())

*instance*(instance.deref())的替换只会发生一次,不会产生*((instance.deref()).deref())的形式。

在函数参数中使用实现了Deref特征的参数类型,将会遇到Rust提供的一个隐式类型转换。若一个类型实现了Deref特征,那么如果在传递它的引用作为函数的实参时,Rust会根据函数的签名确定是否需要自动的使用Deref特征进行转换。例如函数fn display(s: &str)接受的是一个&str类型的参数,因为类型String实现了Deref特征可以使其转换为返回&str的引用,所以就可以将其直接传入函数。

采用这种隐式转换的时候,要求传入的实参必须是引用形式,也就是使用&来触发Deref自动转换。
连续转换

在学习Rust的时候,在多个包装类型之间产生的套娃十分常见,而每一个“套娃”可能都会实现一个Deref,例如let s = Box::new(String::from("hello"));,这里的s就是Box<String>类型了,那么如果要将其使用在fn display(s: &str)函数中,是否需要手动.deref()一下?例如display(&(s.deref()))

答案是不需要,Rust会自动的对实参进行自动的连续转换,并且不止对于Deref特征会进行连续转换,FromInto特征等也会。所以在Rust提供自动连续转换以后,函数的实际调用方式就是display(&s)。此外,Rust还会对诸如&&&&&s之类的多重引用自动进行归一,将其转换成为&s;而对于BoxCowRcArc等包装类型,Deref特征还会将其转换为脱壳后的内部引用类型,即&Box<T>会被转换成&T

Pin

Pin可以算是Rust中比较难以理解的概念之一了。Pin是一个智能指针,其中包含了另一个指针,只要这个内部指针指向了一个没有实现Unpin特征的内容,Pin就可以保证被其内部指针指向的内容在内存中不会被转移所有权(moved)。

如果被“钉住”的类型T实现了Unpin特征,那么Pin<T>就相当于T了,但返回的始终是&T。这种情况只能在结构体中声明一个PhantomPinned类型的成员来使Unpin特征失效。

因为Pin也实现了Deref特征,所以被其包装的实例在使用的时候也可以被自动引用归一。

例如在使用SQL Server数据库驱动tiberius的时候,遇到的BoxStream<Result<Row>>类型,其实际类型为Pin<Box<dyn Stream<Result<Row>>>>,所以在获取其中的Result<Row>类型的时候,可以无视PinBox的包装,直接调用Stream提供的方法,Rust会自动进行连续解引用。

点操作符的魔法

Rust中的点操作符.可并不像其他语言中的点操作符只是用于访问实例的成员那么简单。Rust中的点操作符在使用的时候会发生很多自动进行的操作,例如自动引用、自动解引用、强制类型转换等。

在使用点操作符调用一个方法的时候,Rust会自动按照“值方法调用”(T::func())、“引用方法调用”(&T::func())、“解引用方法调用”((T.deref())::func())的顺序来寻找满足条件的方法。在这个过程中,尝试“引用方法调用”是一个自动引用的过程;尝试“解引用方法调用”是一个自动解引用的过程,而这个自动解引用的过程是会连续解引用的,也就是会沿着实现了Deref特征的的方法逐层解引用下去。

这样一来,在使用Rust中的各种包装类型的时候,就可以放心的根据需要进行“套娃”操作了,“套娃”并不会对我们接下来的操作产生什么影响,我们也没有必要那么重点的关注一层层的.deref()调用,只需要关注我们所使用的“套娃”包装类型是不是实现了可以被自动解引用的Deref特征就可以了。

.iter().into_iter()

迭代器是Rust中的一个重要内容,大部分的可迭代内容都是通过实现Iterator特征来将自己变成一个迭代器的,但是还有相当大的一部分类型选择将自己转换成一个迭代器来迭代其中的元素。这就出现了在许多源码中经常会看到的两个方法.iter().into_iter()

这两个方法都可以用来创建一个迭代器,但是其返回的内容是有区别的,比较容易被忽略。

  • .into_iter()是由IntoIterator特征提供的,其作用是返回会消费其内部元素T的迭代器。
  • .iter()并没有一个归属的特征,它只是一个习惯上会定义使用的方法,.iter()会返回迭代其内部元素引用&T的迭代器。同样属于习惯上定义的方法还有.iter_mut()

所有实现了IntoIterator特征的类型,都可以被用在for循环中,for循环会自动调用其IntoIterator特征提供的方法获取到迭代。这也是为什么for循环会消费掉集合元素的原因。

通常来说,如果一个类型提供了.iter()方法,那么也会同时为其&T引用类型提供IntoInterator特征的实现,使其可以利用for来遍历。


索引标签
Rust
类型转换
装箱
拆箱
智能指针
迭代器