簡體   English   中英

利用內部可變性實現索引

[英]Implementing indexing with interior mutability

為了簡單起見,請考慮使用n個連續元素0,1,...,n-1(即v [i] = i)實現可索引向量v。 該向量應該按需填充,也就是說,如果使用v [i]且當前向量包含n <i + 1個元素,則首先推送值n + 1,n + 2,...,i到v上,然后返回對v [i]的引用。

下面的代碼工作正常。

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有不可變的引用,我也希望能夠對任何位置建立索引。不可變的引用是首選,以防止其他不必要的修改。

可以通過實現Index特質來實現要求(1),就像這樣

impl std::ops::Index<usize> for LazyVector {
    type Output = usize;
    fn index(&self, i: usize) -> &Self::Output {
        self.get(i)
    }
}

但是,由於我們需要可變的引用才能調用LazyVector :: get,因此無法編譯。 由於要求(2),我們不想使該引用可變,即使我們這樣做,我們也不能這樣做,因為它會違反Index特質的接口。 我認為這將通過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
    }
}

但是,這是行不通的,因為它試圖返回一個值,該值引用LazyVector :: get末尾超出了row()范圍的row()返回的Ref結構。 最后,為了避免這種情況,我做了類似的事情

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
}

現在它可以按要求工作,但是作為一個新手,我不太確定不安全的功能! 我認為我實際上是用安全的界面將其包裝起來的,但是我不確定。 所以我的問題是這是否可以,或者是否有更好,完全安全的方法來實現這一目標。

謝謝你的幫助。

編輯由於您提供了有關您的目標的更多信息(對磁盤上巨大文件的大塊文件的懶惰訪問),我更新了我的答案。

您可以使用(嘗試時)單元格。 我引用文檔

由於單元格類型可以實現原本不允許的突變,因此有時可能適合內部變異,甚至必須使用內部變異,例如,邏輯上不變的方法的實現細節。 [...]

這是完成任務的一段代碼(請注意,這與您編寫的內容非常接近):

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將迅速增長並變得與磁盤上的文件一樣大。 因此,您將需要一個映射而不是一個向量,並將該映射中的塊數保持在給定的邊界下。 如果您請求的內存塊不在內存中,則必須選擇一個要刪除的塊。 這僅僅是分頁,並且在這個游戲上,操作系統通常比您更好(請參閱頁面替換算法 )。 正如我在評論中所寫, 內存映射文件 (在“繁重”進程的情況下可能共享內存 )將更加高效:操作系統處理文件的延遲加載和只讀數據的共享。 R. Sedgewick 在《 C語言中的算法》第一版第13章“更簡單的方法”中的評論解釋了為什么對一個大文件(大於內存)進行排序可能比一個想法容易:

在一個好的虛擬內存系統中,程序員可以處理大量數據,而系統有責任確保在需要時將所訪問的數據從外部存儲轉移到內部存儲。

其次,請參閱下面的我以前的答案。

上一個答案

我曾經用Java編寫過這種矢量的代碼。 該用例代表一個非常稀疏的網格(許多行中只有幾個單元格寬,但是該網格的寬度應該為1024)。 為了避免在需要時手動添加單元格,我創建了一個“列表”,該列表大致上做了您嘗試實現的目標(但只有一個默認值)。

最初,我使我的列表實現了List接口,但是我很快意識到我必須制作許多無用(且緩慢)的代碼,才能打破Liskov替換原則 更糟糕的是,某些方法的行為對於常規列表( ArrayListLinkedList ,...)具有誤導性。

看來您處在相同的情況:您希望LazyVector看起來像通常的Vec ,這就是為什么要實現Index以及IndexMut特性的原因。 但是,您正在尋找解決方法來實現此目的(例如,與traits方法簽名匹配的unsafe代碼)。

我的建議是:不要試圖使LazyVector看起來像通常的向量,但要明確指出LazyVector不是通常的向量 這是最小驚訝原則 例如,用get_or_extend替換get (預期僅由用戶真誠地讀取數據)由get_or_extend ,可以清楚地知道要么得到,要么創建。 如果添加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 { ... }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM