深入浅出Rust
上QQ阅读APP看书,第一时间看更新

第5章 trait

Rust语言中的trait是非常重要的概念。在Rust中,trait这一个概念承担了多种职责。在中文里,trait可以翻译为“特征”“特点”“特性”等。由于这些词区分度并不明显,在本书中一律不翻译trait这个词,以避免歧义。

trait中可以包含:函数、常量、类型等。

5.1 成员方法

trait中可以定义函数。用例子来说明,我们定义如下的trait:

    trait Shape {
        fn area(&self) -> f64;
    }

上面这个trait包含了一个方法,这个方法只有一个参数,这个&self参数是什么意思呢?

所有的trait中都有一个隐藏的类型Self(大写S),代表当前这个实现了此trait的具体类型。trait中定义的函数,也可以称作关联函数(associated function)。函数的第一个参数如果是Self相关的类型,且命名为self(小写s),这个参数可以被称为“receiver”(接收者)。具有receiver参数的函数,我们称为“方法”(method),可以通过变量实例使用小数点来调用。没有receiver参数的函数,我们称为“静态函数”(static function),可以通过类型加双冒号::的方式来调用。在Rust中,函数和方法没有本质区别。

Rust中Self(大写S)和self(小写s)都是关键字,大写S的是类型名,小写s的是变量名。请大家一定注意区分。self参数同样也可以指定类型,当然这个类型是有限制的,必须是包装在Self类型之上的类型。对于第一个self参数,常见的类型有self :Self、self : &Self、self : &mut Self等类型。对于以上这些类型,Rust提供了一种简化的写法,我们可以将参数简写为self、&self、&mut self。self参数只能用在第一个参数的位置。请注意“变量self”和“类型Self”的大小写不同。示例如下:

    trait T {
        fn method1(self: Self);
        fn method2(self: &Self);
        fn method3(self: &mut Self);
    }
    // 上下两种写法是完全一样的
    trait T {
        fn method1(self);
        fn method2(&self);
        fn method3(&mut self);
    }

所以,回到开始定义的那个Shape trait,上面定义的这个area方法的参数的名字为self,它的类型是&Self类型。我们可以把上面这个方法的声明看成:

    trait Shape {
        fn area(self: &Self) -> f64;
    }

我们可以为某些具体类型实现(impl)这个trait。

假如我们有一个结构体类型Circle,它实现了这个trait,代码如下:

    struct Circle {
        radius: f64,
    }
    impl Shape for Circle {
        // Self 类型就是 Circle
        // self 的类型是 &Self, &Circle
        fn area(&self) -> f64 {
            // 访问成员变量,需要用 self.radius
            std::f64::consts::PI * self.radius * self.radius
        }
    }
    fn main() {
        let c = Circle { radius : 2f64};
        // 第一个参数名字是 self,可以使用小数点语法调用
        println!("The area is {}", c.area());
    }

在上面的例子中可以看到,如果有一个Circle类型的实例c,我们就可以用小数点调用函数,c.area()。在方法内部,我们可以通过self.radius的方式访问类型的内部成员。

另外,针对一个类型,我们可以直接对它impl来增加成员方法,无须trait名字。比如:

    impl Circle {
        fn get_radius(&self) -> f64 { self.radius }
    }

我们可以把这段代码看作是为Circle类型impl了一个匿名的trait。用这种方式定义的方法叫作这个类型的“内在方法”(inherent methods)。

trait中可以包含方法的默认实现。如果这个方法在trait中已经有了方法体,那么在针对具体类型实现的时候,就可以选择不用重写。当然,如果需要针对特殊类型作特殊处理,也可以选择重新实现来“override”默认的实现方式。比如,在标准库中,迭代器Iterator这个trait中就包含了十多个方法,但是,其中只有fn next(&mut self) ->Option<Self::Item>是没有默认实现的。其他的方法均有其默认实现,在实现迭代器的时候只需挑选需要重写的方法来实现即可。

