改进rust代码的35种具体方法-类型(五)-熟悉标准特征

接上一篇文章


Rust通过一系列描述这些行为的细粒度标准特征,将其类型系统的关键行为方面编码在类型系统本身。

对于来自C++的程序员来说,其中许多特征似乎很熟悉,与复制构造函数、析构函数、相等和赋值运算符等概念相对应。

与C++一样,为您自己的类型实现许多这些特征通常是一个好主意;如果某些操作需要您的类型需要这些特征之一,并且它不存在,Rust编译器将为您提供有用的错误消息。

实现如此大量的特征可能看起来令人生畏,但大多数常见的特征可以通过使用derive宏自动应用于用户定义的类型。这导致了以下类型定义:

    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    enum MyBooleanOption {
        Off,
        On,
    }

这种细粒度的行为规范起初可能会令人不安,但重要的是要熟悉这些最常见的标准特征,以便立即理解类型定义的可用行为。

本项目涵盖的每个标准特征的粗略一句话摘要是:

  • Clone:当被要求时,这种类型的项目可以制作自己的副本。
  • Copy:如果编译器对此项的内存表示进行比特复制,则结果为有效的新项。
  • Default:可以使用合理的默认值制作这种类型的新实例。
  • PartialEq:这种类型的项目存在部分等价关系——任何两个项目都可以明确比较,但x==x可能并不总是正确的。
  • Eq:这种类型的项目有等价关系:任何两个项目都可以明确比较,x==x总是正确的。
  • PartialOrd:这种类型的一些项目可以比较和订购。
  • Ord:这种类型的所有物品都可以比较和订购。
  • Hash:当被问及时,这种类型的项目可以产生其内容的稳定散列。
  • Debug:这种类型的项目可以显示给程序员。
  • Display:这种类型的项目可以显示给用户。

这些特征都可以为用户定义的类型派生,但Display除外(此处包含,因为它与Debug重叠)。然而,在某些情况下,手动实施(或没有实施)是可取的。

Rust还允许通过实现std::ops模块的各种特征,为用户定义的类型重载各种内置一元和二进制运算符。这些特征是不可推导的,通常仅用于表示“代数”对象的类型。

其他(不可derive的)标准特征包含在其他项目中,因此不包括在内。这些包括:

  • FnFnOnceFnMut:实现此特征的项目表示可以调用的闭包。
  • Error:实现此特征的项目表示可以显示给用户或程序员的错误信息,并且可能包含嵌套的子错误信息。
  • Drop:实现此特征的项目在被销毁时执行处理,这对RAII模式至关重要。
  • FromTryFrom:实现此特征的项目可以从其他类型的项目中自动创建,但在后一种情况下可能会失败。
  • DerefDerefMut:实现此特征的项目是类似指针的对象,可以取消引用以访问内部项目。
  • Iterator和朋友:实现此特征的项目代表可以遍历的集合。
  • Send:实现此特征的项目可以安全地在多个线程之间传输。
  • Sync:实现此特征的项目可以安全地被多个线程引用。

Clone

Clone特征表示,可以通过调用clone()方法对项目进行新复制。这大致相当于C++的复制构造函数,但更明确:编译器永远不会单独默默地调用此方法(请阅读下一节)。

