rust用特征和泛型来支持多态。特征类似其他语言的接口和抽象类。用于写字节的特征是std::io::Write,标准类型File和TcpStream都实现了std::io::Write。
一个特征的共享引用和可变引用表示任意实现了这个特征的类型值。
我们可以用特征给已存在的类型扩展方法,包括内建的类型如str和bool。给一个类型添加特征实现不耗费额外的内存。内建的特征提供操作符重载和其它特征的钩子操作。Self类型、关联方法和关联类型3种特征可以优雅地解决其他语言需要变通和hack才能解决的问题。
泛型是多态的另一种特性,类似c++模板,一个泛型函数或者泛型函数可以提供各种类型的值。
意思是一个值的类型T必须实现Ord特征,任意的可排序类型。编译器会为你实际使用的每种类型T生成机器代码。
一个实现std::io::Write的类型可以写字节;一个实现std::iter::Iterator的类型可以产生一系列的值;一个实现std::clone::Clone的类型可以克隆自己生成一个内存拷贝;一个实现std::fmt::Debug的类型可以用{:?}格式符打印。
std::fs::File实现了Write特征用于写本地文件,std::net::TcpStream用于写网络数据。Vec也实现了Write特征,它的.write()方法用于在向量后面追加数据。
Range区间类型实现了Iterator特征,作为迭代类型关联到其他集合类型如切片和哈希表。
大多数标准库类型实现了Clone特征,少数类型比如TcpStream表示的不仅仅是内存数据,TcpStream没有实现Clone特征。
同样大多数标准库类型实现了Deug特征。
特征的方法有个不寻常的规则,特征必须被引用到在当前域,否则它的方法是隐藏状态不可用。rust有这条规则是因为我们可以通过特征给任意类型添加方法,甚至是标准库类型比如u32和str,三方库也可以做同样的事情,如果不限制,可能会导致命名冲突。Clone和Iterator的方法在代码里可以直接用,但是我们并没有显式地引用这些特征,因为这些特征是默认导入的,它们是预包含的一部分,rust会自动为每个模块默认导入一些特征。这些预包含的特征都是经过仔细挑选的。
用特征实现多态有两种方法:特征对象和泛型。
rust不允许变量类型是特征,因为这种变量的大小在编译阶段是不知道的。如果你用过C#或者java,会对这个限制很惊讶。但是原因很简单,在java中一个变量的类型是接口,它仅仅是一个实现了该接口的类型的值的引用。我们也想在rust中也用相同的语法,但是rust中引用是显式的,解决的办法是用特征的引用,被称作特征对象。类似普通引用,特征对象指向一个值,它有生命周期,可以是可变引用或者共享引用。
特征对象的不同之处是它在编译期间并不知道目标类型是什么,因此特征对象包含了一点儿引用类型的额外信息。
你不可以根据特征对象查询目标类型的信息,rust不支持。
在内存中,特征对象是一个肥指针。包含指向值的指针和指向值类型的虚表。每个特征对象都占用两个地址空间的大小。C++也有这种运行时信息,它被称作虚表。在rust中和C++一样,虚表在编译期生成一次,被同一类型的所有值共享。虚表是rust语言的内部细节,对使用者不可见,当你对一个特征对象调用方法的时候,语言自动使用虚表以决定调用哪个具体的实现。在C++中,虚表指针作为结构体的一部分存在,rust则用肥指针。结构体只包含它定义的成员,不包含其他任何东西。这样结构体就可以实现大量的特征,但是并不需要占用大量的虚表指针空间。甚至是i32这种基础类型没有足够的空间存储虚表指针也可以实现特征。
rust会在需要的时候自动转换普通引用成特征对象。对于智能指针Box类型也是一样,会自动转换Box普通引用成Box特征对象。Box特征对象也是肥指针,包含目标值的地址和虚表的地址。其他指针类型也是一样的,如Rc指针。这种类型的转换是创建特征对象的唯一方式,计算机处理起来非常简单,当程序运行到需要转换的地方,rust知道引用的真实类型,它只需要找到合适的虚表然后把虚表指针追加到普通指针后面就变成了肥指针了。
泛型函数
泛型函数的类型参数习惯上用一个大写字母来表示。调用泛型函数的时候编译器会根据参数值自动推断泛型的类型,可以省略掉泛型参数。但是如果泛型函数并不需要参数,就没办法推断了,这时需要显式地写出来。
如果泛型需实现多个特征,多个特征之间用+号连起来。一个泛型类型没有任何特征约束也是可以的,但是你就不能对它的值做太多事情,因为它可以是任意类型。泛型函数可以有多个泛型参数,多个参数用逗号隔开。当泛型参数的声明太长太复杂的时候可以把泛型参数的约束放到where子句中。where子句放在函数签名和函数体中间。这种where子句也可以用在泛型结构体,泛型枚举体,类型别名,泛型方法等任何需要泛型约束的地方。泛型函数可能还有生命周期参数,这时生命周期参数写在前面。
生命周期参数对生成机器代码没有任何影响,它只是编译期的概念。对泛型函数提供相同的类型,但是不同的生命周期参数,调用的时候执行相同的代码。只有不同的类型会使编译器编译多个函数副本。
当然泛型函数并不是唯一一种可以用泛型的代码,结构体的方法也可以是泛型的,即使定义它的结构体类型不是泛型的。类型别名定义也可以是泛型的。泛型约束,where子句,生命周期参数等语法可以用在其他泛型代码中,不仅仅是泛型函数。
使用特征对象还是泛型
当你需要一个混合类型的集合的时候,只能用特征对象,当你对集合用泛型约束的时候集合里的每一个值都必须是相同的类型。
另一个使用特征对象的场景是当你需要减小编译后的机器代码的尺寸的时候。rust需要为泛型的每一种使用类型编译一个机器代码副本,这会使编译出来的二进制文件变大。
泛型相对于特征对象有两点优势,泛型是普遍的选择。
一个优势是速度,rust为每种使用的类型编译的机器代码,在运行的时候它知道值的具体类型,因此不需要动态分发。并且编译器可以针对固定类型的代码做优化,比如把函数内联,对常数的调用可以在编译期计算它,在运行时直接使用编译期计算的结果。总之编译器掌握了优化所需的所有信息。
第二个使用泛型的优势是并不是每个特征都支持特征对象,特征包含多种特性,比如静态方法,这种特征只能用于泛型,不能用于特征对象。
std::io::sink()??
定义和实现特征
定义特征很简单,给个名字和多个特征方法的签名。实现特征用impl TraitName for Type,然后加实现的具体细节块。在这个实现的块里实现的每个方法都必须是特征的方法,如果你需要给结构体加一个辅助方法,需要把辅助方法移到别的impl块里。
特征可以提供默认方法,实现特征的类型可以省略默认方法的实现,当然实现相同签名的方法覆盖默认方法也是可以的。
rust允许你为任意类型实现任意特征。用特征给已有类型添加方法叫扩展特征,你甚至可以给泛型添加扩展特征。
serde库定义了一个Serialize特征,它为库支持的所有类型都实现了这个特征,覆盖了所有的标准数据结构。
之前提到,当你实现一个特征,特征和类型必须至少有一个在当前包中,这叫一致性规则,它让rust确保特征的实现是唯一的。
特征里面有一个关键字Self,表示实现特征的具体类型。为哪个类型实现特征,Self就表示哪个类型,而不是只要一个类型实现了该特征,都可以用Self来表示。Self不具有动态特性。Self不适用于特征对象,不能给一个类型为Self的参数传递一个特征对象。
特征对象只能用于简单的特征,可以像接口或者抽象类那样实现的场景。更高级的特性不能和特征对象共存,因为特征对象丢失了类型信息,而rust需要对你的程序做类型检查。
要使一个方法对特征对象友好,可以把参数写成特征对象而不是Self。
特征可以继承,用冒号:加父特征名定义。实现的时候子特征和父特征都要分别实现,用不同的impl块,顺序可以任意。方法重名怎么办??
可否多继承??
子特征像java或者C#里的子接口,可以通过添加方法来扩展一个现有特征。
特征可以包含静态方法和构造函数。对非泛型的代码,静态方法用类型加::语法来调用;对于泛型代码,用泛型类型加::来调用,比如T::new()。
特征对象不支持静态方法,如果你需要在特征里定义静态方法,又要支持特征对象,那么所有的的静态方法都要用where Self:Sized来约束。静态方法里没有用Self呢??
这告诉编译器静态方法对特征对象放行,这样特征对象就支持了,但是它们不支持静态方法,你也可以对其他的不支持特征对象的方法做相同的处理。
完全限定方法调用
方法只是一种特殊的函数,是可以直接调用的,用类型名::方法名调用。这时需要传入该类型的值作为第一个参数,这有点儿像静态方法调用。如果方法是某个特征的方法,还可以用Trait::method(self)或者::method(self)的形式调用。这些调用语法是等同的。最后这种调用叫完全限定方法调用。
在完全限定方法调用中,你明确指出了调用了谁的方法,这在一些特殊的场景很有用。
当类型实现的多个特征中有相同方法名的情况,可以用Trait::method(self)的形式调用。
类型可以有和特征相同的方法但是不是为特征实现的吗?
当self的类型不能推断的时候可以用Type::method(self)调用。比如self是个数字,可以是多种类型。
当把方法作为参数传递给另外一个方法的时候,可以用::method。
当在宏中使用特征方法的时候也需要完全限定方法名。
关联类型
在特征中用type TypeName;定义一个关联类型,每个实现了该特征的类型都要指出它的关联类型是什么类型。
Iterator迭代器有个关联类型Item,在特征内部使用关联类型应该用Self::Item而不是单纯的Item。Self在特征内部显式地使用它的元素(关联类型或者方法)。
在实现的时候在impl块内部用type Item=String;给出关联类型的具体类型。
在泛型代码里,可以用泛型参数加::来使用关联类型。比如I表示实现Iterator的类型,那么I::Item表示Iterator的关联类型Item,也可以用完整的写法::Item,但是经常用简便的写法。在使用特征的时候还可以对特征的关联类型做约束,如I::Item: Debug或者I: Iterator。如果你认为Iterator是所有迭代器的集合,那么Iterator就是一个子集合,一个只生成String值的迭代器集合。这个语法可以出现在任何普通特征可以出现的地方,包括特征对象,使用特征对象的时候特征的关联类型必须指定,不然生成的值(比如迭代器)不知道是什么类型,类型检查无法继续下去。
在一个线程池的库中,一个Task特征可能有一个Output关联类型,一个Pattern特征可能有个Match关联类型。
泛型特征
特征也可以是泛型的,比如Mul就是一个泛型特征,它的实例Mul,Mul,Mul等等是不同的特征,就像min::和min::是不同的函数,Vec和Vec是不同的类型。
每个泛型特征实例可以有自己的关联类型。
语法Mul表示泛型类型RHS默认是Self,假如我写impl Mul for Complex,没有提供泛型类型RHS,那么它等同于impl Mul for Complex。在限定中,假如我写where T:Mul等同于where T:Mul。
在rust中表达式lhs\*rhs是Mul::(lhs,rhs)的简写。所以通过实现Mul特征就可以重载\*操作符。
特征好友
特征好友仅仅是设计了用于一起配合使用的简单特征。
随机数生成器特征Rand的方法rand有个泛型参数R限定为Rng表示随机算法。实现Rng的类型有XorShiftRng是一种快速的伪随机数算法,而OsRng要慢一些,但是是真实的随机算法,一般用于加密算法。
像f64和bool这些类型实现了Rand随机数生成特征,调用的时候传递一个随机数生成器作为参数,f64::rand(rng),bool::rand(rng)。
std::random方法是个泛型方法,传递能生成随机数的类型给它,它实际上什么都没做,它是简单包装了一下,直接调用了泛型类型的rand方法而已,提供的随机数生成器是一个默认的生成器。
当你看到特征使用了泛型类型用其它的特征作为约束,它们相互配合,由于方法是泛型的,编译器会为使用的每种不同的组合生成机器代码,就像Rand和Rng那样。
实现了Hash的类型是可以用于哈希表的key的,实现了Hasher的类型提供了哈希算法,它们结合在一起工作,像Rand和Rng那样。Hash特征有个泛型方法Hash::hash接受一个实现了Hasher特征的类型的值作为参数。
关联类型,泛型特征和特征好友提供了避免调用虚方法的方法,因为它们在编译期间就提供了类型信息,避免动态分发。
通过索引从切片里取值要求切片内数据类型是拷贝类型的。
开源库num??
rust的显式泛型约束让编译器明明白白的报错,编译器可以清楚地告诉你问题出在哪里,并且让代码和文档更加清晰,你通过浏览函数签名就知道泛型函数到底需要那些类型的值作为参数。