self参数甚至可以是Box指针类型self : Box<Self>。另外,目前Rust设计组也在考虑让self变量的类型放得更宽,允许更多的自定义类型作为receiver,比如MyType<Self>。示例如下:

    trait Shape {
        fn area(self: Box<Self>) -> f64;
    }
    struct Circle {
        radius: f64,
    }
    impl Shape for Circle {
        // Self 类型就是 Circle
        // self 的类型是 Box<Self>, Box<Circle>
        fn area(self : Box<Self>) -> f64 {
            // 访问成员变量,需要用 self.radius
            std::f64::consts::PI * self.radius * self.radius
        }
    }
    fn main() {
        let c = Circle { radius : 2f64};
        // 编译错误
        // c.area();
        let b = Box::new(Circle {radius : 4f64});
        // 编译正确
        b.area();
    }

impl的对象甚至可以是trait。示例如下:

    trait Shape {
        fn area(&self) -> f64;
    }
    trait Round {
        fn get_radius(&self) -> f64;
    }
    struct Circle {
        radius: f64,
    }
    impl Round for Circle {
        fn get_radius(&self) -> f64 { self.radius }
    }
    // 注意这里是 impl Trait for Trait
    impl Shape for Round {
        fn area(&self) -> f64 {
            std::f64::consts::PI * self.get_radius() * self.get_radius()
        }
    }
    fn main() {
        let c = Circle { radius : 2f64};
        // 编译错误
        // c.area();
        let b = Box::new(Circle {radius : 4f64}) as Box<Round>;
        // 编译正确
        b.area();
    }

注意这里的写法,impl Shape for Round和impl<T: Round> Shape for T是不一样的。在前一种写法中,self是&Round类型,它是一个trait object,是胖指针。而在后一种写法中,self是&T类型,是具体类型。前一种写法是为trait object增加一个成员方法,而后一种写法是为所有的满足T: Round的具体类型增加一个成员方法。所以上面的示例中,我们只能构造一个trait object之后才能调用area()成员方法。trait object和“泛型”之间的区别请参考本书第三部分。

题外话,impl Shape for Round这种写法确实是很让初学者纠结的,Round既是trait又是type。在将来,trait object的语法会被要求加上dyn关键字,所以在Rust 2018 edition以后应该写成impl Shape for dyn Round才合理。关于trait object的内容,请参考本书第三部分第23章。

5.2 静态方法

没有receiver参数的方法(第一个参数不是self参数的方法)称作“静态方法”。静态方法可以通过Type::FunctionName()的方式调用。需要注意的是,即便我们的第一个参数是Self相关类型,只要变量名字不是self,就不能使用小数点的语法调用函数。

    struct T(i32);
    impl T {
        // 这是一个静态方法
        fn func(this: &Self) {
            println!{"value {}", this.0};
        }
    }
    fn main() {
        let x = T(42);
        // x.func(); 小数点方式调用是不合法的
        T::func(&x);
    }

在标准库中就有一些这样的例子。Box的一系列方法Box::into_raw(b: Self) Box::leak(b: Self),以及Rc的一系列方法Rc::try_unwrap(this: Self) Rc::downgrade(this: &Self),都是这种情况。它们的receiver不是self关键字,这样设计的目的是强制用户用Rc::downgrade(&obj)的形式调用,而禁止obj. downgrade()形式的调用。这样源码表达出来的意思更清晰,不会因为Rc<T>里面的成员方法和T里面的成员方法重名而造成误解问题(这又涉及Deref trait的内容,读者可以把第16章读完再回看这一段)。

trait中也可以定义静态函数。下面以标准库中的std::default::Default trait为例,介绍静态函数的相关用法:

    pub trait Default {
        fn default() -> Self;
    }

上面这个trait中包含了一个default()函数,它是一个无参数的函数,返回的类型是实现该trait的具体类型。Rust中没有“构造函数”的概念。Default trait实际上可以看作一个针对无参数构造函数的统一抽象。

比如在标准库中,Vec::default()就是一个普通的静态函数。

    // 这里用到了“泛型”,请参阅第21
    impl<T> Default for Vec<T> {
        fn default() -> Vec<T> {
            Vec::new()
        }
    }

跟C++相比,在Rust中,定义静态函数没必要使用static关键字,因为它把self参数显式在参数列表中列出来了。作为对比,C++里面成员方法默认可以访问this指针,因此它需要用static关键字来标记静态方法。Rust不采取这个设计,主要原因是self参数的类型变化太多,不同写法语义差别很大,选择显式声明self参数更方便指定它的类型。

