Rust中的引用

Rust是有三种引用的




好的,我会按照您的要求整理这段内容,使其变得合理通顺,同时不遗漏任何内容。以下是整理后的内容:

我们来看一篇吐槽文章。这篇文章是23号写的,时间不久,是上个月的。原文链接有兴趣可以看一下,作者是DAVINCHAMPIRE,应该是在谷歌工作的一个人。

与其他一些带有GC的语言不同,Rust提供了手动控制值如何存储和销毁的机制,这带来了可预测性能和内存安全方面的优势。避免可变别名是其中一个原因,但这种方式的代价就是控制的复杂性和多种存储类型的存在。

在像Python这样的语言中,只有一个用于存储值的类型,就是垃圾回收引用。这些引用总是可复制、可移动的,并且可以在线程之间传递等等,程序员不需要做选择。而在Rust中,你有多种选择,但每一种选择都需要权衡取舍。比如说如果你要用RC,这无法跨线程发送数据或对它进行修改,而使用Box就无法克隆数据。总而言之,这些引用类型之间互不兼容。如果你写了一个函数,期望传入RC或者Box类型的参数,那么你就不能够轻易地用&T类型的变量来调用这个函数,它们之间不太兼容。

存储类型种类繁多并非Rust的独有,C++也面临着类似的问题。C++提供了多种用于存储值的类型,比如说T*、Shared_ptr、Unique_ptr、Weak_ptr等,甚至还有更多正在提案标准化的类型,比如说Indirect和Polymorphic

显然就是不可能处理所有可能的类型。正如Howard Hinnant在文章N3339中提出的那样,如果函数的参数类型是引用,那么你的代码可以适用于任何调用者,因为每个调用者都可以将它使用的值转化为引用。引用就像是一种通用接口。他的原文叫”Because converting to a reference is a narrow waist”。”Narrow waist”是指这个比较瘦的腰,像一个很瘦的腰,表示很窄的地方。我们往往就是预设就是承上启下、可以通用的一个地方,比较窄一点的地方,就是大家都可以用。

所以他可以说引用就像是一个通用接口。在C++中是这样的,在Rust当中也是这样。使用引用类型的策略在C++当中效果相当不错。C++中的一切最终都可以归结为三种不同的引用类型:T&引用的是可变值的引用,const T&是不可变值的引用,T&&是指右值引用。也存在const T&&这种引用类型,但出于忽略volatile的相同原因,这里也忽略它。如果你的接口接受这些类型,那么它几乎可以用于任何存储方式或者存储位置的值,甚至是没有存储位置的临时值。

Rust则试图将可接受的引用类型简化为两种:&T和&mut T。也就是说无论你的对象怎么样去存储,都期望可以获得一个&T的引用,对于可变对象,还可以获得一个&mut T的引用。例如,如果你想实现索引这个运算符[],那么只有两个trait: Index和IndexMut,它们分别接受&self或者&mut self作为参数。Rust的设计期望任何对象都可以转化为这两种引用类型之一,没有第三种引用类型可以对应的第三个trait。

遗憾的是确实存在第三种引用类型,也就是Pin<&mut T>。这个就真正能讲到他这篇文章要讲的东西了,就是为复杂和不便来寻找荒唐的借口。好像只有两种,但事实上其实你会发现有三种。

Rust提供了一种特殊的引用类型Pin<&mut T>,适用于内存位置不能改变的类型。关于Pin的文章我们看到过好多了,视频也发了好多个了。因为它其实我们仔细想来,它也是很好理解的一个东西。

举个例子,想象一个结构体struct MyStruct { a: A, b: *mut B },成员b可能指向成员a,也就是一个自引用的结构体。就是我这个b的指针指向了a,形成一个自引用。所以是因为它是一个自引用,所以说如果你移动整个的这个结构体的话,那指针b它就失效了。因为它指的是原来的这个物理地址,你一旦整个一挪动了,你这结构体一整个一挪动了,你这个b的指针指的是原来的物理地址,现在物理地址变了,它就指针b就失效了。这就是自引用结构体的问题。

所以为了防止这一类内存错误,Rust就引入了这个Pin<&mut T>的引用。所以他把这个Pin住,不要让它移动。其实我们可以看到这个Pin它就是一个marker。从编译器角度来讲,就是你要跟这个编译器说一声,这个东西这个地方不能动。事实上哪怕你可以动,但跟你说一声,就像我们经常会跟谁临时说,”这个东西你不能动”。你每次要这么去做的话,你就要写个Pin过去。来个Pin,这个东西不能动,请勿动。