Clone可以推导;宏实现通过克隆其每个成员来克隆聚合类型,同样大致相当于C++中的默认复制构造函数。这使得特征选择加入(通过添加#[derive(Clone)]),与C++中的退出行为(MyType(const MyType&) = delete;)形成鲜明对比。

这是一个如此常见和有用的操作,调查您不应该或无法实现Clone的情况,或者默认derive实现不合适的情况会更有趣。

  • 如果项目体现了对某些资源的唯一访问(如RAII类型),或者当有其他限制副本的原因(例如,如果项目包含加密密钥材料),则不应实现Clone
  • 如果您类型的某些组件反过来是不可Clone的,则无法实现Clone。例子包括:
    • 可变引用(&mut T)的字段,因为借阅检查器一次只允许单个可变引用。
    • 属于上一个类别的标准库类型,如MutexGuard(体现唯一访问)或Mutex(为线程安全限制副本)。
  • 如果您的项目有任何内容不会被(递归)逐字段副本捕获,或者如果存在与项目生命周期相关的额外簿记,则应手动实现Clone。例如,考虑一种在运行时跟踪现有项目数量的类型,用于度量;手动Clone实现可以确保计数器保持准确。

Copy

Copy特征有一个微不足道的声明:

pub trait Copy: Clone { 
// todo

}

此特征中没有方法,这意味着它是一个标记特征(如第2项所述):它用于指示类型系统中没有直接表达的类型的某些约束。

Copy的情况下,此标记的含义是,不仅可以复制这种类型的项目(因此可以绑定Clone特征),而且保存项目的内存的逐位副本可以给出正确的新项目。实际上,这种特征是一个标记,表示类型是“普通旧数据”(POD)类型

与用户定义的标记特征(不同,Copy对编译器1具有特殊意义,而不是可用于特征边界——它将编译器从移动语义转移到复制语义

使用赋值运算符的移动语义,右手给出的东西,左手拿走:

        #[derive(Debug, Clone)]
        struct KeyId(u32);
        let k = KeyId(42);
        let k2 = k; // value moves out of k in to k2
        println!("k={:?}", k);
error[E0382]: borrow of moved value: `k`
  --> std-traits/src/main.rs:52:28
   |
50 |         let k = KeyId(42);
   |             - move occurs because `k` has type `main::KeyId`, which does not implement the `Copy` trait
51 |         let k2 = k; // value moves out of k in to k2
   |                  - value moved here
52 |         println!("k={:?}", k);
   |                            ^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)

使用复制语义,原始项目仍然存在:

        #[derive(Debug, Clone, Copy)]
        struct KeyId(u32);
        let k = KeyId(42);
        let k2 = k; // value bitwise copied from k to k2
        println!("k={:?}", k);

这使得Copy需要注意的最重要特征之一:它从根本上改变了赋值的行为——这包括方法调用的参数。

在这方面,与C++的复制构造函数再次重叠,但值得强调一个关键的区别:在Rust中,没有办法让编译器静默调用用户定义的代码——它要么是显式的(对.clone()的调用),要么不是用户定义的(按位副本)。

要完成本节,请注意,由于Copy具有Clone特征绑定,因此可以.clone()任何可Copy的项目。然而,这不是一个好主意:按位复制总是比调用特征方法更快。Clippy(第29项)将警告您:

        let k3 = k.clone();
warning: using `clone` on type `main::KeyId` which implements the `Copy` trait
  --> std-traits/src/main.rs:68:18
   |
68 |         let k3 = k.clone();
   |                  ^^^^^^^^^ help: try removing the `clone` call: `k`
   |

Default

Default特征通过default()方法定义了默认构造函数。此特征可以为用户定义的类型派生,前提是所有所涉及的子类型都有自己的Default实现;如果没有,则必须手动实现该特征。继续与C++的比较,请注意必须显式触发默认构造函数;编译器不会自动创建一个。

Default特征最有用的方面是它与结构更新语法的结合。对于任何未显式初始化的字段,此语法允许通过从同一struct的现有实例中复制或移动其内容来初始化struct字段。要复制的模板在初始化结束时,在..之后给出,Default特征提供了一个理想的模板:

    #[derive(Default)]
    struct Colour {
        red: u8,
        green: u8,
        blue: u8,
        alpha: u8,
    }

    let c = Colour {
        red: 128,
        ..Default::default()
    };

这使得初始化具有大量字段的结构变得容易得多,其中只有一些字段具有非默认值。

PartialEqEq

PartialEqEq特征允许您为用户定义的类型定义相等。这些特征具有特殊意义,因为如果它们存在,编译器将自动使用它们进行相等(==)检查,类似于C++中的operator==。默认的derive实现通过递归逐个字段进行比较来做到这一点。

Eq版本只是PartialEq的标记特征扩展,它增加了反射性的假设:任何声称支持Eq的类型T都应该确保x == x对任何x: T都是真的。

这足够奇怪,可以立即提出一个问题:什么时候x不== x?这种分割背后的基本原理与浮点数有关,特别是与特殊的“非数字”值NaN (Rust中的f32:: NaN / f64:: NaN)有关。浮点规范要求任何东西都不能与NaN相比,包括NaN本身;PartialEq特性就是由此产生的连锁反应。

对于没有任何浮点相关特征的用户定义类型,每当您实现PartialEq,您都应该实现Eq。如果您想将类型用作HashMap中的键(以及Hash特征),也需要完整的Eq特征。

如果您的类型包含任何不影响项目身份的字段,例如内部缓存和其他性能优化,您应该手动实现PartialEq

PartialOrdOrd

排序特征PartialOrdOrd允许在类型的两个项目之间进行比较,返回LessGreaterEqual。特征需要实现等效的平等特征(PartialOrd需要PartialEqOrd需要Eq),两者必须相互同意(特别是手动实现时要注意这一点)。

与相等性状一样,比较性状具有特殊意义,因为编译器将自动使用它们进行比较操作(<><=>=)。

derive产生的默认实现按照定义的顺序对字段(或enum变体)进行词典比较,因此,如果这不正确,您需要手动实现特征(或对字段重新排序)。

PartialEqPartialOrd特征确实对应于各种真实情况。例如,它可用于表达集合之间的子集关系3{1, 2}{1, 2, 4}的子集,但{1, 3}不是{2, 4}的子集,反之亦然。

然而,即使部分顺序确实准确地模拟了您类型的行为,也要警惕只实施PartialOrd(这种情况很少与第2项在类型系统中编码行为的建议相矛盾)-它可能会导致令人惊讶的结果:

    let x = Oddity(1);
    let x2 = Oddity(1);
    if x <= x2 {
        println!("It's possible to not hit this!");
    }

    let x = Oddity(1);
    let y = Oddity(2);
    // Programmers are unlikely to cover all possible arms...
    if x <= y {
        println!("y is bigger"); // Not hit
    } else if y <= x {
        println!("x is bigger"); // Not hit
    } else {
        println!("Neither is bigger"); // This one
    }

Hash

Hash特征用于生成一个单个值,该值对不同项目具有很高的差异概率;该值被用作基于哈希桶的数据结构(如HashMapHashSet)的基础。

翻转这一点,“相同”项(根据Eq)始终产生相同的散列至关重要;如果x == y(通过Eq),则必须始终为真hash(x) == hash(y)如果您有手动Eq实现,请检查您是否还需要手动实现Hash来满足此要求。

DebugDisplay

DebugDisplay特征允许类型指定如何将其包含在输出中,用于正常({}格式参数)或调试目的({:?}格式参数),大致类似于C++中iostreamoperator<<重载。

然而,这两个特征的意图之间的差异超出了需要哪种格式说明符:

  • Debug可以自动导出,Display只能手动实现。这与...有关
  • Debug输出的布局可能会在不同的Rust版本之间发生变化。如果输出将被其他代码解析,请使用Display
  • Debug面向程序员,Display面向用户。一个有助于做到这一点的思想实验是考虑如果程序本地化为作者不会说的语言会发生什么;如果内容应该翻译,则Display是合适的,如果不应该Debug

作为一般规则,为您的类型添加自动生成的Debug实现,除非它们包含敏感信息(个人详细信息、加密材料等)。当自动生成的版本会发出大量细节时,手动实现Debug可能是合适的。

如果您的类型设计为在文本输出中向最终用户显示,则实现Display

运算符重载在下表中。这些都无法推导出来。

为了完整起,其他项目中涵盖的标准特征包含在下表中;这些特征都不可derive(但SendSync可能由编译器自动实现)。

特质编译器使用界限方法
Fnx(a)FnMut

call

FnMutx(a)FnOnce

call_mut

FnOncex(a)call_once
ErrorDisplay + Debug[source]
Fromfrom
TryFromtry_from
Intointo
TryIntotry_into
AsRefas_ref
AsMutas_mut
BorrwBorrw
BorrwMutBorrwBorrw_mut
ToOwnedTo_owned
Deref*x&xderef
DerefMut*x&mut xderefderef_mut
Indexx[idx]index
IndexMutx[idx] = ...Indexindex_mut
Pointerformat("{:p}", x)fmt
Iteratornext
IntoIteratorfor y in xinto_iter
FromIteratorfrom_iter
ExactSizeIteratorIterator(size_hint)
DoubleEndedIteratorIteratornext_back
drop}(范围结束)drop
Sized标记特征
Send跨线程传输标记特征
Sync交叉线程使用标记特征


1:就像std::marker中的其他几个标记特征一样。

2:当然,为了平等而比较浮动总是一个危险的游戏,因为通常不能保证四舍五入计算会产生与你第一次想到的数字相比特相同的结果。

3:更一般地说,任何晶格结构也具有偏序。

下一篇文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值