发布于 2026-01-06 1 阅读
0

Rust Concept Clarification: Deref vs AsRef vs Borrow vs Cow

Rust 概念澄清:deref、asRef、Borrow 和 Cow 的区别

替代文字

作者:张汉东

按模块理解

事实上,按标准库进行分类首先可以让你对它们的功能有一个大致的了解。

  1. `std :: ops :: Deref`。如您所见,Deref它被归类为一个ops模块。如果您查看文档,就会发现该模块定义了所有可重载运算符的traitAdd trait 。例如,`std::ops::Deref`对应于`std::ops::Deref` +,而 ` Deref traitstd::ops::Deref` 对应于共享(不可变)借用解引用操作,例如`std::ops::Deref`。*v相应地,还有 `std::ops::Deref` ,它对应于排他(可变)借用解引用操作。由于 Rust 的所有权语义是贯穿整个语言的特性,因此`std :: ops::Deref`DerefMut trait的所有语义都同时出现。Ownerimmutable borrowing(&T)mutable borrowing(&mut T)
  2. `std :: convert :: AsRef`。如您所见,AsRef它被归类在 `convert` 模块下。如果您查看文档,就会发现与类型转换相关的trait都定义在这个模块中。例如,我们熟悉的 `From/To`、`TryFrom/TryTo` 和 `AsRef/AsMut` 也成对出现在这里,表明该特性与类型转换相关。根据Rust API 指南中的命名规则,我们可以推断以 `std::convert::AsRef` 开头的方法表示从` std::convert` 到 `std::AsRef` 的as_转换,并且这些转换没有额外开销。而且,这样的转换不会失败。borrow -> borrowreference -> reference
  3. `std :: borrow :: Borrow`。如您所见,Borrow它被归类在 `borrow` 模块中。该模块的文档非常简略,只有一句话说明它是用于使用借用的数据。因此,该 trait 或多或少与表达借用语义有关。它提供了三个trait : `Borrow``BorrowMut``ToOwned`,它们恰好对应于所有权语义。
  4. `std :: borrow :: Cow`也属于Cow借用模块。根据描述,Cow它是一个写时克隆智能指针。将其放入借用模块的主要原因是尽可能使用借用机制,避免复制,从而实现优化。

std :: ops :: Deref

首先,我们来看一下这个特征的定义。



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


Enter fullscreen mode Exit fullscreen mode

这个定义并不复杂,Deref只包含一个deref方法签名。这个特性的妙处在于它是由编译器“隐式”调用的,官方名称是“解引用强制转换”。

以下是标准库中的一个示例。



use std::ops::Deref;

struct DerefExample<T> {
    value: T
}

impl<T> Deref for DerefExample<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

let x = DerefExample { value: 'a' };
assert_eq!('a', *x);


Enter fullscreen mode Exit fullscreen mode

在代码中,该DerefExample结构体实现了该Deref特性,因此可以使用解引用运算符执行它*。在本例中,字段值直接返回。

如你所见,`T`DerefExample具有类似指针的行为,因为它实现了 `Integer` 接口Deref,因此可以被解引用。DerefExample它也成为了一种智能指针。判断一个类型是否为智能指针的一种方法,就是看它是否实现了 `Integer` 接口Deref。但并非所有智能指针都实现了 `Integer`接口Deref,有些实现了 `Integer` 接口,有些Drop则两者都实现了。

现在我们来总结一下Deref

如果T实现了Deref<Target=U>,并且x是类型的实例T,则。

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

Rust 的妙处Deref在于它增强了 Rust 的开发体验。标准库中的一个典型例子是,它通过实现Vec<T>共享了 Rust 的所有方法sliceDeref



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) }
    }
}


Enter fullscreen mode Exit fullscreen mode

例如,最简单的方法len()实际上定义在模块中。在 Rust 中,当执行`call` 或在函数参数位置执行 `call` 时,编译器会自动执行隐式的解引用强制转换。因此,它等同于同时拥有`call` 方法。 slice .Vec<T>slice



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


Enter fullscreen mode Exit fullscreen mode

Rust 中的隐式行为并不常见,但Deref它是其中之一,其隐式强制转换使得智能指针易于使用。



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


Enter fullscreen mode Exit fullscreen mode

例如,如果我们操作 T Box<T>,而不是手动解引用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");
}


Enter fullscreen mode Exit fullscreen mode

上述方法的参数类型uppercase显然是&str,但主函数中实际传递的类型是&String,那么为什么它能编译成功呢?这是因为String实现了Deref



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

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


Enter fullscreen mode Exit fullscreen mode

这就是它的妙处所在Deref。但有些人可能会把它误认为是继承。大错特错。

这种行为看起来有点像继承,但请不要仅仅用它Deref来模拟继承。

std :: convert :: AsRef

让我们来看一下它的定义AsRef



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


Enter fullscreen mode Exit fullscreen mode

我们已经知道它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);
}


Enter fullscreen mode Exit fullscreen mode

