关于rust中trait(二)

在Rust中默认定义了一些比较常用的trait,主要是为了满足不同的场景下使用。但其中一些trait(Deref/AsRef/Borrow/Cow)的概念理解起来有点“晦涩”;
对于初学 Rust 的新手,对这几个概念会十分迷惑。所以,现在就让我们一起来探索一下。

一、按模块分类理解

其实按标准库的分类,首先就可以略知一二它们的作用。

  1. std::ops::Deref :Deref 是被归类为 ops 模块。这个模块下放的都是可重载的操作符。这些操作符都有对应的 trait:比如Add trait 对应的就是 +
    Deref trait 则对应共享(不可变)借用 的解引用操作,比如 *v; 相应的,也有 DerefMut trait,对应独占(可变)借用的解引用操作。由于 Rust 所有权语义是贯穿整个语言特性,所以 拥有(Owner)/不可变借用(&T)/可变借用(&mut T)的语义都是配套出现的。
  2. std::convert::AsRef :AsRef 被归类到 convert 模块。这个模块下放的都是拥有类型转换的 trait 。比如熟悉的 From/IntoTryFrom/TryInto ,而 AsRef/AsMut也是作为配对出现在这里,说明该trait 是和类型转化有关。再根据 Rust API Guidelines 里的命名规范可以推理,以 as_ 开头的方法,代表从 borrowed -> borrowed ,即 reference -> reference的一种转换,并且是无开销的。并且这种转换不能失败。
  3. std::borrow::Borrow: Borrow 被归类到 borrow 模块中。而该模块的文档则相对比较简陋:这是用于使用借来的数据。所以该 trait 多多少少和表达借用语义是相关的。提供了三个 trait : Borrow / BorrowMut/ ToOwned ,可以说是和所有权语义完全对应了。
  4. std::borrow::Cow: Cow 也被归类为 borrow 模块中。根据描述,Cow 是 一种 clone-on-write 的智能指针。被放到 borrow 模块,主要还是为了尽可能的使用 借用 而避免 拷贝,是一种优化。

二、trait详解

接下来逐个深入了解

std::ops::Deref

1、定义:

pub trait Deref {
    type Target: ?Sized;
    #[must_use]
    pub fn deref(&self) -> &Self::Target;
}

其实定义不复杂,Deref 只包含一个 deref 方法签名。该 trait 妙就妙在,它会被编译器 「隐式」调用,官方的说法叫 deref. 强转(deref coercion) 。标准库示例:

use std::ops::Deref;

struct DerefExample<T> {
    value: T,    // Value类型为T(泛型)
}
// 实现Deref特型trait
// 内部可操作类型:T;与结构体DerefExample中字段value相同
impl<T> Deref for DerefExample<T> {
    type Target = T;

   // 接收一个DerefExample的引用; 输出DerefExample的内部字段的引用
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

// 验证
// x类型:DerefExample<char>
// 此时DerefExample中的T=char
let x = DerefExample { value: 'a' };

// 此时*x解引用会调用deref方法,进行引用转换输出field: value的内容
// *x等同于*Deref::deref(&x)
assert_eq!('a', *x);

代码中,DerefExample 结构体实现了 Deref trait,那么它就能被使用 解引用操作符* 来执行了。示例中,直接返回字段 value 的值。

可以看得出来:DerefExample 实现了 Deref 而拥有了一种类似于 指针的行为,因为它可以被解引用了。所以为了方便理解这种行为,我们称之为「指针语义」。DerefExample 也就变成了一种智能指针。这也是识别一个类型是否为智能指针的方法之一,看它是否实现 Deref。但并不是所有智能指针都要实现 Deref ,也有的是实现 Drop ,或同时实现。

现在让我们来总结 Deref。
如果 T实现了 Deref<Target = U>,并且 x是 类型 T的一个实例,那么:

  1. 在不可变的上下文中,*x (此时 T 既不是引用也不是原始指针)操作等价于 *Deref::deref(&x)
  2. &T 的值会强制转换为 &U 的值。
  3. 相当于 T 实现了 U 的所有(不可变)方法。

Deref 的妙用在于提升了 Rust 的开发体验。标准库里典型的示例就是 Vec<T> 通过实现 Deref 而共享了 slice的所有方法。

impl<T, A: Allocator> ops::Deref for Vec<T, A> {
    type Target = [T];