这个东西它不是一个规则性的一个东西,它也不是一个大家约定俗成了什么都不需要谁打招呼就是怎么样的一个规则。它不是,你一定要告诉他跟编译器说一声,否则编译器不会出错,它就不管你这个事。所以这个是它的一个作用就在这里,Pin就是干这个的。

从语法上看,这个Pin<&mut T>就像是&mut T的一种封装,但两者之间不能直接转换。而且在安全代码中不允许这种转换,Pin<&mut T>必须替代&mut T的使用。Pin还引申出一系列Pin的指针类型,比如说Pin<Box>、Pin<Rc>等,它们与&mut T的关系就好像是Box与&mut T的关系一样。

这种设计非常巧妙。Pin<&mut T>不是Rust的内置类型,也就是说它不是一个对于编译器来讲已经约定俗成的规则。它不是这么个东西,如果要是Pin的话,你要跟编译器去打招呼,每次都要去跟它去打这个招呼。它也没有说是哪个东西我一旦是什么东西我就能推导它谁是Pin的,它没有这个能力。所以没有相应的特殊规则,没有相应的trait给它去做这个事的。

因此,&mut T的这些优点在Pin<&mut T>上并不适用。就是说能在&mut T上面使用的一些东西,在这个Pin<&mut T>上面并不适用。

比如说我定义了一个Pin的数组类型Pin<[T; N]>,它类似于普通数组[T; N],但是它是结构性的Pin。也就是说给定一个指向Pin<[T; N]>的Pin<&mut [T; N]>,你可以获取到数组中某个元素的Pin<&mut T>引用,因为数组本身的Pin属性会传递到它的元素上。这跟普通数组&mut [T; N]获取元素的&mut T引用类似。

C++中类似于Pin<[T; N]>的类型是std::array<T, N>,它原生支持这个Pin的功能。那么使用这种新型的数组类型,Rust API会是什么样的呢?

看上去这个Pin<&mut T>和普通&mut T一点都不像。Pin<&mut T>是一种和&mut T相似的引用类型,但是两者之间不能直接安全地相互转换。所以所有原本支持&mut T的语言特性和trait都没有办法直接用于Pin的引用。

比如说#[derive(DerefMut)]只支持&mut T的自动解引用,DerefMut也没有办法用于Pin<&mut T>。比如说调用者拿着一个Box<[T; N]>,可以直接调用它可变引用的get_mut方法,因为数组可以直接隐式转换成切片,所以get_mut方法实际上是在[T]上的实现。但是对于这个Pin<Box<[T; N]>>就没有自动解引用这一说了,调用者必须显式地写成x.as_mut().get_mut(),而且get_mut方法也需要针对Pin的切片和Pin的数组分别去实现。

类似的,语法层面这些自动的、但凡跟自动相关的、自动的重引用的特性也只适用于&mut T和&T。即使这个x本身已经是Pin值的引用,在调用方法的时候也必须显式地去重引用它,要写成x.as_mut().get_mut()。如果不写,你就会发现你这个Pin的引用并没有被重引用,而是被整体地移动到方法调用里面去了。它没有这种自动的去处理的东西了。

前面也提到了,IndexMut只支持&mut T,所以我们不能用[]操作符来实现数组访问。这个问题很普遍,任何你之前见过的支持&mut T的方法或者函数,都不会支持Pin的引用。

有时候这是好事,比如说std::mem::swap函数,这正是Pin的意义所在。但是对于索引和运算符重载来说,它就不是好事儿了。甚至存在一个专门的第三方库,用于重新实现Pin的引用访问字段的功能。就是我要访问字段,我还要去写一下,它不会自动地去Pin它。还有好几个crate用于安全地创建Pin的局部变量,其中一个就是我写的。

别无选择了,就是Rust并不是只有&mut T和&T两种引用类型。因为对于以上场景,我们都不能把Pin的引用简化为两种类型中的任何一种。实际上Rust是有三种引用类型:共享引用、可变引用和Pin的引用。遗憾的是,Rust语言本身只针对前两种做了特殊处理,所以Pin的引用是没有办法像出很多你原本期望的语言特性,经常需要借助大量复杂难懂的第三方库代码才能达到想要的效果。

不过听起来也没那么糟,因为实际上像这种Pin的使用并不是很常用,并不是很常见。但是我正在做,因为这个本文的作者是在做C++和Rust的互操作,这个项目名字叫Rabbit。他用的是CRIT,他在写这个Rabbit是做C++和Rust互操作的。