在上面的例子中,`function`is_hello是一个泛型函数。转换是通过限定并在函数内部T: AsRef<str>使用显式调用来实现的,例如`function`。`function`或 `function`实际上都实现了该trait。s.as_ref()StringstrAsRef

所以现在的问题是,什么时候用AsRef?为什么不直接用&T

考虑这样一个例子。



pub struct Thing {
    name: String,
}

impl Thing {
    pub fn new(name: WhatTypeHere) -> Self {
        Thing { name: name.some_conversion() }
}


Enter fullscreen mode Exit fullscreen mode

在上面的例子中,new函数名的类型参数有以下几种选项。

  1. &str在这种情况下,调用方需要传入一个引用。但为了将其转换为字符串,被调用方(调用者)需要控制自身的内存分配,并且会保留一份副本。
  2. String在这种情况下,调用者传递字符串是可以的,但如果传递的是引用,则与情况 1 类似。
  3. T: Into<String>在这种情况下,调用者可以传递 `and`&str和 `or` String,但在类型转换期间也会有内存分配和复制。
  4. T: AsRef<str>与情况 3 相同。
  5. T: Into<Cow<'a, str>>其中一些分配是可以避免的,Cow稍后会详细说明。

何时使用哪种类型并没有统一的答案。有些人就是喜欢&str,无论如何都会使用某种类型。这里面存在着各种利弊权衡。

  1. 在赋值和复制不太重要的情况下,无需使类型签名过于复杂,只需使用即可&str
  2. 有些人需要查看方法定义,以及它们是否需要消耗所有权,或者返回所有权或借用权。
  3. 有些需要尽量减少赋值和复制,因此有必要使用更复杂的类型签名,如案例 5 所示。

Deref 和 A​​sRef 在 API 设计中的应用

wasm-bindgen库包含一个名为web-sys 的组件

该组件将 Rust 与浏览器 Web API 绑定在一起。因此,web-sys 使得使用 Rust 代码操作浏览器 DOM、获取服务器数据、绘制图形、处理音频和视频、处理客户端存储等成为可能。

然而,使用 Rust 绑定 Web API 并非易事。例如,操作 DOM 依赖于 JavaScript 类继承,因此 web-sys 必须提供对这种继承层次结构的访问。在 web-sys 中,可以通过 ` Derefand` 和 `.`来访问这种继承结构AsRef

使用 Deref



let element: &Element = ...;

element.append_child(..); // call a method on `Node`

method_expecting_a_node(&element); // coerce to `&Node` implicitly

let node: &Node = &element; // explicitly coerce to `&Node`


Enter fullscreen mode Exit fullscreen mode

如果你有web_sys::Element,那么你可以web_sys::Node通过使用解引用来隐式地获取。

使用 deref 主要是为了 API 的人体工程学考虑,使开发人员能够轻松地使用该.操作透明地使用父类。

使用 AsRef

AsRefweb-sys 还针对各种类型实现了大量转换。



impl AsRef<HtmlElement> for HtmlAnchorElement
impl AsRef<Element> for HtmlAnchorElement
impl AsRef<Node> for HtmlAnchorElement
impl AsRef<EventTarget> for HtmlAnchorElement
impl AsRef<Object> for HtmlAnchorElement
impl AsRef<JsValue> for HtmlAnchorElement


Enter fullscreen mode Exit fullscreen mode

可以通过显式调用来获取对父结构的引用.as_ref()

Deref 侧重于隐式且透明地使用父结构,而 AsRef 侧重于显式地获取对父结构的引用。这是一种针对特定 API 设计的权衡,而非对面向对象编程继承的简单模拟。

AsRef 的另一个应用实例是http-types库,它使用 AsRef 和 A​​sMut 来转换各种类型。

例如,Request 是 的组合Stream / headers/ URL,因此它实现了AsRef<Url>AsRef<Headers>AsyncRead。类似地,Response 也是 的组合Stream / headers/ Status Code。因此它实现了AsRef<StatusCode>AsRef<Headers>AsyncRead



fn forwarded_for(headers: impl AsRef<http_types::Headers>) {
    // get the X-forwarded-for header
}

// 所以,forwarded_for 可以方便处理 Request/ Response / Trailers 
let fwd1 = forwarded_for(&req);
let fwd2 = forwarded_for(&res);
let fwd3 = forwarded_for(&trailers);


Enter fullscreen mode Exit fullscreen mode

std :: borrow :: Borrow

请查看该词的定义Borrow



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


Enter fullscreen mode Exit fullscreen mode

对比AsRef



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


Enter fullscreen mode Exit fullscreen mode

这不是非常相似吗?因此,有人建议完全移除这两个函数中的一个。但实际上,Borrow 和 AsRef 之间存在区别,它们各有用途。

Borrow trait 用于表示借用的数据。AsRef trait 用于类型转换。在 Rust 中,通常会为不同的用例和语义提供不同的类型表示。

类型通过实现 `require`接口T在方法中提供引用/借用,表达了它可以被借用而非转换为其他类型的语义。类型可以自由地被借用为多种不同的类型,也可以以可变的方式借用。borrow()Borrow<T>T

那么,如何在借贷和参考引用之间进行选择呢?

