Beginng_Rust(译):使用特征(Traits)(第十八章)

在本章中,您将学习:

•调用泛型函数时,traits如何避免难以理解的编译器错误消息

•泛型参数的边界如何可以是单一的,或者可以在几个特征中分解

•traits如何为其包含的函数创建范围

•如何使用“self”关键字创建可使用“点符号”调用的函数,语法更简单

•如何使用标准库特征,如“显示”特征

•迭代如何只是一个特征

•如何定义类型别名

•如何定义泛型迭代器

•如何使用关联类型来简化通用迭代器的使用

•如何定义自己的迭代器

特征(Traits)的需要

假设我们需要一个函数来计算数学第四根,名为“四次根”。 利用sqrt标准库函数,它计算应用它的数字的平方根,我们可以写:

fn quartic_root(x: f64) -> f64 { x.sqrt().sqrt() }
let qr = quartic_root(100f64);
print!("{} {}", qr * qr * qr * qr, qr);

这将打印:“100.00000000000003 3.1622776601683795”。

但是我们还需要一个函数来计算32位浮点数的四次根,而不是将它们转换为f64类型。 利用f32也有sqrt函数的事实,我们可以写:

fn quartic_root_f64(x: f64) -> f64 { x.sqrt().sqrt() }
fn quartic_root_f32(x: f32) -> f32 { x.sqrt().sqrt() }
print!("{} {}",
quartic_root_f64(100f64),
quartic_root_f32(100f32));

这将打印:“3.1622776601683795 3.1622777”。

但是,我们可以尝试编写一个泛型函数,而不是编写类似的函数,只有它们的参数和变量的类型不同,如下所示:

fn quartic_root<Number>(x: Number) -> Number {
x.sqrt().sqrt()
}
print!("{} {}",
quartic_root(100f64),
quartic_root(100f32));

但是这段代码是非法的,会产生编译错误“在当前范围内找不到类型为Number的名为sqrt的方法”。 这意味着在表达式“x.sqrt()”中,表达式“x”是“Number”泛型类型,并且这种类型没有sqrt适用的函数。 实际上,刚刚定义了类型Number,因此它几乎没有适用的功能。

在这方面,Rust与C ++不同。 在后一种语言中,我们可以编写以下代码,其中函数模板对应于我们的泛型函数:

#include <iostream>
#include <cmath>
template <typename Number>
Number quartic_root(Number x) {
return sqrt(sqrt(x));
}

int main() {
std::cout << quartic_root((float)100)
<< " " << quartic_root((double)100);
}

即使在C ++代码中,当编译器第一次遇到“sqrt”的调用时,“Number”泛型类型也没有适用的函数,因此编译器无法知道是否允许这样的语句。 但是当遇到“quartic_root”函数的两次调用时,编译器会生成两个具体函数“quartic_root ”和“quartic_root ”。 这称为“通用函数实例化”或“函数单形化”。 这种实例化检查对于“浮动”和“双”具体类型,“sqrt”函数是适用的。

当出现编程错误时,会出现C ++解决方案的缺点,如下所示:

#include <iostream>
#include <cmath>
template <typename Number>
Number quartic_root(Number x) {
return sqrt(sqrt(x));
}
int main() {
std::cout << quartic_root("Hello");
}

当C ++编译器尝试为“const char *”类型实例化“quartic_root”函数时,它是表达式“Hello”的类型,它需要生成其签名为“sqrt(const char *)的函数的调用”。)”。 但是没有这样的函数声明,因此编译器发出编译错误抱怨该缺失函数。

缺点是通常“quartic_root”函数声明由一个开发人员(或开发人员组织)编写,并且该函数的调用由另一个开发人员(或开发人员组织)编写。 调用传递字符串而不是数字的函数的开发人员希望得到类似的错误消息

“在第10行的quartic_root调用中,你不能传递字符串”,而是在C ++中,你必须得到一条错误信息,例如“在第6行,你不能在数字上应用sqrt,其中Number是一个字符串,当这个 函数从第10行调用“。

