为什么Rust会有关联的类型,它们与类型实参又叫泛型有什么区别,因为它们是如此相似? 像所有普通语言一样,仅后者还不够吗? 对于刚开始学习Rust的人,尤其是对于那些来自其他语言的人(“这是泛型!”-多年以来最明智的人会说),经常会出现这样的问题。 让我们做对。
TL; DR前者控制被调用的代码,后者控制调用者。
泛型与关联类型
因此,我们已经有了类型参数,或者每个人都喜欢的泛型。 看起来像这样:
trait Foo<T> { fn bar(self, x: T); }
在这里T
恰好是类型参数。 似乎这对每个人都足够了(例如640 KB的内存)。 但是在Rust中,也有关联的类型,如下所示:
trait Foo { type Bar;
乍一看,相同的卵,但是角度不同。 为什么需要在语言中引入另一个实体? (顺便说一下,这不是该语言的早期版本。)
类型实参恰好是实参 ,这意味着它们将被传递给调用位置的trait,并控制使用哪种类型代替T
属于调用方。 即使我们没有在调用位置明确指定T
,编译器也会使用类型推断为我们完成此操作。 也就是说,无论如何,隐式地,此类型将在调用方上推断并作为参数传递。 (当然,所有这些都发生在编译期间,而不是在运行时。)
考虑一个例子。 标准库具有AsRef AsRef
,该AsRef
允许一种类型在一段时间内假装为另一种类型,将自身的链接转换为其他链接。 简化后,此特征看起来像这样(实际上,它有点复杂,我特意删除了所有不必要的内容,仅保留了理解所需的最低限度):
trait AsRef<T> { fn as_ref(&self) -> &T; }
在这里,类型T
由调用方作为参数传递,即使它隐式发生(如果编译器为您推断出该类型)。 换句话说,由调用者决定哪个新类型T
将假装为我们的类型,以实现此特征:
let foo = Foo::new(); let bar: &Bar = foo.as_ref();
在这里,使用bar: &Bar
知识的编译器将使用AsRef<Bar>
实现来调用as_ref()
方法,因为它是调用者所需的Bar
类型。 毋庸置疑, Foo
类型必须实现AsRef AsRef<Bar>
,除此之外,它还可以实现许多其他AsRef<T>
选项,其中调用者从中选择所需的选项。
对于关联类型,一切都完全相反。 关联的类型完全由实现此特征的人员控制,而不是由调用方控制。
一个常见的例子是迭代器。 假设我们有一个集合,并且我们想从中获得一个迭代器。 迭代器应返回哪种类型的值? 正是这个集合中包含的一个! 由调用者决定迭代器将返回什么,并且迭代器本人更清楚他确切知道如何返回的信息。 这是标准库中的缩写代码:
trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }
请注意,迭代器没有允许调用者选择迭代器应返回什么的类型参数。 相反, next()
方法返回的值的类型由迭代器本身使用关联的类型确定,但不会被钉子卡住,即 每个迭代器实现都可以选择其类型。
停下 那又怎样 都一样,尚不清楚为什么它比通用更好。 想象一下,我们使用通常的泛型而不是关联的类型。 迭代器的特征将如下所示:
trait GenericIterator<T> { fn next(&mut self) -> Option<T>; }
但是现在,首先,需要在提到迭代器的每个位置都一次又一次地指示类型T
,其次,现在已经可以用不同的类型多次实现此特征,这对于迭代器来说似乎有些奇怪。 这是一个例子:
struct MyIterator; impl GenericIterator<i32> for MyIterator { fn next(&mut self) -> Option<i32> { unimplemented!() } } impl GenericIterator<String> for MyIterator { fn next(&mut self) -> Option<String> { unimplemented!() } } fn test() { let mut iter = MyIterator; let lolwhat: Option<_> = iter.next();
看到了吗? 我们不能只蹲不坐就调用iter.next()
-我们需要让编译器显式或隐式地知道将返回哪种类型。 它看起来很尴尬:为什么在调用方我们应该知道(并告诉编译器!)迭代器将返回的类型,而该迭代器应该更清楚其返回的类型? 所有这些都是因为我们能够为同一个MyIterator
使用不同的参数两次实现GenericIterator GenericIterator
,从迭代器语义的角度来看,这也很荒谬:为什么同一个迭代器可以返回不同类型的值?
如果我们返回具有关联类型的变量,那么可以避免所有这些问题:
struct MyIter; impl Iterator for MyIter { type Item = String; fn next(&mut self) -> Option<Self::Item> { unimplemented!() } } fn test() { let mut iter = MyIter; let value = iter.next(); }
在这里,首先,编译器将正确地输出value: Option<String>
类型而没有不必要的单词,其次,它将无法使用不同的返回类型第二次实现MyIter
的Iterator
MyIter
,从而破坏了一切。
用于固定。 集合可以实现这样的特征,以便能够将自身变成迭代器:
trait IntoIterator { type Item; type IntoIter: Iterator<Item=Self::Item>; fn into_iter(self) -> Self::IntoIter; }
同样,这里是集合,它将决定迭代器是什么,即:一个迭代器,其返回类型与集合本身中元素的类型匹配,而没有其他类型。
手指上更多
如果上面的示例仍然难以理解,那么这里的解释就不那么科学了,但更容易理解。 类型参数可以视为我们为特征提供的“输入”信息。 关联的类型可以被视为特征提供给我们的“输出”信息,以便我们可以使用其工作结果。
标准库具有为其类型(加法,减法,乘法,除法等)重载数学运算符的能力。 为此,您需要实现标准库中的相应特征之一。 例如,在这里,此特征如何寻找加法运算(再次简化):
trait Add<RHS> { type Output; fn add(self, rhs: RHS) -> Self::Output; }
在这里,我们有“输入” RHS
参数-这是将对我们的类型应用加法运算的类型。 并且有一个“输出”参数Add::Output
这是加法产生的类型。 在一般情况下,它可以与术语的类型不同,后者又可以是不同的类型(为蓝色添加美味并变软-但是,我一直在这样做)。 第一个使用type参数指定,第二个使用关联的类型指定。
您可以使用不同类型的第二个参数来实现任意数量的加法,但是每次只会有一种类型的结果,并且由该加法的实现来确定。
让我们尝试实现此特征:
use std::ops::Add; struct Foo(&'static str); #[derive(PartialEq, Debug)] struct Bar(&'static str, i32); impl Add<i32> for Foo { type Output = Bar; fn add(self, rhs: i32) -> Bar { Bar(self.0, rhs) } } fn test() { let x = Foo("test"); let y = x + 42;
在此示例中,变量y
的类型由加法算法而不是调用代码确定。 如果有可能写出let y: Baz = x + 42
这样的东西,那将是非常奇怪的let y: Baz = x + 42
,即,强制加法操作返回某种无关类型的结果。 正是基于这种情况,关联类型Add::Output
我们保证了。
合计
我们在不介意为同一类型具有多个特征实现的地方使用泛型,并且可以在调用端指示特定的实现。 我们使用关联的类型来获取一个“规范的”实现,该实现本身控制这些类型。 如上例所示,按正确的比例合并和混合。
硬币失败了吗? 用评论杀死我。