  • 当您想要以统一的方式抽象不同的借用类型,或者想要创建一个以相同方式处理自包含值(拥有)和借用值(借用)的数据结构时,请选择“借用”。
  • 当您想要将类型直接转换为引用并且正在编写通用代码时,请选择 AsRef。更简单的情况。

事实上,标准库文档中给出的 HashMap 示例对此解释得非常清楚。让我来为你翻译一下。

HashMap<K, V>它存储键值对,其 API 应该能够使用键自身的值或其引用正确地检索 HashMap 中对应的值。由于 HashMap 需要对键进行哈希和比较,因此它必须要求键自身的值和引用在哈希和比较时表现一致。



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

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

impl<K, V> HashMap<K, V> {
    // The insert method uses the key's own value and takes ownership of it.
    pub fn insert(&self, key: K, value: V) -> Option<V>
    where K: Hash + Eq
    {
        // ...
    }

    // If you use the get method to get the corresponding value by key, you can use the reference of key, which is denoted by &Q here
    // and requires Q to satisfy `Q: Hash + Eq + ?Sized`
    // As for K, it is expressed as a borrowed data of Q by `K: Borrow<Q>`.
    // So, the hash implementation of Q is required to be the same as K
    pub fn get<Q>(&self, k: &Q) -> Option<&V>
    where
        K: Borrow<Q>,
        Q: Hash + Eq + ?Sized
    {
        // ...
    }
}


Enter fullscreen mode Exit fullscreen mode

Borrow 是对借用数据的约束,并与其他特征一起使用,例如示例中的Hashand 。Eq

请看另一个例子。



//  Can this structure be used as the key of a HashMap?
pub struct CaseInsensitiveString(String);

// It implements PartialEq without problems
impl PartialEq for CaseInsensitiveString {
    fn eq(&self, other: &Self) -> bool {
        // Note that the comparison here is required to ignore ascii case
        self.0.eq_ignore_ascii_case(&other.0)
    }
}

impl Eq for CaseInsensitiveString { }

// Implementing Hash is no problem
// But since PartialEq ignores case, the hash calculation must also ignore case
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)
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

CaseInsensitiveString 可以实现吗Borrow<str>

显然,`CaseInsensitiveString` 和 `str` 对哈希的实现不同。`str` 不会忽略大小写。因此,Borrow<str>`CaseInsensitiveString` 不能使用 `str` 作为哈希映射的键。如果我们强制Borrow<str>使用 `str` 会怎样?由于在确定键时存在大小写差异,映射将会失败。

但是 CaseInsensitiveString 可以完全通过 AsRef 实现。

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

std :: borrow :: Cow

请查看定义Cow



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


Enter fullscreen mode Exit fullscreen mode

如您所见,Cow 是一个枚举类型。它与 Option 有些类似,都表示两种情况之一。这里的 Cow 指的是借来的或自有的,但这两种情况只能发生其中一种。

牛的主要功能是:

  1. 充当智能指针,提供对该类型实例的透明不可变访问(例如,可以直接调用该类型的原始不可变方法,实现 Deref,但不能实现 DerefMut)。
  2. 如果需要修改此类型的实例,或者需要获得此类型的实例的所有权,Cow则提供克隆方法,避免重复克隆。

Cow旨在提高性能(减少复制次数)并增强灵活性,因为大多数业务场景都是读取多于写入。借助此功能Cow,可以以统一规范的形式实现这一点,即仅在需要写入时才执行一次对象复制。这可以显著减少复制次数。

掌握以下几个关键点至关重要。

  1. Cow<T>可以直接调用的不可变方法T,因为Cow枚举实现了Deref
  2. .to_mut()方法可用于在T需要修改时获得具有所有权值的可变借用。
    1. 请注意,调用该函数.to_mut()不一定会创建一个克隆对象。
    2. 如果所有权已存在,则调用.to_mut()是有效的,但不会创建新的克隆。
    3. 多次调用.to_mut()只会生成一个克隆体。
  3. .into_owned()当需要修改对象时,可以使用此方法创建新的自有对象T,这一过程通常意味着内存复制和创建新对象。
    1. 如果前一个值Cow处于借用状态,则调用此操作将执行克隆。
    2. 此方法的参数类型为self,它将“消耗”该类型的原始实例,之后该类型的原始实例的生命周期将结束,并且不能对调用多次Cow

在 API 设计中,牛的使用频率更高。



use std::borrow::Cow;

// Use Cow for the return value to avoid multiple copies
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);
}


Enter fullscreen mode Exit fullscreen mode

当然,何时使用 Cow 又回到了AsRef我们上一篇文章中讨论的“何时使用”的问题,这其中存在权衡取舍,没有一劳永逸的标准答案。

概括

要理解 Rust 中的各种类型和特性,你需要考虑所有权语义,并仔细阅读文档和示例,这些内容应该很容易理解。不知道这篇文章是否解答了你的疑问?欢迎分享你的反馈。

文章来源:https://dev.to/zhanghandong/rust-concept-clarification-deref-vs-asref-vs-borrow-vs-cow-13g6