前言
如题,我们看下泛型的实现和对应的用法。
泛型实现(Generic implementations)
我们也可以为泛型类型编写impl块,但由于额外的泛型类型参数,形式上会变得冗长。让我们在 Container<T> 结构体上实现一个new()方法,键入rustc 加文件名,编译一下。
// generic_struct_impl.rs
struct Container
<
T
>
{
item
:
T
}
impl Container
<
T
>
{
fn
new
(
item
:
T
)
->
Self
{
Container
{
item
}
}
}
fn
main
()
{
// stuff
}
报错如下,
上述报错是说,找不到泛型类型T。当我们为任何泛型类型写impl块时,需要在使用之前声明泛型类型的参数。换句话说,T就像一个变量——一个类型变量——我们需要声明它。因此,这里需要修改impl块,在impl后添加<T>,如下所示:
impl
<
T
>
Container
<
T
>
{
fn
new
(
item
:
T
)
->
Self
{
Container
{
item
}
}
}
于是整体代码如下
// generic_struct_impl.rs
struct Container
<
T
>
{
item
:
T
}
//impl<T> Container<T>
impl
<
T
>
Container
<
T
>
{
fn
new
(
item
:
T
)
->
Self
{
Container
{
item
}
}
}
fn main
()
{
// stuff
}
通过更改,可以编译前面的代码了,伴随两个合理的warning。
前面的impl块基本上意味着我们正在为所有类型T实现这些方法,其都出现在容器<T>中。这个impl块是一个泛型实现。因此,每个生成的具体容器都将具有这些方法。现在,也可以为容器<T>写一个更具体的impl块,通过把任何具体的类型来代替T,比如以下所示:
impl Container
<
u32
>
{
fn
sum
(
item
:
u32
)
->
Self
{
Container
{
item
}
}
}
在上面的代码中,我们实现了一个名为sum的方法,它只在容器<u32>类型上存在。这里,我们不需要impl后面的<T>,因为u32是一个具体的类型。这是impl块的另一个很好的属性,允许开发者通过独立实现方法来专门化泛型类型。
使用泛型
现在可以看到,实例化或使用泛型类型的方式与非泛型的对应类型有所不同。当我们进行实例化时,编译器需要知道的类型T的具体类型,也就是signature,用来提供类型信息以使泛型代码单一性化。大多数情况下,具体类型是基于类型的实例化推断,或者在泛型函数的场景下,通过调用接受具体类型的任何方法推断的。在极少数情况下,可能需要通过使用turbofish (::<>) 操作符来专门输入具体类型代替泛型类型,以帮助编译器确定。稍后会介绍这是如何使用的。
让我们看看实例化Vec<T>的例子,这是一个泛型类型。如果没有任何类型签名,以下代码将无法编译:
// creating_generic_vec.rs
fn
main
()
{
let
a
=
Vec
::
new
();
}
报错如下:
这是因为此时编译器不知道a将包含什么类型,直到我们手动去指定或调用相关的方法之一,从而可以传入一个具体的值。如下代码:
// using_generic_vec.rs
fn main
()
{
// providing a type
let
v1
:
Vec
<
u8
>
=
Vec
::
new
();
// or calling method
let
mut v2
=
Vec
::
new
();
v2
.
push
(
2
);
// v2 is now Vec<i32>
// or using turbofish
let
v3
=
Vec
::
<
u8
>
::
new
();
// not so readable
}
编译通过
在第上面的代码片段中,我们将v1的类型指定为u8的Vec,可以很好的编译。与v2一样,另一种方法是调用接受任何具体类型的方法。在push方法调用之后,编译器可以推断出v2是Vec<i32>。创建Vec的另一种方法是使用turbofish操作符,就像前面代码中的v3绑定一样。
一般函数中的turbofish操作符出现在函数名之后和括号之前。下一个代码例子是来自std::str模块的通用解析函数。parse可以从字符串中解析值,许多类型都可以从字符串中解析,比如i32、f64、usize等等,所以它是一个泛型类型。因此,在使用parse时,确实需要使用turbofish操作符,如下所示:
// using_generic_func.rs
use std
::
str
;
fn
main
()
{
let
num_from_str
=
str
::
parse
::
<
u8
>
(
"34"
).
unwrap
();
println
!
(
"Parsed number {}"
,
num_from_str
);
需要注意的是,只有实现FromStr接口或trait的类型才能传递给parse函数。u8有一个FromStr的实现,因此能够在前面的代码中解析。parse函数使用FromStr trait来限制可以传递给它的类型。在探索完特性之后,我们将了解如何把泛型和特性这两者混搭一起。
结语
有了泛型的概念,我们就可以马上探索Rust中最普遍的特色了——特性(Trait),想必读者有所感受了吧,我们的教程内容越来越有意味了。
主要参考和建议读者进一步阅读的文献
https://doc.rust-lang.org/book
Rust编程之道,2019, 张汉东
The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
Beginning Rust ,2018,Carlo Milanesi
Rust Cookbook,2017,Vigneshwer Dhinakaran