实现具有内部可变性的索引
Posted
技术标签:
【中文标题】实现具有内部可变性的索引【英文标题】:Implementing indexing with interior mutability 【发布时间】:2020-01-11 18:44:38 【问题描述】:考虑一下,为了简单起见,我想实现一个具有 n 个连续元素 0,1,...,n-1 的可索引向量 v,即 v[i] = i。这个向量应该是按需填充的,也就是说,如果使用 v[i] 并且当前向量包含 n
下面的代码可以正常工作。
struct LazyVector
data: Vec<usize>
impl LazyVector
fn new() -> LazyVector
LazyVector
data: vec![]
fn get(&mut self, i:usize) -> &usize
for x in self.data.len()..=i
self.data.push(i);
&self.data[i]
pub fn main()
let mut v = LazyVector::new();
println!("v[5]=",v.get(5)); // prints v[5]=5
但是,上面的代码只是我尝试实现的实际结构的模型。除此之外,(1)我希望能够使用索引运算符,(2)尽管在访问位置时实际上可能会修改向量,但我希望这对用户是透明的,即也就是说,即使我对 v 有不可变引用,我也希望能够索引任何位置。不可变引用是首选,以防止其他不需要的修改。
要求 (1) 可以通过实现 Index trait 来实现,就像这样
impl std::ops::Index<usize> for LazyVector
type Output = usize;
fn index(&self, i: usize) -> &Self::Output
self.get(i)
但是,这不会编译,因为我们需要一个可变引用才能调用 LazyVector::get。由于要求 (2),我们不想让这个引用可变,即使我们这样做了,我们也不能这样做,因为它会违反 Index trait 的接口。我认为这可以通过 RefCell 智能指针实现内部可变性模式(如 The Rust Book 的第 15 章)。所以我想出了类似的东西
struct LazyVector
data: std::cell::RefCell<Vec<usize>>
impl LazyVector
fn new() -> LazyVector
LazyVector
data: std::cell::RefCell::new(vec![])
fn get(&self, i:usize) -> &usize
let mut mutref = self.data.borrow_mut();
for x in mutref.len()..=i
mutref.push(x)
&self.data.borrow()[i] // error: cannot return value referencing a temporary value
但这不起作用,因为它试图返回一个引用借用()返回的 Ref 结构的值,该结构在 LazyVector::get 的末尾超出范围。最后,为了避免这种情况,我做了类似的事情
struct LazyVector
data: std::cell::RefCell<Vec<usize>>
impl LazyVector
fn new() -> LazyVector
LazyVector
data: std::cell::RefCell::new(vec![])
fn get(&self, i:usize) -> &usize
let mut mutref = self.data.borrow_mut();
for x in mutref.len()..=i
mutref.push(x)
unsafe // Argh!
let ptr = self.data.as_ptr();
&std::ops::Deref::deref(&*ptr)[i]
impl std::ops::Index<usize> for LazyVector
type Output = usize;
fn index(&self, i: usize) -> &Self::Output
self.get(i)
pub fn main()
let v = LazyVector::new(); // Unmutable!
println!("v[5]=",v.get(5)); // prints v[5]=5
现在它可以按要求工作,但作为新手,我不太确定 unsafe 块!我想我正在用一个安全的界面有效地包装它,但我不确定。所以我的问题是这是否可行,或者是否有更好、完全安全的方法来实现这一目标。
感谢您的帮助。
【问题讨论】:
由于您返回对usize
的引用,如果您的代码按原样工作,它将扩展向量并在对usize
的引用存在时重新分配向量中的内存,这会导致无效的内存访问。如果你想这样做,你需要返回 usize
而不是引用,这意味着你不能使用 Index
特征。
不安全块不健全。添加到向量可能会导致它重新分配,因此引用可能最终成为悬空指针。这是当发生变异的方法采用 &mut self
时,Rust 保护你的事情之一。
无论您在这里做什么,都会变得非常复杂。这应该暗示你正在尝试一些奇怪的东西,你应该重新考虑为什么你甚至需要这个。
天哪!呸!现在很明显,你指出来了。我非常专注于应该在真实场景中使用它的方式,以至于我错过了这个明显的问题。 (见 cmets 到下一个答案)
【参考方案1】:
编辑由于您提供了有关您的目标的更多信息(对位于磁盘上的大文件块的延迟访问),我更新了我的答案。
您可以使用(如您尝试的那样)单元格。我引用doc:
由于细胞类型在原本不允许的情况下启用突变,因此在某些情况下内部可变性可能是合适的,甚至必须使用,例如[...] 逻辑不可变方法的实现细节。 [...]
这是完成这项工作的一段代码(请注意,这与您编写的内容非常接近):
use std::cell::RefCell;
use std::ops::Index;
// This is your file
const DATA: &str = "Rust. A language empowering everyone to build reliable and efficient software.";
#[derive(Debug)]
struct LazyVector<'a, 'b>
ref_v: RefCell<&'a mut Vec<&'b str>>
impl<'a, 'b> LazyVector<'a, 'b>
fn new(v: &'a mut Vec<&'b str>) -> LazyVector<'a, 'b>
LazyVector
ref_v: RefCell::new(v)
/// get or load a chunk of two letters
fn get_or_load(&self, i: usize) -> &'b str
let mut v = self.ref_v.borrow_mut();
for k in v.len()..=i
v.push(&DATA[k * 2..k * 2 + 2]);
v[i]
impl<'a, 'b> Index<usize> for LazyVector<'a, 'b>
type Output = str;
fn index(&self, i: usize) -> &Self::Output
self.get_or_load(i)
pub fn main()
let mut v = vec![];
let lv = LazyVector::new(&mut v);
println!("v[5]=", &lv[5]); // v[5]=ng
println!(":?", lv); // LazyVector ref_v: RefCell value: ["Ru", "st", ". ", "A ", "la", "ng"]
println!("v[10]=", &lv[10]); // v[10]=ow
println!(":?", lv); // LazyVector ref_v: RefCell value: ["Ru", "st", ". ", "A ", "la", "ng", "ua", "ge", " e", "mp", "ow"]
与您的尝试的主要区别在于底层Vec
是一个外部可变向量,而LazyVector
在此向量上仅获得一个(可变)引用。 RwLock 应该是处理并发访问的方式。
但是,我不推荐该解决方案:
首先,您的底层Vec
将迅速增长并变得与磁盘上的文件一样大。因此,您将需要一个地图而不是矢量,并将该地图中的块数保持在给定边界之下。如果您要求一个不在内存中的块,您必须选择一个要删除的块。这只是Paging,操作系统通常比你更擅长这个游戏(见page replacement algorithm)。正如我在评论中所写,memory mapped files(在“繁重”进程的情况下可能是shared memory)会更有效:操作系统处理文件的延迟加载和只读数据的共享。 R. Sedgewick 在Algorithms in C 第一版第 13 章“更简单的方法”一节中的评论解释了为什么对大文件(比内存大)进行排序可能比想象的更容易:
在良好的虚拟内存系统中,程序员可以处理大量数据,让系统负责确保在需要时将寻址数据从外部存储传输到内部存储。
其次,请看下面我之前的回答。
以前的答案
我曾经用 Java 编写过这种向量。用例是表示一个非常稀疏的网格(许多 只有几个单元格宽的行,但网格应该有 1024 的宽度)。避免 为了必须在需要时手动添加单元格,我创建了一个“列表”,大致完成了您尝试实现的目标(但只有一个默认值)。
起初,我让我的列表实现了List
接口,但我很快意识到我必须编写很多无用(而且速度很慢)的代码才能不破坏Liskov substitution principle。更糟糕的是,某些方法的行为对通常的列表具有误导性(ArrayList
、LinkedList
、...)。
您似乎处于相同的情况:您希望您的 LazyVector
看起来像通常的 Vec
,这就是您想要实现 Index
和可能 IndexMut
特征的原因。但是您正在寻找解决方法来实现这一点(例如,unsafe
代码以匹配特征方法签名)。
我的建议是:不要试图让LazyVector
看起来像一个普通的向量,但要明确LazyVector
不是一个普通的向量。这是Principle of least astonishment。例如。用get_or_extend
替换get
(预计只能由用户善意地读取数据),这清楚地表明要么你得到一些东西,要么你创造它。
如果你添加一个get_or_extend_mut
函数,你得到的东西不是很吸引人,但高效且可预测:
impl LazyVector
fn new() -> LazyVector ...
fn get_or_extend(&mut self, i: usize) -> &usize ...
fn get_or_extend_mut(&mut self, i: usize) -> &mut usize ...
【讨论】:
感谢所有 cmets。确实非常明智的建议。然而,这实际上是我试图用更简单的术语来解释这个问题。实际上,这应该是位于磁盘上且不需要立即加载到内存中的大型文本数据的代理。我只需要在任何给定时间对其中的一部分进行只读访问,而且我还没有考虑并发访问。与我的简化相反,一般来说,我希望引用文本片段,而不是单个字母,这就是为什么我不想像第一条评论中(正确地)建议的那样返回元素的拥有副本。 非常重要的是,我努力的全部目的是能够使用 Index trait 和随之而来的语法糖。这将极大地简化事情并使代码更易于阅读:(我开始担心这不可能......干杯! @Paulo 所以你想通过索引读取大文本片段吗? 1.你有没有想过内存映射文件(见en.wikipedia.org/wiki/Memory-mapped_file和***.com/questions/28516996/…)?操作系统会照顾你的延迟加载 2。你能详细说明“极大地简化事情”吗?因为在我看来差异并不那么重要([i]
vs get_or_load(i)
)。
@jferad 好的,“非常”可能被夸大了,但我的意思是能够在任何带有下标语法的“可索引”文本上使用相同的代码会很棒。我来自 C 语言,这就像lazy_vec_get_or_load(v, i),所以简单地使用 v[i] 会很简洁。我认为您建议的 get_or_load 基本上是我第一个版本中的 get,如果没有更好的方法也可以。至于内存映射文件,我见过它,但它似乎没有实现 Index 特征,正如我所说,这很重要。所以我以为我可以制作自己的东西,但显然我错了。再次感谢!以上是关于实现具有内部可变性的索引的主要内容,如果未能解决你的问题,请参考以下文章