5.3 扩展方法

我们还可以利用trait给其他的类型添加成员方法,哪怕这个类型不是我们自己写的。比如,我们可以为内置类型i32添加一个方法:

    trait Double {
        fn double(&self) -> Self;
    }
    impl Double for i32 {
        fn double(&self) -> i32 { *self * 2 }
    }
    fn main() {
        // 可以像成员方法一样调用
        let x : i32 = 10.double();
        println! ("{}", x);
    }

这个功能就像C#里面的“扩展方法”一样。哪怕这个类型不是在当前的项目中声明的,我们依然可以为它增加一些成员方法。但我们也不是随随便便就可以这么做的,Rust对此有一个规定。

在声明trait和impl trait的时候,Rust规定了一个Coherence Rule(一致性规则)或称为Orphan Rule(孤儿规则):impl块要么与trait的声明在同一个的crate中,要么与类型的声明在同一个crate中。

也就是说,如果trait来自于外部crate,而且类型也来自于外部crate,编译器不允许你为这个类型impl这个trait。它们之中必须至少有一个是在当前crate中定义的。因为在其他的crate中,一个类型没有实现一个trait,很可能是有意的设计。如果我们在使用其他的crate的时候,强行把它们“拉郎配”,是会制造出bug的。比如说,我们写了一个程序,引用了外部库lib1和lib2, lib1中声明了一个trait T, lib2中声明了一个struct S,我们不能在自己的程序中针对S实现T。这也意味着,上游开发者在给别人写库的时候,尤其要注意,一些比较常见的标准库中的trait,如Display Debug ToString Default等,应该尽可能地提供好。否则,使用这个库的下游开发者是没办法帮我们把这些trait实现的。

同理,如果是匿名impl,那么这个impl块必须与类型本身存在于同一个crate中。

更多关于“一致性规则”的解释,可以参见编译器的详细错误说明:

    rustc --explain E0117
    rustc --explain E0210

当类型和trait涉及泛型参数的时候,一致性规则实际上是很复杂的,用户如果需要了解所有的细节,还需要参考对应的RFC文档。

许多初学者会用自带GC的语言中的“Interface”、抽象基类来理解trait这个概念,但是实际上它们有很大的不同。

Rust是一种用户可以对内存有精确控制能力的强类型语言。我们可以自由指定一个变量是在栈里面,还是在堆里面,变量和指针也是不同的类型。类型是有大小(Size)的。有些类型的大小是在编译阶段可以确定的,有些类型的大小是编译阶段无法确定的。目前版本的Rust规定,在函数参数传递、返回值传递等地方,都要求这个类型在编译阶段有确定的大小。否则,编译器就不知道该如何生成代码了。

而trait本身既不是具体类型,也不是指针类型,它只是定义了针对类型的、抽象的“约束”。不同的类型可以实现同一个trait,满足同一个trait的类型可能具有不同的大小。因此,trait在编译阶段没有固定大小,目前我们不能直接使用trait作为实例变量、参数、返回值。

有一些初学者特别喜欢写这样的代码:

    let x: Shape = Circle::new(); // Shape 不能做局部变量的类型
    fn use_shape(arg : Shape) {}  // Shape 不能直接做参数的类型
    fn ret_shape() -> Shape {}     // Shape 不能直接做返回值的类型

这样的写法是错误的。请一定要记住,trait的大小在编译阶段是不固定的。那怎样写才是对的呢?后面我们讲到泛型的时候再说。

5.4 完整函数调用语法

Fully Qualified Syntax提供一种无歧义的函数调用语法,允许程序员精确地指定想调用的是那个函数。以前也叫UFCS(universal function call syntax),也就是所谓的“通用函数调用语法”。这个语法可以允许使用类似的写法精确调用任何方法,包括成员方法和静态方法。其他一切函数调用语法都是它的某种简略形式。它的具体写法为<T as TraitName>::item。示例如下:

    trait Cook {
        fn start(&self);
    }
    trait Wash {
        fn start(&self);
    }
    struct Chef;
    impl Cook for Chef {
        fn start(&self) { println!("Cook::start"); }
    }
    impl Wash for Chef {
        fn start(&self) { println!("Wash::start"); }
    }
    fn main() {
        let me = Chef;
        me.start();
    }