所以在C++中,几乎所有复杂类型都是Pin的。如果互操作做得很好,并且代码库需要频繁地在C++和Rust之间通信,那么这个Pin<&mut T>可能和&mut T一样常见。这就是真是成也萧何败也萧何,就是Rust语言的强大特性,在某些场景下反而变成了累赘。

其实事实上我们可以看到这里,这就是C++和Rust之间的歧义,它们之间的一个冲突。其实你看他如果他俩要非常好的去做互操作的话,那么在Rust上面的安全性的操作,可能就你如果要跟C++很同步的话,那安全上面可能要牺牲很多东西。

当然这个对于他这个工作来讲,他就很难受。他做这个CRIT的时候就比较难受,因为很多处理它就不一样了。你本来希望它是自动的,但是它没有这个自动。我们就是要临时的去跟编译器去打招呼,但是他得不停的去做这种招呼。

如果Pin<&mut T>拥有和&mut T一样的易用性,那么我们肯定会去修复&mut T。没有哪一个语言能够承受核心数据类型这么的不便捷。只有在罕见的情况下,这种接口才明显可以接受。所以如果Pin在某些代码库中并不罕见,如果它真的是第三个核心引用类型,那么就应该修复它。但是在Rust当中它的确不是常见的。

那么Pin的这种困境能解决吗?上面列举了使用Pin遇到的一些具体问题,每个问题都可以通过相对针对性的方法来解决。像#[derive(DerefMut)]及语法糖、隐式转换、自动重引用、自动投影都不支持这个Pin<&mut T>。就是标准库中的trait通常只接受&mut T,没有等价的类型用到这个Pin<&mut T>上面。

最显而易见的解决办法就是在语言层面加入一个Pin的引用,就是多一个引用了。将Pin的底层类型从&mut T改成Pin&T,并为所有trait添加引用Pin的版本,同时在引用语法中去支持它。真要如果完成他这种愿望的话,那真要如果完成他这种愿望的话,那改动是相当大了。因为那么多的trait,那么多的东西都去支持他,这个工作量就不是一般的大了。

但是这种方法似乎不够通用,毕竟还可能有其他类似于引用的类型需要支持。这篇文章主要关注的是Pin的引用,但请考虑一下情况:就是对于C++和Rust的互操作,有些人(比如我)希望支持右值引用。而由于它们不是内置类型,所以会遇到很多和Pin<&mut T>相似的问题。更不用说别名引用了,它比&mut T的问题还糟糕。

因此,一种不太明显的方法是让#[derive(DerefMut)]变得更加通用,这样任何定义新引用类型的都可以像使用&mut T一样享受相同的语法糖,甚至可以通过像IndexMut这样的trait来访问它们。但是老实说我不知道应该怎么做。

还有一种最不明显的方法,也是我有点不好意思承认,但我喜欢的就是:直接向Rust添加所有你想要的引用类型。当然想要一个易用的固定类型,那就加上这个Pin&T吧。想要C++右值引用,那就加上这个cpp::RValue&T吧。别名引用也想要?说得有道理,那就加上&alias T吧。这种方式极其临时,尤其是右值引用除了C++互操作之外,没有任何理由。但如果它愚蠢但有效,那么它就不是愚蠢了。

他可能这一点我可以,我们就可以感觉到就是C++用户的习惯,就很喜欢自由,然后也喜欢这个辩解。但是便捷和安全是一对矛盾。

所以这篇文章可以看得出,就基本上对于这个Pin这块,因为它不是内置的类型,所以说有一些吐槽在上面。也是比较有意思的一个点。毕竟像他这种互操作的项目可能并不是那么的多,但在实际当中可能会遇到类似这样的一个问题。



到底Rust的自动解引用规则是啥



好的,我会按照您的要求整理内容,使其变得合理通顺,同时不遗漏任何内容。以下是整理后的内容:

Rust的自动解引用规则是什么?这个问题在过去8年多来一直是个热门话题。让我们来理解一下Rust的自动解引用规则到底是什么样子的。

问题提出者说:我正在学习和实验Rust。在我发现这门语言的优雅之处的同时,有些地方让我困惑,并似乎完全不合逻辑。就是Rust在调用方法的时候会自动解引用指针。我做了一些测试,来搞清楚它确切的行为是什么样子的。