    fn deref(&self) -> &[T] {
        unsafe { slice::from_raw_parts(self.as_ptr(), self.len) } // 此处开销并不大
    }
}

比如, len() 方法实际上是在 slice 模块被定义的。但因为 在 Rust 里,当执行 .调用,或在函数参数位置,都会被编译器自动执行 deref 强转这种隐式行为,所以,就相当于 Vec<T> 也拥有了 slice的方法。
如下样例:

fn main() {
 let a = vec![1, 2, 3];
 assert_eq!(a.len(), 3); // 当 a 调用 len() 的时候,发生 deref 强转
}

在Rust 中的隐式行为并不多见,但是 Deref 这种隐式强转的行为,为我们方便使用智能指针提供了便利。

fn main() {
    let h = Box::new("hello");
    assert_eq!(h.to_uppercase(), "HELLO");
}

比如我们操作 Box<T>,我们就不需要手动解引用取出里面T来操作,而是当 Box<T> 外面这一层是透明的,直接来操作 T 就可以了。

再比如:

fn uppercase(s: &str) -> String {
    s.to_uppercase()
}

fn main() {
    let s = String::from("hello");
    assert_eq!(uppercase(&s), "HELLO");
}

上面 uppercase 方法的参数类型 明明是 &str,但现在main函数中实际传的类型是 &String,为什么编译可以成功呢?就是因为 String 实现了 Deref :

impl ops::Deref for String {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

这就是 Deref 的妙用。但是有些人可能会“恍然大悟”,这不就是继承吗?大误。
这种行为好像有点像继承,但请不要随便用 Deref 来模拟继承。

std::convert::AsRef

来看一下 AsRef 的定义:

pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

我们已经知道 AsRef 可以用于转换。相比较于拥有隐式行为的 Deref ,AsRef 属于显式的转换

fn is_hello<T: AsRef<str>>(s: T) {
   assert_eq!("hello", s.as_ref());
}

fn main() {
    let s = "hello";
    is_hello(s);

    let s = "hello".to_string();
    is_hello(s);
}

上面示例中,is_hello 的函数是泛型函数。通过 T: AsRef<str>的限定,并且在函数内使用 s.as_ref()这样的显式调用来达到转换的效果。不管是 String 还是 str其实都实现了 AsRef trait。

那现在问题来了,什么时候使用 AsRef 呢?为啥不直接用 &T

举一个示例:

// 定义struct
pub struct Thing {
    name: String,
}
// 实现
impl Thing {
    pub fn new(name: WhatTypeHere) -> Self {
        Thing { name: name.some_conversion() }
}

上面示例中,new函数 name的类型参数有以下几种情况选择:

  1. &str。此时, 调用方(caller)需要传入一个引用。但是为了转换为 String ,则被调方(callee)则需要自己控制内存分配,并且会有拷贝。
  2. String。此时,调用方传 String 还好,如果是传引用,则和情况 1 相似。
  3. T: Into<String>。此时,调用方可以传 &strString,但是在类型转换的时候同样会有内存分配和拷贝的情况。
  4. T: AsRef<str>。同 情况 3 。
  5. T: Into<Cow<'a, str>>,此时,可以避免一些分配。后面会介绍 Cow

到底何时使用哪种类型,这个其实没有一个标准答案。有的人就是喜欢 &str ,不管在什么地方都会使用它。这里面其实是需要权衡的:

  1. 有些分配和拷贝是无关紧要的,所以就没有必要让类型签名过度复杂化,直接使用 &str就可以了。
  2. 有些是需要看方法定义,是否需要消耗所有权,或者返回所有权还是借用。
  3. 有些则是需要尽量减少分配和拷贝,那就必须使用比较复杂的类型签名,比如情况5。

通过显式调用 .as_ref(),就可以得到父类结构的引用。
Deref 注重隐式透明地使用 父类结构,而 AsRef 则注重显式地获取父类结构的引用。这是结合具体的 API 设计所作的权衡,而不是无脑模拟 OOP 继承。

std::borrow::Borrow

来看一下 Borrow 的定义:

pub trait Borrow<Borrowed: ?Sized> {
    fn borrow(&self) -> &Borrowed;
}

对比一下 AsRef:

pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

是不是非常相似?所以,有人提出,这俩 trait 完全可以去掉一个。但实际上,Borrow 和 AsRef 是有区别的,它们都有存在的意义。

Borrow trait是用来表示 借用数据。而 AsRef 则是用来表示类型转换。在Rust中,为不同的语义不同的使用情况提供不同的类型表示是很常见的。

一个类型通过实现 Borrow,在 borrow()方法中提供对 T 的引用/借用,表达的语义是可以作为某个类型 T被借用,而非转换。一个类型可以自由地借用为几个不同的类型,也可以用可变的方式借用。

所以 Borrow 和 AsRef 如何选呢?

  • 当你想把不同类型的借用进行统一抽象,或者当你要建立一个数据结构,以同等方式处理自拥有值(ownered)和借用值(borrowed)时,例如散列(hash)和比较(compare)时,选择Borrow。
  • 当你想把某个类型直接转换为引用,并且你正在编写通用代码时,选择AsRef。比较简单的情况。

其实在标准库文档中给出的 HashMap 示例已经说明的很好了:
HashMap<K, V> 存储键值对,对于 API 来说,无论使用 Key 的自有值,还是其引用,应该都可以正常地在 HashMap 中检索到对应的值。因为 HashMap 要对 key 进行 hash计算 和 比较,所以必须要求 不管是 Key 的自有值,还是引用,在进行 hash计算和比较的时候,行为应该是一致的。

use std::borrow::Borrow;
use std::hash::Hash;

pub struct HashMap<K, V> {
    // fields omitted
}

impl<K, V> HashMap<K, V> {
    // insert 方法使用 Key 的自有值,拥有所有权
    pub fn insert(&self, key: K, value: V) -> Option<V>
    where K: Hash + Eq
    {
        // ...
    }