我们定义了两个trait,它们的start()函数有同样方法签名。

如果一个类型同时实现了这两个trait,那么如果我们使用variable.start()这样的语法执行方法调用的话,就会出现歧义,编译器不知道你具体想调用哪个方法,编译错误信息为“multiple applicable items in scope”。

这时候,我们就有必要使用完整的函数调用语法来进行方法调用,只有这样写,才能清晰明白且无歧义地表达清楚期望调用的是哪个函数:

    fn main() {
        let me = Chef;
    // 函数名字使用更完整的path来指定,同时,self参数需要显式传递
        <Cook>::start(&me);
        <Chef as Wash>::start(&me);
    }

由此我们也可以看到,所谓的“成员方法”也没什么特殊之处,它跟普通的静态方法的唯一区别是,第一个参数是self,而这个self只是一个普通的函数参数而已。只不过这种成员方法也可以通过变量加小数点的方式调用。变量加小数点的调用方式在大部分情况下看起来更简单更美观,完全可以视为一种语法糖。

需要注意的是,通过小数点语法调用方法调用,有一个“隐藏着”的“取引用”步骤。虽然我们看起来源代码长的是这个样子me.start(),但是大家心里要清楚,真正传递给start()方法的参数是&me而不是me,这一步是编译器自动帮我们做的。不论这个方法接受的self参数究竟是Self、&Self还是&mut Self,最终在源码上,我们都是统一的写法:variable.method()。而如果用UFCS语法来调用这个方法,我们就不能让编译器帮我们自动取引用了,必须手动写清楚。

下面用一个示例演示一下成员方法和普通函数其实没什么本质区别。

    struct T(usize);
    impl T {
        fn get1(&self) -> usize {self.0}
        fn get2(&self) -> usize {self.0}
    }
    fn get3(t: &T) -> usize { t.0 }
    fn check_type( _ : fn(&T)->usize ) {}
    fn main() {
        check_type(T::get1);
        check_type(T::get2);
        check_type(get3);
    }

可以看到,get1、get2和get3都可以自动转成fn(&T)→usize类型。

5.5 trait约束和继承

Rust的trait的另外一个大用处是,作为泛型约束使用。关于泛型,本书第三部分还会详细解释。下面用一个简单示例演示一下trait如何作为泛型约束使用:

    use std::fmt::Debug;
    fn my_print<T : Debug>(x: T) {
        println!("The value is {:? }.", x);
    }
    fn main() {
        my_print("China");
        my_print(41_i32);
        my_print(true);
        my_print(['a', 'b', 'c'])
    }

上面这段代码中,my_print函数引入了一个泛型参数T,所以它的参数不是一个具体类型,而是一组类型。冒号后面加trait名字,就是这个泛型参数的约束条件。它要求这个T类型实现Debug这个trait。这是因为我们在函数体内,用到了println!格式化打印,而且用了{:? }这样的格式控制符,它要求类型满足Debug的约束,否则编译不过。

在调用的时候,凡是满足Debug约束的类型都可以是这个函数的参数,所以我们可以看到以上四种调用都是可以编译通过的。假如我们自定义一个类型,而它没有实现Debug trait,我们就会发现,用这个类型作为my_print的参数的话,编译就会报错。

所以,泛型约束既是对实现部分的约束,也是对调用部分的约束。

泛型约束还有另外一种写法,即where子句。示例如下:

    fn my_print<T>(x: T) where T: Debug {
        println!("The value is {:? }.", x);
    }

对于这种简单的情况,两种写法都可以。但是在某些复杂的情况下,泛型约束只有where子句可以表达,泛型参数后面直接加冒号的写法表达不出来,比如涉及关联类型的时候,请参见第21章。

trait允许继承。类似下面这样:

    trait Base { ... }
    trait Derived : Base { ... }

这表示Derived trait继承了Base trait。它表达的意思是,满足Derived的类型,必然也满足Base trait。所以,我们在针对一个具体类型impl Derived的时候,编译器也会要求我们同时impl Base。示例如下:

    trait Base {}
    trait Derived : Base {}
    struct T;
    impl Derived for T {}
    fn main() {
    }