如果你不知道如何实现quartic_root,那么C ++消息有点模糊。 这是一个非常简单的例子。 在现实世界的C ++代码中,泛型函数调用中类型错误的错误消息往往非常模糊,因为它们讨论的是属于库实现的变量,函数和类型,而不是它的接口。 要了解它们,仅仅很好地了解API是不够的; 需要知道整个库的实现。

救援的特征(Traits to the Rescue)

对于像这样的简单情况,避免这种缺点的Rust技术比C ++技术更复杂,但它为更复杂的情况创建了更清晰的错误消息,如真实软件。

trait HasSquareRoot {
fn sq_root(self) -> Self;
}
impl HasSquareRoot for f32 {
fn sq_root(self) -> Self { f32::sqrt(self) }
}
impl HasSquareRoot for f64 {
fn sq_root(self) -> Self { f64::sqrt(self) }
}
fn quartic_root<Number>(x: Number) -> Number
where Number: HasSquareRoot {
x.sq_root().sq_root()
}
print!("{} {}",
quartic_root(100f64),
quartic_root(100f32));

这将打印:“3.1622776601683795 3.1622777”。

第一个语句是名为“HasSquareRoot”的“特征”的声明,其中包含名为“sq_root”的函数的签名。 Rust特征是函数签名的容器; 在这种情况下,它只包含一个签名。 特征的含义是使用某些功能的能力。 “HasSquareRoot”特征的含义是“sq_root”函数可以在具有“HasSquareRoot”功能的每个类型上调用,或者,如通常所说,每种类型都满足“HasSquareRoot”特性。

但哪种类型满足“HasSquareRoot”特性? 嗯,没有人这样做,因为我们只是定义了这个特征,任何类型都不满足任何特征,除了那些已被宣布满足它的特征。

因此,上面示例中的下两个语句使“f32”和“f64”类型满足该特征。 换句话说,使用这些“impl”语句,调用“sq_root”函数的功能被赋予“f32”和“f64”类型。 为此目的,引入了另一种语句,其中“impl”关键字是“实现”的简写。

那些“impl”语句意味着“HasSquareRoot”特征,它只是一个编程接口或API,在这里由指定的代码为指定的类型实现。 当然,“impl”语句中包含的函数的签名与前一个“trait”语句中包含的签名相同; 但他们也有一个身体,因为他们是这种签名的实现。 用C ++术语来说,它们是前一个函数声明的定义。

Rust特征类似于Java或C#接口,或者是没有数据成员的抽象类。

由于前三个语句,我们有一个新的特征和两个现有类型,现在实现这样的特征。

第四个语句是“quartic_root”泛型函数的声明,由“Number”泛型类型参数化。 但是,这样的声明有一个新的部分:在签名结束时,有一个子句“where Number:HasSquareRoot”。 这样的子句被命名为“trait bound”,它是函数签名的一部分。 它字面意思是“数字”泛型类型必须实现“HasSquareRoot”特征。

函数签名是调用函数的代码和函数体中的代码之间的一种契约,因此它们的“where”子句是该契约的一部分。

对于调用该函数的代码,这样的“where”子句意味着“在调用此函数时,必须确保为Number类型参数传递的类型实现HasSquareRoot特征”。 例如,“100f32”和“100f64”表达式分别是“f32”和“f64”类型,并且这两种类型都实现“HasSquareRoot”特性,因此它们是有效参数。 但是,如果用“quartic_root(”Hello“))替换程序的最后一行;”,那么表达式“Hello”的类型,即“&str”,不会实现“HasSquareRoot”特征,并且 所以你违反了合同。 实际上,你得到编译错误“trait bound&str:main :: HasSquareRoot不满意”。

尝试计算字符串的四次根是没有意义的,但即使用“quartic_root(81i32));”替换程序的最后一行,也会出现编译错误。 它的消息是“特征绑定i32:main :: HasSquareRoot不满意”。 这是因为i32类型尚未实现“HasSquareRoot”特性,无论此类操作的合理性如何。 如果您认为这样的特性对于其他类型是值得的,您可以为它们实现它。