    // 使用 get 方法通过 key 来获取对应的值,则可以使用 key的引用,这里用 &Q 表示
    // 并且要求 Q 要满足 `Q: Hash + Eq + ?Sized `
    // 而 K 呢 ,通过 `K: Borrow<Q>` 来表达 K 是 Q 的一个借用数据。
    // 所以,这里要求 Q 的 hash 实现 和 K 是一样的,否则编译就会出错
    pub fn get<Q>(&self, k: &Q) -> Option<&V>
    where
        K: Borrow<Q>,
        Q: Hash + Eq + ?Sized
    {
        // ...
    }
}

代码的注释基本已经说明了问题。Borrow 是对借用数据的一种限制,并且配合额外的trait来使用,比如示例中的 HashEq 等。

再看一个示例:

// 这个结构体能不能作为 HashMap 的 key?
pub struct CaseInsensitiveString(String);

// 它实现 Eq 没有问题
impl  PartialEq for CaseInsensitiveString {
    fn eq(&self, other: &Self) -> bool {
       // 但这里比较是要求忽略了 ascii 大小写
        self.0.eq_ignore_ascii_case(&other.0)
    }
}

impl Eq for CaseInsensitiveString { }

// 实现 Hash 没有问题
// 但因为 eq 忽略大小写,那么 hash 计算也必须忽略大小写
impl Hash for CaseInsensitiveString {
    fn hash<H: Hasher>(&self, state: &mut H) {
        for c in self.0.as_bytes() {
            c.to_ascii_lowercase().hash(state)
        }
    }
}

但是 CaseInsensitiveString 可以实现 Borrow<str>吗?

很显然,CaseInsensitiveString 和 str 对 Hash 的实现不同,str 是不会忽略大小写的。因此,CaseInsensitiveString 不能实现 Borrow<str>,所以 CaseInsensitiveString 不能作为 HashMap 的 key,但编译器无法通过 Borrow trait 来识别这种情况。

但是 CaseInsensitiveString 完全可以实现 AsRef 。

这就是 Borrow 和 AsRef 的区别,Borrow 更加严格一些,并且表示的语义和 AsRef 完全不同。

std::borrow::Cow

看一下 Cow 的定义:

pub enum Cow<'a, B> 
where  B: 'a + ToOwned + ?Sized, 
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

看得出来, Cow 是一个枚举。有点类似于 Option,表示两种情况中的某一种。Cow 在这里就是表示 借用的 和 自有的,但只能出现其中的一种情况。

Cow 主要功能:

  1. 作为智能指针,提供对此类型实例的透明的不可变访问(比如可直接调用此类型原有的不可变方法,实现了Deref ,但没实现 DerefMut);
  2. 如果遇到需要修改此类型实例,或者需要获得此类型实例的所有权的情况,Cow 提供方法做克隆(Clone)处理,并避免多次重复克隆。

Cow 的设计目的是提高性能(减少复制)同时增加灵活性,因为大部分情况下,业务场景都是读多写少。利用 Cow,可以用统一,规范的形式实现,需要写的时候才做一次对象复制。这样就可能会大大减少复制的次数。

它有以下几个要点需要掌握:

  1. Cow<T> 能直接调用 T 的不可变方法,因为 Cow 这个枚举,实现了 Deref
  2. 在需要修改T的时候,可以使用.to_mut()方法得到一个具有所有权的值的可变借用;
      1. 注意,调用 .to_mut() 不一定会产生Clone;
      1. 在已经具有所有权的情况下,调用 .to_mut() 有效,但是不会产生新的Clone;
      1. 多次调用 .to_mut() 只会产生一次Clone。
  3. 在需要修改T的时候,可以使用.into_owned()创建新的拥有所有权的对象,这个过程往往意味着内存拷贝并创建新对象;
      1. 如果之前 Cow 中的值是借用状态,调用此操作将执行Clone;
      1. 本方法,参数是self类型,它会“消费”原先的那个类型实例,调用之后原先的类型实例的生命周期就截止了,在 Cow 上不能调用多次;

Cow 在 API 设计上用的比较多:

use std::borrow::Cow;

// 返回值使用 Cow ,避免多次拷贝
fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> {
    if input.contains(' ') {
        let mut buf = String::with_capacity(input.len());
        for c in input.chars() {
            if c != ' ' {
                buf.push(c);
            }
        }
        return Cow::Owned(buf);
    }
    return Cow::Borrowed(input);
}

当然,什么时候使用 Cow ,又回到了我们前文中那个「什么时候使用 AsRef 」的讨论,一切都要权衡,并没有放之四海皆准的标准答案。

三、引用

关于Deref
deref. 强转(deref coercion)
关于AsRef
关于Borrow
关于Cow

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343