编译,出现错误,提示信息为:

    --> test.rs:7:6
      |
    7 | impl Derived for T {}
      |      ^^^^^^^ the trait `Base` is not implemented for `T`

我们再加上一句

    impl Base for T {}

编译器就不再报错了。

实际上,在编译器的眼中,trait Derived : Base {}等同于trait Derived where Self : Base {}。这两种写法没有本质上的区别,都是给Derived这个trait加了一个约束条件,即实现Derived trait的具体类型,也必须满足Base trait的约束。

在标准库中,很多trait之间都有继承关系,比如:

    trait Eq: PartialEq<Self> {}
    trait Copy: Clone {}
    trait Ord: Eq + PartialOrd<Self> {}
    trait FnMut<Args>: FnOnce<Args> {}
    trait Fn<Args>: FnMut<Args> {}

读完本书后,读者应该能够理解这些trait是用来做什么的,以及为什么这些trait之间会有这样的继承关系。

5.6 Derive

Rust里面为类型impl某些trait的时候,逻辑是非常机械化的。为许多类型重复而单调地impl某些trait,是非常枯燥的事情。为此,Rust提供了一个特殊的attribute,它可以帮我们自动impl某些trait。示例如下:

    #[derive(Copy, Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
    struct Foo {
        data : i32
    }
    fn main() {
        let v1 = Foo { data : 0 };
        let v2 = v1;
        println!("{:? }", v2);
    }

如上所示,它的语法是,在你希望impl trait的类型前面写#[derive(…)],括号里面是你希望impl的trait的名字。这样写了之后,编译器就帮你自动加上了impl块,类似这样:

    impl Copy for Foo { ... }
    impl Clone for Foo { ... }
    impl Default for Foo { ... }
    impl Debug for Foo { ... }
    impl Hash for Foo { ... }
    impl PartialEq for Foo { ... }
    ......

这些trait都是标准库内部的较特殊的trait,它们可能包含有成员方法,但是成员方法的逻辑有一个简单而一致的“模板”可以使用,编译器就机械化地重复这个模板,帮我们实现这个默认逻辑。当然我们也可以手动实现。

目前,Rust支持的可以自动derive的trait有以下这些:

    Debug    Clone    Copy    Hash    RustcEncodable    RustcDecodable    PartialEq    Eq
        ParialOrd    Ord    Default    FromPrimitive    Send    Sync

5.7 trait别名

跟type alias类似的,trait也可以起别名(trait alias)。假如在某些场景下,我们有一个比较复杂的trait:

    pub trait Service {
        type Request;
        type Response;
        type Error;
        type Future: Future<Item=Self::Response, Error=Self::Error>;
        fn call(&self, req: Self::Request) -> Self::Future;
    }

每次使用这个trait的时候都需要携带一堆的关联类型参数。为了避免这样的麻烦,在已经确定了关联类型的场景下,我们可以为它取一个别名,比如:

    trait HttpService = Service<Request = http::Request,
            Response = http::Response,
            Error = http::Error>;

5.8 标准库中常见的trait简介

标准库中有很多很有用的trait,本节挑几个特别常见的给大家介绍一下。

5.8.1 Display和Debug

这两个trait在标准库中的定义是这样的:

    // std::fmt::Display
    pub trait Display {
        fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
    }
    // std::fmt::Debug
    pub trait Debug {
        fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
    }

它们的主要用处就是用在类似println!这样的地方:

    use std::fmt::{Display, Formatter, Error};
    #[derive(Debug)]
    struct T {
        field1: i32,
        field2: i32,
    }
    impl Display for T {
        fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
            write! (f, "{{ field1:{}, field2:{} }}", self.field1, self.field2)
        }
    }
    fn main() {
        let var = T { field1: 1, field2: 2 };
        println!("{}", var);
        println!("{:? }", var);
        println!("{:#? }", var);
    }

