【2025 Rust学习 --- 09 特型和泛型】

特型和泛型

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 WriteT <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 Filesay_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 的这种方式的

  • 一个优点是泛型代码的前向兼容性。你可以更改公共泛型函 数或方法的实现,只要没有更改签名,对它的用户就没有任何影响。
  • 类型限界的另一个优点是,当遇到编译器错误时,至少编译器可以告诉你问题出在哪里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/946208.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【开源免费】基于Vue和SpringBoot的网上商城系统(附论文)

本文项目编号 T 129 &#xff0c;文末自助获取源码 \color{red}{T129&#xff0c;文末自助获取源码} T129&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…

使用Locust对MySQL进行负载测试

1.安装环境 pip install locust mysql-connector-python 2.设置测试环境 打开MySQL服务 打开Navicat新建查询&#xff0c;输入SQL语句 3.编写locust脚本 load_mysql.py # codingutf-8 from locust import User, TaskSet, task, between import mysql.connector import ran…

MF248:复制工作表形状到Word并调整多形状位置

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

极品飞车6的游戏手柄设置

极品飞车&#xff0c;既可以用键盘来控制车辆的前进、后退、左转、右转、加速与减速&#xff0c;也可以使用游戏手柄来操作车辆的运行。需要注意的是&#xff0c;极品飞车虽然支持手柄&#xff0c;但是仅支持常见的北通、罗技还有部分Xbox系列的手柄&#xff0c;至于其他的PS4手…

2025元旦源码免费送

我们常常在当下感到时间慢&#xff0c;觉得未来遥远&#xff0c;但一旦回头看&#xff0c;时间已经悄然流逝。对于未来&#xff0c;尽管如此&#xff0c;也应该保持一种从容的态度&#xff0c;相信未来仍有许多可能性等待着我们。 免费获取源码。 更多内容敬请期待。如有需要可…

【CSS in Depth 2 精译_095】16.3:深入理解 CSS 动画(animation)的性能

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第五部分 添加动效 ✔️【第 16 章 变换】 ✔️ 16.1 旋转、平移、缩放与倾斜 16.1.1 变换原点的更改16.1.2 多重变换的设置16.1.3 单个变换属性的设置 16.2 变换在动效中的应用 16.2.1 放大图标&am…

通过Cephadm工具搭建Ceph分布式存储以及通过文件系统形式进行挂载的步骤

1、什么是Ceph Ceph是一种开源、分布式存储系统&#xff0c;旨在提供卓越的性能、可靠性和可伸缩性。它是为了解决大规模数据存储问题而设计的&#xff0c;使得用户可以在无需特定硬件支持的前提下&#xff0c;通过普通的硬件设备来部署和管理存储解决方案。Ceph的灵活性和设计…

Mac连接云服务器工具推荐

文章目录 前言步骤1. 下载2. 安装3. 常用插件安装4. 连接ssh测试5. 连接sftp测试注意&#xff1a;ssh和sftp的区别注意&#xff1a;不同文件传输的区别解决SSL自动退出 前言 Royal TSX是什么&#xff1a; Royal TSX 是一款跨平台的远程桌面和连接管理工具&#xff0c;专为 mac…

xterm + vue3 + websocket 终端界面

xterm.js 下载插件 // xterm npm install --save xterm// xterm-addon-fit 使终端适应包含元素 npm install --save xterm-addon-fit// xterm-addon-attach 通过websocket附加到运行中的服务器进程 npm install --save xterm-addon-attach <template><div :…

[2025] 如何在 Windows 计算机上轻松越狱 IOS 设备

笔记 1. 首次启动越狱工具时&#xff0c;会提示您安装驱动程序。单击“是”确认安装&#xff0c;然后再次运行越狱工具。 2. 对于Apple 6s-7P和iPad系列&#xff08;iOS14.4及以上&#xff09;&#xff0c;您应该点击“Optinos”并勾选“允许未经测试的iOS/iPadOS/tvOS版本”&…

网页排名:PageRank 算法的前世今生

PageRank算法全解析&#xff1a;从理论到实践 引言 PageRank 是由拉里佩奇&#xff08;Larry Page&#xff09;和谢尔盖布林&#xff08;Sergey Brin&#xff09;在1996年发明的一种链接分析算法&#xff0c;最初用于Google搜索引擎来评估网页的重要性。该算法通过模拟随机浏览…

嵌入式开发之使用 FileZilla 在 Windows 和 Ubuntu 之间传文件

01-FileZilla简介 FileZilla 是一个常用的文件传输工具&#xff0c;它支持多种文件传输协议&#xff0c;包括以下主要协议&#xff1a; FTP (File Transfer Protocol) 这是 FileZilla 最基本支持的协议。FTP 是一种明文传输协议&#xff0c;不加密数据&#xff08;包括用户名和…

Jmeter的安装与使用

1.下载压缩包&#xff0c;并解压到本地 2.在bin目录下找到jmeter.bat双击打开图形化界面 3.在测试计划上点击右键添加一个线程组 4.可以自定义线程数&#xff0c;Ramp_Up表示在该时间内将一组线程将运行完毕&#xff0c;循环次数可自定义 5.在线程组点击右键添加配置元件…

pycharm pytorch tensor张量可视化,view as array

Evaluate Expression 调试过程中&#xff0c;需要查看比如attn_weight 张量tensor的值。 方法一&#xff1a;attn_weight.detach().numpy(),view as array 方法二&#xff1a;attn_weight.cpu().numpy(),view as array

XIAO ESP32 S3网络摄像头——2视频获取

本文主要是使用XIAO Esp32 S3制作网络摄像头的第2步,获取摄像头图像。 1、效果如下: 2、所需硬件 3、代码实现 3.1硬件代码: #include "WiFi.h" #include "WiFiClient.h" #include "esp_camera.h" #include "camera_pins.h"// 设…

数据仓库中的指标体系模型介绍

数据仓库中的指标体系介绍 文章目录 数据仓库中的指标体系介绍前言什么是指标体系指标体系设计有哪些模型?1. 指标分层模型2. 维度模型3. 指标树模型4. KPI&#xff08;关键绩效指标&#xff09;模型5. 主题域模型6.平衡计分卡&#xff08;BSC&#xff09;模型7.数据指标框架模…

K3知识点

提示&#xff1a;文章 文章目录 前言一、顺序队列和链式队列题目 顺序队列和链式队列的定义和特性实际应用场景顺序表题目 链式队列 二、AVL树三、红黑树四、二叉排序树五、树的概念题目1左子树右子树前序遍历、中序遍历&#xff0c;后序遍历先根遍历、中根遍历左孩子右孩子题目…

jQuery学习笔记1

// jQuery的入口函数 // 1.等着DOM结构渲染完毕即可执行内部代码&#xff0c;不必等到所以外部资源加载完毕&#xff0c;jQuery帮我们完成了封装 // 相当于原生js中的DOMContentLoaded <script src"./jquery.min.js"></script> <style>div {width…

HTML——41有序列表

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>有序列表</title></head><body><!--有序列表&#xff1a;--><!--1.列表中各个元素在逻辑上有先后顺序&#xff0c;但不存在一定的级别关系-->…

典型常见的基于知识蒸馏的目标检测方法总结二

来源&#xff1a;https://github.com/LutingWang/awesome-knowledge-distillation-for-object-detection收录的方法 NeurIPS 2017&#xff1a;Learning Efficient Object Detection Models with Knowledge Distillation CVPR 2017&#xff1a;Mimicking Very Efficient Networ…