他写了一段测试代码:

  1. 定义了一个结构体X,里面只有一个i32的值。
  2. 为X实现了Deref trait。
  3. 定义了一个trait M,分别为X、&X、&&X、&&&X实现了这个trait。
  4. M trait中定义了两个方法,一个是m(),另一个是ref_m(&self)。
  5. 又定义了一个结构体Y,与X完全相同,包括其Deref实现。
  6. 定义了一个结构体Z,其类型是Y。
  7. 实现了一些其他的trait如Clone、Copy等。
  8. 在main函数中进行了一系列测试调用。

通过这个测试,他得出了以下结论:

  1. 编译器会插入尽可能多的解引用操作符来调用方法。
  2. 当编译器解析使用&self(按引用调用)声明的方法时:
    • 首先尝试对self进行一次解引用来调用方法。
    • 如果失败,只会尝试调用self的确切类型的方法。
    • 如果还是失败,那么会尝试插入尽可能多的解引用操作符,直到找到匹配的方法。
  3. 对类型T使用self声明的方法,其行为就像是对类型&T使用&self声明并在.操作符左侧的引用上调用的。
  4. 先用内置的原生解引用尝试上面这些规则,如果没有匹配到,再使用Deref trait,再使用raw trait。

一个回答者给出了更详细的解释:

算法的核心是解引用。对于每一个解引用步骤U:

  1. 设U等于T,然后U=*T(T是原始变量foo的类型)
  2. 如果存在方法bar的接收者类型(即self的类型)与U完全匹配,那么就使用它(按值的方法)
  3. 否则对接收者添加一次自动引用(注意不是解引用),如&或&mut。如果某个方法的接收者匹配到这个&U,那么就会使用它(自动引用方法)

注意:所有规则都考虑方法的接收者类型,而不是trait的Self类型。

如果在内部步骤中有多个有效的trait方法,就会出现错误。固有方法优先于trait方法。如果循环结束时没有找到匹配的方法,也会出错。递归的Deref实现也会导致错误,因为它会使循环无限进行。

关于自动引用,之所以只添加一次自动引用,有以下两点原因:

  1. 如果没有限制不停地加引用,情况会变得糟糕或缓慢,因为每个类型都可以被任意次引用。
  2. 取一次引用&foo会保持与foo的强连接(foo本身的地址),但取更多引用会逐渐失去这种连接。&&foo是存储&foo的某个临时栈变量的地址。

然后回答者给出了几个具体的例子来说明这些规则如何应用。

另一个评论者提供了一个简短易懂的总结:

  1. 解引用会尽可能多地进行。例如: &&&&String会被解引用4次。
  2. 最多自动引用一次。

最后,有人引用了Rust参考手册中关于方法调用表达式的部分,进一步解释了这个过程:

  1. 首先构建候选接收者类型的列表。
  2. 对于每个候选类型T,在T之后立即将&T和&mut T添加到列表中。
  3. 对于每个候选类型T,在以下位置搜索具有该类型接收者的可见方法:
    • T的固有方法
    • T实现的任何可见trait提供的方法

这就是Rust中自动引用和自动解引用的基本规则。解引用会尽可能多地进行,而自动引用只会进行一次。

好的,我会继续整理剩余的内容。以下是接续的部分:

这位回答者还给出了一个具体的例子来说明这个过程:

假设我们有一个类型Box,那么它的候选类型列表将会是:

  1. Box
  2. &Box
  3. &mut Box
  4. i32 (通过解引用Box)
  5. &i32
  6. &mut i32
  7. i32 (通过无大小强制转换)
  8. &i32
  9. &mut i32

对于每个候选类型,Rust会按照上述顺序搜索可见的方法。

然后,回答者详细解析了原问题中提供的代码例子。以下是其中一个例子的解析:

对于x.m()这个调用,其中x的类型是i32:

  1. 接收者表达式的类型是i32
  2. 创建候选类型列表:[i32](i32不能被解引用,所以列表只有一个元素)
  3. 搜索每个候选类型的方法,找到impl M for i32中的m方法,其接收者是i32,匹配成功

回答者还解析了其他几个例子,但我们就不一一详细介绍了。

总的来说,Rust的自动引用和自动解引用规则可以总结为:

  1. 解引用会尽可能多地进行
  2. 自动引用最多只会进行一次

这个规则的原因我们之前已经讨论过了。这种设计使得Rust能够在保持类型安全的同时,提供更加便利的语法。

这就是关于Rust自动解引用规则的全部内容。这个问题虽然看起来复杂,但理解了其背后的原理后,就能更好地使用Rust语言,写出更加简洁和高效的代码。