相反,如果函数体中的代码看到了契约,那么“where”子句意味着“当调用此函数时,确保为”Number“类型参数传递的类型实现”HasSquareRoot“特征,所以你可以使用属于这种特征的每个功能,但没有其他功能“。例如,在函数体中,“x.sq_root()”表达式是有效的,因为“x”表达式是“Number”泛型类型,并且声明这样的类型实现“HasSquareRoot”特性,并且该特征包含“sq_root”函数,因此“x”变量可以使用这样的函数。但是如果你用“x.abs()”替换那个主体,如果“x”是“f64”或“f32”类型,这将是一个有效的语句,你得到编译错误:“找不到名为abs的方法对于当前范围中的“Number”类型“。这意味着“x”表达式(“Number”泛型类型)无法调用“abs”函数。实际上,在函数体内部,不知道“x”是“f64”还是“f32”;它只是一个“数字”,而“数字”能够做的就是“HasSquareRoot”特征中包含的内容,即“sq_root”函数。

没有特征界限的通用函数

在泛型函数的声明中,如果没有“where”子句,或者在“where”子句中没有引用类型参数,则没有特性与该类型相关联,因此您可以对一个对象做很少的事情 那个泛型类型。 你只能这样做:

fn _f1<T>(a: T) -> T { a }
fn _f2<T>(a: T) -> T {
let b: T = a;
let mut c = b;

c = _f1(c);
c
}
fn _f3<T>(a: &T) -> &T { a }

使用无界类型参数“T”的值,您只能:

•通过值或引用将其作为函数参数传递;

•从函数,值或参考中返回;

•声明,初始化或分配局部变量。 相反,即使是繁琐的以下代码也会导致编译错误:

fn g(a: i32) { }
fn f<T>(a: T) -> bool {
g(a);
a == a
}

第三行是非法的,因为“g”需要具有“i32”类型的值,而“a”可以具有任何类型。 并且第四行是非法的,因为“T”类型可能无法进行相等性比较。

因此,几乎总是使用泛型函数的类型参数的特征界限。

在此程序中使用了一个非常罕见的重要通用标准库函数,它不需要特征限制:

let mut a = 'A';
let mut b = 'B';
print!("{}, {}; ", a, b);
std::mem::swap(&mut a, &mut b);
print!("{}, {}", a, b);

它将打印:“A,B; B,A”。
“swap”泛型函数可以交换具有相同类型的任何两个对象的值。 它的签名是:“fn swap (x:&mut T,y:&mut T)”。

特征作用域

我们的特性包含一个名为“sq_root”的函数,以表明它与“sqrt”标准库函数不同。 “sq_root”函数的两个实现使用标准库“sqrt”函数,但它们是其他函数。 但是,我们也可以将该函数命名为“sqrt”,获取此等效的有效程序:

fn sqrt() {}
trait HasSquareRoot {
fn sqrt(self) -> Self;
}
impl HasSquareRoot for f32 {
fn sqrt(self) -> Self { f32::sqrt(self) }
}
impl HasSquareRoot for f64 {
fn sqrt(self) -> Self { f64::sqrt(self) }
}
fn quartic_root<Number>(x: Number) -> Number
where Number: HasSquareRoot {
x.sqrt().sqrt()
}
sqrt();

print!("{} {}",
quartic_root(100f64),
quartic_root(100f32));

请注意,现在我们对名称“sqrt”有四种不同的用法:

•第六行中使用的“f32 :: sqrt”是指在标准库中声明的函数,它与“f32”类型相关联。 它计算具有“f32”类型的值的平方根,并返回具有“f32”类型的值。

•第9行中使用的“f64 :: sqrt”是指在标准库中声明的函数,它与“f64”类型相关联。 它计算具有“f64”类型的值的平方根,并返回具有“f64”类型的值。

•“fn sqrt(self) - > Self”是“HasSquareRoot”特征的一种方法,实现此特征的所有类型必须具有相同签名的方法。 它的签名在第三行中声明,并在第六行和第九行中实现。

•“fn sqrt()”是“HasSquareRoot”特征之外的本地函数,它与它无关。 它在第一行声明,并在第十五行中调用。 顺便说一下,它什么也没做。

您知道Rust不允许在同一范围内使用两个具有相同名称的函数。 不过上面的代码是有效的; 这意味着这四个具有相同名称的函数属于四个不同的范围。

猜你喜欢

转载自blog.csdn.net/m0_37696990/article/details/82955814