特型和泛型
Rust 通过两个相关联的特性来支持多态:特型和泛型。许多 程序员熟悉这些概念,但 Rust 受到 Haskell 类型类(typeclass)的启发,采用 了一种全新的方式。
1、特型是 Rust 体系中的接口或抽象基类。乍一看,它们和 Java 或 C# 中的接口
差 不多。
写入字节的特型称为 std::io::Write,它在标准库中的定义开头部分 是这样的
trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
...
}
File 和 TcpStream 这两个标准类型以及 Vec 都实现了 std::io::Write。这 3 种类型都提供了 .write()、.flush() 等方法。
第一个多态案例:
use std::io::Write;
fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
out.write_all(b"hello world\n")?;
out.flush()
}
out 的类型是 &mut dyn Write,意思是“对实现了 Write 特型的任意值的可 变引用”。我们可以将任何此类【或者理解为子类】值的可变引用传给 say_hello:
use std::fs::File;
let mut local_file = File::create("hello.txt")?;
say_hello(&mut local_file)?; // 正常
let mut bytes = vec![];
say_hello(&mut bytes)?; // 同样正常
assert_eq!(bytes, b"hello world\n");
2、 泛型是 Rust 中多态的另一种形式。与 C++ 模板一样,泛型函数或泛型类型可以和不同类型的值一起使用:
编译器会针对你实际用到的每种类型 T 生成一份单独的机器码。【静态多态】
/// 给定两个值,找出哪个更小
fn min<T: Ord>(value1: T, value2: T) -> T {
if value1 <= value2 {
value1
} else {
value2
}
}
min 函数可以与实现了 Ord 特型的任意类型(任 意有序类型)T 的参数一起使用。像这样的要求称为限界
泛型和特型紧密相关:泛型函数会在限界中使用特型来阐明它能针对哪些类型的 参数进行调用。我们将会讨论 &mut dyn Write
和T <Write>
的相似之处、不同之处,以及对它俩的选择。
特型
特
代表独特,代表自我个人或者一类人,特型代表着一种能力,即一个类型能做什么
- 实现了 std::io::Write 的值能写出一些字节。
- 实现了 std::iter::Iterator 的值能生成一系列值。
- 实现了 std::clone::Clone 的值能在内存中克隆自身。
- 实现了 std::fmt::Debug 的值能用带有 {:?} 格式说明符的 println! () 打印出来。
特型本身必须在作用域内。否则,它的所 有方法都是不可见的
use std::io::Write;
let mut buf: Vec<u8> = vec![];
buf.write_all(b"hello")?; //省略use导入时,此条语句报错[找不到特型方法:write_all]
Rust 这条规则,拒绝你使用特型为任意类型添加新方法—— 甚至是像 u32 和 str 这样的标准库类型,拒绝第三方 crate 也这样做而导致的命名冲突。
Rust 会要求你导入自己想用的特型,因此 crate 可以放心地利用这种超能力。只有导入两个特型,才会发生冲突,将具有相同名称的方法添加到同一个类型中。[使用带完全限定符的方法名解决它]
Clone 和 Iterator 的各个方法在没有任何特殊导入的情况下就能工作,因为 默认情况下它们始终在作用域中:它们是标准库预导入的一部分,Rust 会把这 些名称自动导入每个模块中。
C++ 程序员和 C# 程序员可能已经看出来了,特型方法类似于虚方法
。不过, 特型方法的调用仍然很快,与任何其他方法调用一样快。这里没有 多态,buf 是向量,编译器可以生成 对 Vec::write() 的简单调用,甚至可以内联该方法。只有通过&mut dyn Write
【父类指针哈哈】调用时才会产生动态派发(也叫虚方法调用)的开销,类型上的 dyn
关 键字指出了这一点。dyn Write
叫作特型对象
特型对象—特型的引用
Rust 中使用特型编写多态代码有两种方法:特型对象和泛型
Rust 不允许 dyn Write 类型的变量:
use std::io::Write;
let mut buf: Vec<u8> = vec![];
let writer: dyn Write = buf; // 错误:`Write`的大小不是常量 所以无法创建writer实例
// 但可以这样:
let mut buf: Vec<u8> = vec![];
let writer: &mut dyn Write = &mut buf; // 正确
变量的大小必须是编译期已知的
Java
中,OutputStream 类型
(类似于 std::io::Write 的 Java 标准接口)的变量其实是对任何实现了 OutputStream 的对象的引用
。
对特型类型(如 writer)的引用叫作特型对象。与任何其他引用一样,特型对 象指向某个值,它具有生命周期,并且可以是可变或共享的。
Rust 通常无法在编译期间知道引用目标的类型。因此,特型对象要包含一些关于引用目标类型的额外信息。这仅供 Rust 自 己在幕后使用:当你调用 writer.write(data)
时,Rust 需要使用类型信息 来根据 *writer
的具体类型动态调用
正确的 write 方法。你不能直接查询这些类型信息,Rust 也不支持从特型对象&mut dyn Write
向下转型回像 Vec 这样的具体类型。
特型对象的内存布局:
在内存中,特型对象是一个胖指针
,由指向值的指针和指向表示该值类型的虚表
的指针组成。因此,每个特型对象会占用两个机器字
。
C++ 也有这种运行期类型信息,叫作虚表或 vtable。就像在 C++ 中一样,在 Rust 中,虚表只会在编译期生成一次,并由同一类型的所有对象共享。调用特型对象的方法时,会自动使用虚表来确定要调用哪个实现。
Rust 和 C++ 在内存使用上略有不同。在 C++ 中,虚表指针或 vptr 是作为结构体/类的一部分存储的,而 Rust 使用的是胖指针方案。结构体本身只包含自己的字段。这样一来,每个结构体就可以实现几十个特型而不必包含虚函数表了。甚至连 i32 这样大小不足以容纳 vptr 的类型都可以实现特型。
也就是:
- C++:father指针指向堆区空间x -> 空间x中存在 vptr -> vptr指向vtable -> 通过vtable找到要调用的方法
- rust:father胖指针存在vtable地址 -> 通过vtable找到要调用的方法
Rust 在需要时会自动将普通引用转换为特型对象【将子类指针转化为父类指针】。
这就是为什么我们能够在这 个例子中把 &mut local_file
传给 say_hello
:
let mut local_file = File::create("hello.txt")?;
say_hello(&mut local_file)?;
&mut local_file
的类型是 &mut File
,say_hello
的参数类型是 &mut dyn Write
。由于 File 也是一种写入器,因此 Rust 允许这样操作,它会自动 将普通引用转换为特型对象。
同样,Rust 会愉快地将 Box<File>
转换为 Box<dyn Write>
,这是一个拥有 在堆中分配的写入器的值:
let w: Box<dyn Write> = Box::new(local_file);
Box 也是一个胖指针,即包含写入器本身的地址和虚表的地址。其他指针类型(如 Rc)同样如此。
**这种转换是创建特型对象的唯一方法。**编译器在这里真正做的事非常简单。在发生转换的地方,Rust 知道引用目标的真实类型(在本例中为 File),因此它只要加上适当的虚表的地址,把常规指针变成胖指针就可以了。 【666】
泛型函数与类型参数
use std::io::Write;
fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
out.write_all(b"hello world\n")?;
out.flush()
}
重写为泛型函数:
fn say_hello<W: Write>(out: &mut W) -> std::io::Result<()> {
out.write_all(b"hello world\n")?;
out.flush()
}
只发生了函数签名的变化:
fn say_hello(out: &mut dyn Write) // 普通函数
fn say_hello<W: Write>(out: &mut W) // 泛型函数
把函数变成了泛型形式。此短语叫作类型参数
Rust 从参数的类型推断出类型 W,这个过程叫作单态化
可以明确写出类型参数: say_hello::(&mut local_file)?;
泛型函数没有任何能提供有用线索的参数,则可能需要把它明确写 出来:比如使用默认参数或者无参数时
let v1 = (0 .. 1000).collect(); // 错误:无法推断类型
let v2 = (0 .. 1000).collect::<Vec<i32>>(); // 正确
例子:打印出向量中前十个最常用的 值,那么就要让这些值是可打印的:
use std::fmt::Debug;
fn top_ten<T: Debug>(values: &Vec<T>) { ... }
新需求:要确定哪些值是最常用的该怎么办?
意味着这些值还要支持 Hash 操作和 Eq 操作。T 的类型限界必须包括这些特型,就像 Debug 一样。这种情况下就要使用 + 号语 法:
use std::hash::Hash;
use std::fmt::Debug;
fn top_ten<T: Debug + Hash + Eq>(values: &Vec<T>) { ... }
表示支持参数必须同时实现了这 3 个
泛型函数支持多个T:
fn run_query<M: Mapper + Serialize, R: Reducer + Serialize>( data: &DataSet, map: M, reduce: R)
-> Results
{ ... }
<>限界可能会变得很长,让人眼花缭乱。Rust 使用关键字 where 提供了另一种语法:
fn run_query<M, R>(data: &DataSet, map: M, reduce: R) -> Results
where M: Mapper + Serialize,R: Reducer + Serialize
{ ... }
where 子句也允许用于泛型结构体、枚举、类型别名和方法——任何允许使用限界的地方。
生命周期写在前面:
fn nearest<'t, 'c, P>(target: &'t P, candidates: &'c [P]) -> &'c P
where P: MeasureDistance
{
...
}
生命周期永远不会对机器码产生任何影响。如果对 nearest() 进行的两次调用 使用了相同的类型 P 和不同的生命周期,那么就会调用同一个编译结果函数。
只有不同的类型才会导致 Rust 编译出泛型函数的多个副本。
泛型结合常数:
fn dot_product<const N: usize>(a: [f64; N], b: [f64; N]) -> f64 {
let mut sum = 0.;
for i in 0..N {
sum += a[i] * b[i];
}
sum
}
// 显式提供`3`作为`N`的值
dot_product::<3>([0.2, 0.4, 0.6], [0., 0., 1.])
// 让Rust推断`N`必然是`2`
dot_product([3., 4.], [-5., 1.])
指出函数 dot_product 需要一个泛型参 数 N,该参数必须是一个 usize。给定了 N!
泛型并不是一定基于泛型类型:
- 单独的方法也可以是泛型的,即使它并没有定义在泛型类型上
- 类型别名也可以是泛型的。
impl PancakeStack {
fn push<T: Topping>(&mut self, goop: T) -> PancakeResult<()> {
goop.pour(&self);
self.absorb_topping(goop)
}
}
type PancakeResult<T> = Result<T, PancakeError>;
选择特型还是泛型?
- 需要一些混合类型值的集合时,选择特型对象
struct Salad {
veggies: Vec<dyn Vegetable> // 错误:`dyn Vegetable`的大小不是常量
}
//解决:使用泛型对象
struct Salad {
veggies: Vec<Box<dyn Vegetable>> // Box大小只是两个指针的大小 常量
}
- 减少编译后代码的总大小,选择特型对象
因为Rust 可能会不 得不多次编译泛型函数,针对用到了它的每种类型各编译一次。
泛型的优点:
- 速度,简单指令泛型函数直接编译期调用给出返回值,而特型对象是运行时动态确定类型
- 并不是每个特型都能支持特型对象
- 容易同时指定具有多个特型的泛型参数限界
定义与实现特型
定义特型很简单,给它一个名字并列出特型方法的类型签名即可
实现特型:impl 特型名字 for 本类名{}
trait Visible {
/// 画布上渲染此对象
fn draw(&self, canvas: &mut Canvas);
/// 如果单击(x, y)时应该选中此对象,就返回true
fn hit_test(&self, x: i32, y: i32) -> bool;
}
impl Visible for Broom {
fn draw(&self, canvas: &mut Canvas) {
for y in self.y - self.height - 1 .. self.y {
canvas.write_at(self.x, y, '|');
}
canvas.write_at(self.x, self.y, 'M');
}
fn hit_test(&self, x: i32, y: i32) -> bool {
self.x == x
&& self.y - self.height - 1 <= y
&& y <= self.y
}
}
这个 impl 只能包含 Visible 特型中每个方法的实现
write 方法和 flush 方法是每个写入器必须实现的基本方法。写入器也可以自 行实现 write_all,但如果没实现,就会使用默认实现。Iterator 特型,它有一个必要 方法 (.next()) 和几十个默认方法
Rust 允许在任意类型上实现任意特型,但特型或类型二者必须至少有一个是在 当前 crate 中新建的。
想为任意类型添加一个方法,都可以使用特型来完成:
trait IsEmoji {
fn is_emoji(&self) -> bool;
}
/// 为内置的字符类型实现IsEmoji特型
impl IsEmoji for char {
fn is_emoji(&self) -> bool {
...
}
}
这个特殊特型的唯一目的是向现有类型 char 中添加一个方法。这称为扩展特型
实现特型时,特型或类型二者必须至少有一个是在当前 crate 中新建的。这叫作孤儿规则:解释
不能写成 impl Write for u8,因为 Write 和 u8 都是在标准库中定义 的。如果 Rust 允许 crate 这样做,那么在不同的 crate 中可能会有多个 u8 的 Write 实现
C++ 有一个类似的唯一性限制:单一定义规则
特型中的 Self
pub trait Clone {
fn clone(&self) -> Self;
...
}
以 Self 作为返回类型意味着 x.clone()
返回值的类型与 x 的类型相同
pub trait Spliceable {
fn splice(&self, other: &Self) -> Self;
}
impl Spliceable for CherryTree { //圣诞树
fn splice(&self, other: &Self) -> Self {
...
}
}
impl Spliceable for Mammoth { //猛犸象
fn splice(&self, other: &Self) -> Self {
...
}
}
第一个 impl 中,Self 是 CherryTree 的别名,而在第二个 impl 中,它是 Mammoth 的别名。这意味着可以将两棵樱桃树或两头猛犸象拼接在一 起,但不表示可以创造出猛犸象和樱桃树的混合体。原则上self 的类型和 other 的 类型必须匹配。
使用了 Self 类型的特型与特型对象不兼容
// 错误:特型`Spliceable`不能用作特型对象
fn splice_anything(left: &dyn Spliceable, right: &dyn Spliceable) {
let combo = left.splice(right);
// ...
}
特型对象的全部意义恰恰在于其类型要到运行期才能知道。Rust 在编译期无从 了解 left 和 right 是否为同一类型
特型对象实际上是为最简单的特型类型而设计的,这些类型都可以使用 Java 中 的接口或 C++ 中的抽象基类来实现。特型的高级特性很有用,但它们不能与特 型对象共存,因为一旦有了特型对象,就会失去 Rust 对你的程序进行类型检查 时所必需的类型信息。
解决:
pub trait MegaSpliceable {
fn splice(&self, other: &dyn MegaSpliceable) -> Box<dyn MegaSpliceable>;
}
此特型与特型对象兼容。对 .splice() 方法的调用可以通过类型检查,因为参 数 other 的类型不需要匹配 self 的类型,只要这两种类型都是 MegaSpliceable 就可以了
**子特型:**可以声明一个特型是另一个特型的扩展
// 生物是可视物体的扩展
trait Creature: Visible {
fn position(&self) -> (i32, i32);
fn facing(&self) -> Direction;
...
}
每个实现了 Creature 的类型也必须实现 Visible 特型
说 Creature 是 Visible 的子特型, 而 Visible 是 Creature 的超特型
子特型与 Java 或 C# 中的子接口
类似,因为用户可以假设实现了子特型的任何 值也会实现其超特型。但是在 Rust 中,子特型不会继承其超特型的关联项,如 果你想调用超特型的方法,那么仍然要保证每个特型都在作用域内。
Rust 的子特型只是对 Self 类型限界的简写,原型:
trait Creature where Self: Visible {
...
}
类型关联函数
大多数面向对象语言中,接口不能包含静态方法或构造函数,但特型可以包含 类型关联函数,这是 Rust 对静态方法的模拟
trait StringSet {
/// 返回一个新建的空集合
fn new() -> Self;
/// 返回一个包含`strings`中所有字符串的集合
fn from_slice(strings: &[&str]) -> Self;
/// 判断这个集合中是否包含特定的`string`
fn contains(&self, string: &str) -> bool;
/// 把一个字符串添加到此集合中
fn add(&mut self, string: &str);
}
每个实现了 StringSet 特型的类型都必须实现这 4 个关联函数。
在非泛型代码中,可以使用 ::
语法调用这些函数,就像调用任何其他类型关联函数一样【调用类静态函数】
// 创建实现了StringSet的两个假想集合类型:
let set1 = SortedStringSet::new();
let set2 = HashedStringSet::new();
在泛型代码中,也可以使用 ::
语法,其类型部分通常是类型变量
与 Java 接口和 C# 接口一样,特型对象也不支持类型关联函数。如果想使用 &dyn StringSet
特型对象,就必须修改此特型,为每个未通过引用接受 self 参数的关联函数加上类型限界 where Self: Sized:
trait StringSet {
fn new() -> Self
where Self: Sized;
fn from_slice(strings: &[&str]) -> Self
where Self: Sized;
fn contains(&self, string: &str) -> bool;
fn add(&mut self, string: &str);
}
这个限界告诉 Rust,特型对象不需要支持特定的关联函数【父类指针不支持调用此类静态函数】 。通过添加这些限 界,就能把 StringSet 作为特型对象使用了,但你还是可以创建它们并用其调用 .contains() 和 .add()。
Sized后面解释
完全限定的方法调用
- 等同的调用:
"hello".to_string(); str::to_string("hello")
第二种形式看起来很像关联函数调用。尽管 to_string 方法需要一个 self 参 数,但是仍然可以像关联函数一样调用。只需将 self 作为此函数的第一个参数 传进去即可。
- 由于 to_string 是标准 ToString 特型的方法之一,因此你还可以使用另外 两种形式:
ToString::to_string("hello"); <str as ToString>::to_string("hello")
最后一种带有尖括号的形式,同时指定了类型和特型,这就是完全 限定的方法调用。
写下 “hello”.to_string() 时,使用的是 . 运算符,你并没有确切说 明要调用哪个 to_string 方法。Rust 有一个“方法查找”
算法,它可以根据类型、隐式解引用等来解决这个问题。通过完全限定的调用,你可以准确地指出是 哪一个方法:
-
当两个方法具有相同的名称时
-
当无法推断 self 参数的类型时:
let zero = 0; // 类型未指定:可能为`i8`、`u8`…… zero.abs(); // 错误:无法在有歧义的数值类型上调用方法`abs` i64::abs(zero); // 正确
-
将函数本身用作函数类型的值时
完全限定语法也适用于关联函数
定义类型之间关系的特型
每种面向对象的语言都内置了某种对迭代器的支持,迭代器是用以遍历某种值序列的对象
关联类型(或迭代器的工作原理)
定义关联类型Item
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>; // 这里必须显示书写:Self::Item
...
}
//(来自标准库中std::env模块的代码)
impl Iterator for Args {
type Item = String;
fn next(&mut self) -> Option<String> {
...
}
...
}
这个特型的第一个特性(type Item;)是一个关联类型
/// 遍历迭代器,将值存储在新向量中
fn collect_into_vector<I: Iterator>(iter: I) -> Vec<I::Item> {
let mut results = Vec::new();
for value in iter {
results.push(value);
}
results
}
这个函数体中,Rust 为我们推断出了 value 的类型,必须明确写出collect_into_vector
的返回类型,而 Item 关联类型是唯一 的途径。(用 Vec *肯定不对,因为那样是在宣告要返回一个由迭代器组成 的向量。)后面介绍
/// 打印出迭代器生成的所有值
fn dump<I>(iter: I)
where I: Iterator
{
for (index, value) in iter.enumerate() {
println!("{}: {:?}", index, value); // 错误 value 不一定是可打印的类型
}
}
编译此泛型函数,就必须确保 I::Item 实现了 Debug 特型,也就是用 {:?} 格式化值时要求的特型。
通过在 I::Item 上设置一个限界来做到这一点:
use std::fmt::Debug;
fn dump<I>(iter: I)
where I: Iterator, I::Item: Debug
{
...
}
或者“I必须是针对 String 值的迭代器”:
fn dump<I>(iter: I)
where I: Iterator<Item=String>
{
...
}
Iterator 本身就是一个特型。
迭代器 是迄今为止使用关联类型的最主要场景
但当特型需要包含的不仅仅是方法的时候,关联类型会很有用。
泛型特型(或运算符重载的工作原理)
Rust乘法使用此特型:
/// std::ops::Mul,用于标记支持`*`(乘号)的类型的特型
pub trait Mul<RHS> {
/// 在应用了`*`运算符后的结果类型
type Output;
/// 实现`*`运算符的方法
fn mul(self, rhs: RHS) -> Self::Output;
}
Mul 是一个泛型特型。类型参数 RHS 是右操作数(right-hand side)的缩写。
Mul、Mul、Mul 等都是不同的特型
泛型特型在涉及孤儿规则时会得到特殊豁免:你可以为外部类型实现外部特型, 只要特型的类型参数之一是当前 crate 中定义的类型即可
默认值:
pub trait Mul<RHS=Self> {
...
}
语法 RHS=Self
表示 RHS 默认为 Self。如果我写下 impl Mul for Complex
,而不指定 Mul 的类型参数,则表示 impl Mul<Complex> for Complex
。在类型限界中,如果我写下 where T: Mul
,则表示 where T: Mul<T>
。
impl Trait
use std::iter;
use std::vec::IntoIter;
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) ->
iter::Cycle<iter::Chain<IntoIter<u8>, IntoIter<u8>>> { //返回值难读
v.into_iter().chain(u.into_iter()).cycle()
}
//我们可以很容易地用特型对象替换这个“丑陋的”返回类型:
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> Box<dyn Iterator<Item=u8>> {
Box::new(v.into_iter().chain(u.into_iter()).cycle())
}
如果仅仅是为了避免“丑陋的”类型签名,就要在每次调 用这个函数时承受动态派发和不可避免的堆分配开销,可不太划算【代码量节省 换来了 运行慢】
impl Trait 允许我们“擦除”返回值的类型,仅指定它实现的一个或多个特型, 而无须进行动态派发或堆分配
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> impl Iterator<Item=u8> {
v.into_iter().chain(u.into_iter()).cycle()
}
cyclical_zip 的签名中再也没有那种带着迭代器组合结构的嵌套类型 了,而只是声明它会返回某种 u8 迭代器
使用 impl Trait 意味着你将来可以更改返回的实际类型,只要返回类型仍然会实现 Iterator
trait Shape {
fn new() -> Self;
fn area(&self) -> f64;
}
fn make_shape(shape: &str) -> impl Shape {
match shape {
"circle" => Circle::new(),
"triangle" => Triangle::new(), // 错误:不兼容的类型
"shape" => Rectangle::new(),
}
}
impl Trait 是一种静态派发形式,因此编译器必须在编译期就知道从函数返回的类型,以便在栈上分配正 确的空间数量并正确访问该类型的字段和方法
Rust 不允许特型方法使用 impl Trait 作为返回值 比如:impl Shape
用在带泛型参数的函数中:
fn print<T: Display>(val: T) {
println!("{}", val);
}
//它与使用 impl Trait 的版本完全相同:
fn print(val: impl Display) {
println!("{}", val);
}
使用泛型时允许函数的调用者指定泛型参数的类型,比如 print::<i32>(42)
,而如果使用 impl Trait 则不能这样做。
特型的关联常量
trait Greet {
const GREETING: &'static str = "Hello";
fn greet(&self) -> String;
}
// 可以只声明 不定义 是为了:特型的实现者可以定义这些值:
trait Float {
const ZERO: Self;
const ONE: Self;
}
impl Float for f32 {
const ZERO: f32 = 0.0;
const ONE: f32 = 1.0;
}
关联常量不能与特型对象一起使用
因为为了在编译期选择正确的值, 编译器会依赖相关实现的类型信息。
即使是没有任何行为的简单特型(如 Float),也可以提供有关类型的足够信 息,再结合一些运算符,以实现像斐波那契数列这样常见的数学函数
fn fib<T: Float + Add<Output=T>>(n: usize) -> T {
match n {
0 => T::ZERO,
1 => T::ONE,
n => fib::<T>(n - 1) + fib::<T>(n - 2)
}
}
逆向工程求限界
fn dot(v1: &[i64], v2: &[i64]) -> i64 {
let mut total = 0;
for i in 0 .. v1.len() {
total = total + v1[i] * v2[i];
}
total
}
扩展到浮点数:
fn dot<N>(v1: &[N], v2: &[N]) -> N {
let mut total: N = 0;
for i in 0 .. v1.len() {
total = total + v1[i] * v2[i];
}
total
}
编译失败:Rust 会报错说乘法(*)的使用以及 0 的类型有问题。我们可以使用 Add 和 Mul 的特型要求 N 是支持 + 和 * 的类型。但是,对 0 的用法需要改 变,因为 0 在 Rust 中始终是一个整数,对应的浮点值为 0.0。幸运的是,对于 具有默认值的类型,有一个标准的 Default
特型。对于数值类型,默认值始终 为 0:
use std::ops::{Add, Mul};
fn dot<N: Add + Mul + Default>(v1: &[N], v2: &[N]) -> N {
let mut total = N::default();
for i in 0 .. v1.len() {
total = total + v1[i] * v2[i];
}
total
}
编译失败:
我们需要以某种方式 让 Rust 知道这个泛型函数只适用于那些支持正常乘法规范的类型,其中 N * N 一定会返回 N
将 Mul 替换为 Mul<Output=N>
来做到这一点:
fn dot<N>(v1: &[N], v2: &[N]) -> N
where N: Add<Output=N> + Mul<Output=N> + Default + Copy // 这里必须支持Copy类型
{
...
}
标准库中没有那么一个 Number 特型包含我们想要使用的所有运算符和方法,为什么 Rust 的设计者不让泛型更像 C++ 模板中 的“鸭子类型”那样在代码中隐含约束呢?
“鸭子类型”源自一句格言:“如果它走起来像鸭子,叫起来也像鸭子,那么它很可能就是一只鸭子。” 在编程中,这意味着如果你有一个对象,它可以响应你调用的方法,那么你就可以把它当作那种类型的对象来使用,而不必关心它的实际类型。
Rust 的这种方式的
- 一个优点是泛型代码的前向兼容性。你可以更改公共泛型函 数或方法的实现,只要没有更改签名,对它的用户就没有任何影响。
- 类型限界的另一个优点是,当遇到编译器错误时,至少编译器可以告诉你问题出在哪里。