只有实现了Display trait的类型,才能用{}格式控制打印出来;只有实现了Debug trait的类型,才能用{:? } {:#? }格式控制打印出来。它们之间更多的区别如下。

❏ Display假定了这个类型可以用utf-8格式的字符串表示,它是准备给最终用户看的,并不是所有类型都应该或者能够实现这个trait。这个trait的fmt应该如何格式化字符串,完全取决于程序员自己,编译器不提供自动derive的功能。

❏ 标准库中还有一个常用trait叫作std::string::ToString,对于所有实现了Display trait的类型,都自动实现了这个ToString trait。它包含了一个方法to_string(&self) -> String。任何一个实现了Display trait的类型,我们都可以对它调用to_string()方法格式化出一个字符串。

❏ Debug则是主要为了调试使用,建议所有的作为API的“公开”类型都应该实现这个trait,以方便调试。它打印出来的字符串不是以“美观易读”为标准,编译器提供了自动derive的功能。

5.8.2 PartialOrd / Ord / PartialEq / Eq

在前文中讲解浮点类型的时候提到,因为NaN的存在,浮点数是不具备“total order(全序关系)”的。在这里,我们详细讨论一下什么是全序、什么是偏序。Rust标准库中有如下解释。

对于集合X中的元素a, b, c,

❏ 如果a < b则一定有! (a > b);反之,若a > b,则一定有!(a < b),称为反对称性。

❏ 如果a < b且b < c则a < c,称为传递性。

❏ 对于X中的所有元素,都存在a < b或a > b或者a == b,三者必居其一,称为完全性。

如果集合X中的元素只具备上述前两条特征,则称X是“偏序”。同时具备以上所有特征,则称X是“全序”。

从以上定义可以看出,浮点数不具备“全序”特征,因为浮点数中特殊的值NaN不满足完全性。这就导致了一个问题:浮点数无法排序。对于任意一个不是NaN的数和NaN之间做比较,无法分出先后关系。示例如下:

    fn main() {
        let nan = std::f32::NAN;
        let x = 1.0f32;
        println!("{}", nan < x);
        println!("{}", nan > x);
        println!("{}", nan == x);
    }

以上不论是NaN < x, NaN > x还是NaN == x,结果都是false。这是IEEE754标准中规定的行为。

因此,Rust设计了两个trait来描述这样的状态:一个是std::cmp::PartialOrd,表示“偏序”,一个是std::cmp::Ord,表示“全序”。它们的对外接口是这样定义的:

    pub trait PartialOrd<Rhs: ? Sized = Self>: PartialEq<Rhs> {
        fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
        fn lt(&self, other: &Rhs) -> bool { //... }
        fn le(&self, other: &Rhs) -> bool { //... }
        fn gt(&self, other: &Rhs) -> bool { //... }
        fn ge(&self, other: &Rhs) -> bool { //... }
    }
    pub trait Ord: Eq + PartialOrd<Self> {
        fn cmp(&self, other: &Self) -> Ordering;
    }

从以上代码可以看出,partial_cmp函数的返回值类型是Option<Ordering>。只有Ord trait里面的cmp函数才能返回一个确定的Ordering。f32和f64类型都只实现了PartialOrd,而没有实现Ord。

因此,如果我们写出下面的代码,编译器是会报错的:

    let int_vec = [1_i32, 2, 3];
    let biggest_int = int_vec.iter().max();
    let float_vec = [1.0_f32, 2.0, 3.0];
    let biggest_float = float_vec.iter().max();

对整数i32类型的数组求最大值是没问题的,但是对浮点数类型的数组求最大值是不对的,编译错误为:

    the trait 'core::cmp::Ord' is not implemented for the type 'f32'

笔者认为,这个设计是优点,而不是缺点,它让我们尽可能地在更早的阶段发现错误,而不是留到运行时再去debug。假如说编译器无法静态检查出这样的问题,那么就可能发生下面的情况,以Python为例:

    Python 3.4.2 (default, Oct  82014, 10:45:20)
    [GCC 4.9.1] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> v = [1.0, float("nan")]
    >>> max(v)
    1.0
    >>> v = [float("nan"), 1.0]
    >>> max(v)
    nan

上面这个示例意味着,如果数组v中有NaN,对它求最大值,跟数组内部元素的排列顺序有关。

Rust中的PartialOrd trait实际上就是C++20中即将加入的three-way comparison运算符<=>。

同理,PartialEq和Eq两个trait也就可以理解了,它们的作用是比较相等关系,与排序关系非常类似。

5.8.3 Sized

Sized trait是Rust中一个非常重要的trait,它的定义如下:

    #[lang = "sized"]
    #[rustc_on_unimplemented  =  "`{Self}`  does  not  have  a  constant  size  known  at
compile-time"]
    #[fundamental] // for Default, for example, which requires that `[T]: ! Default`
be evaluatable
    pub trait Sized {
        // Empty.
    }

这个trait定义在std::marker模块中,它没有任何的成员方法。它有#[lang ="sized"]属性,说明它与普通trait不同,编译器对它有特殊的处理。用户也不能针对自己的类型impl这个trait。一个类型是否满足Sized约束是完全由编译器推导的,用户无权指定。

我们知道,在C/C++这一类的语言中,大部分变量、参数、返回值都应该是编译阶段固定大小的。在Rust中,但凡编译阶段能确定大小的类型,都满足Sized约束。那还有什么类型是不满足Sized约束的呢?比如C语言里的不定长数组(Variable-length Array)。不定长数组的长度在编译阶段是未知的,是在执行阶段才确定下来的。Rust里面也有类似的类型[T]。在Rust中VLA类型已经通过了RFC设计,只是暂时还没有实现而已。不定长类型在使用的时候有一些限制,比如不能用它作为函数的返回类型,而必须将这个类型藏到指针背后才可以。但它作为一个类型,依然是有意义的,我们可以为它添加成员方法,用它实例化泛型参数,等等。

Rust中对于动态大小类型专门有一个名词Dynamic Sized Type。我们后面将会看到的[T], str以及dyn Trait都是DST。

5.8.4 Default

Rust里面并没有C++里面的“构造函数”的概念。大家可以看到,它只提供了类似C语言的各种复合类型各自的初始化语法。主要原因在于,相比普通函数,构造函数本身并没有提供什么额外的抽象能力。所以Rust里面推荐使用普通的静态函数作为类型的“构造器”。比如,常见的标准库中提供的字符串类型String,它包含的可以构造新的String的方法不完全列举都有这么多:

    fn new() -> String
    fn with_capacity(capacity: usize) -> String
    fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
    fn from_utf8_lossy<'a>(v: &'a [u8]) -> Cow<'a, str>
    fn from_utf16(v: &[u16]) -> Result<String, FromUtf16Error>
    fn from_utf16_lossy(v: &[u16]) -> String
    unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String
    unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String

这还不算Default::default()、From::from(s: &'a str)、FromIte r a t-or::from_iter <I: IntoIterator<Item=char>>(iter: I)、Iter a t-or::collect等相对复杂的构造方法。这些方法接受的参数各异,错误处理方式也各异,强行将它们统一到同名字的构造函数重载中不是什么好主意(况且Rust坚决反对ad hoc式的函数重载)。

不过,对于那种无参数、无错误处理的简单情况,标准库中提供了Default trait来做这个统一抽象。这个trait的签名如下:

    trait Default {
        fn default() -> Self;
    }

它只包含一个“静态函数”default()返回Self类型。标准库中很多类型都实现了这个trait,它相当于提供了一个类型的默认值。

在Rust中,单词new并不是一个关键字。所以我们可以看到,很多类型中都使用了new作为函数名,用于命名那种最常用的创建新对象的情况。因为这些new函数差别甚大,所以并没有一个trait来对这些new函数做一个统一抽象。

5.9 总结

本章对trait这个概念做了基本的介绍。除了上面介绍的之外,trait还有许多用处:

❏ trait本身可以携带泛型参数;

❏ trait可以用在泛型参数的约束中;

❏ trait可以为一组类型impl,也可以单独为某一个具体类型impl,而且它们可以同时存在;

❏ trait可以为某个trait impl,而不是为某个具体类型impl;

❏ trait可以包含关联类型,而且还可以包含类型构造器,实现高阶类型的某些功能;

❏ trait可以实现泛型代码的静态分派,也可以通过trait object实现动态分派;

❏ trait可以不包含任何方法,用于给类型做标签(marker),以此来描述类型的一些重要特性;

❏ trait可以包含常量。

trait这个概念在Rust语言中扮演了非常重要的角色,承担了各种各样的功能,在写代码的时候会经常用到。本章还远没有把trait相关的知识讲解完整,更多关于trait的内容,请参阅本书后文中与泛型、trait object线程安全有关的章节。