在可变成员上迭代时访问不可变方法



在了解Rust中的迭代器时,我创建了以下结构来隐藏二维集合的实现:

use std::slice::{Items, MutItems};
use std::vec::{Vec};
pub struct Table<T> {
    pub width: uint,
    pub height: uint,
    data: Vec<T>
}
impl<T: Clone> Table<T> {
    pub fn from_elem(width: uint, height: uint, value: T) -> Table<T> {
        Table {
            width: width,
            height: height,
            data: Vec::from_elem(width * height, value)
        }
    }
}
impl<T> Table<T> {
    pub fn get_row_column(&self, index: uint) -> (uint, uint) {
        (index / self.width, index % self.width)
    }
    pub fn iter<'a>(&'a self) -> Items<'a, T> {
        self.data.iter()
    }
    pub fn iter_mut<'a>(&'a mut self) -> MutItems<'a, T> {
        self.data.iter_mut()
    }
}

iteriter_mut方法的目标是该结构的用户不需要担心数据是以行主格式还是列主格式存储的;迭代器将简单地以最有效的顺序提供元素。

然而,在使用这种数据结构时,我经常需要知道特定的行和列,以便获得一些外部数据:

fn get_input(row: uint, column: uint) -> uint {
    row * 10 + column / 2
}
fn main() {
    let mut table = Table::from_elem(640, 480, 0u);
    for (index, value) in table.iter_mut().enumerate() {
        let (row, column) = table.get_row_column(index);
        *value = get_input(row, column);
    }
}

但是,当我尝试调用get_row_column方法时,我会得到以下编译器错误:

main.rs:56:33: 56:38 error: cannot borrow `table` as immutable because it is also borrowed as mutable
main.rs:56             let (row, column) = table.get_row_column(index);
                                           ^~~~~
main.rs:55:31: 55:36 note: previous borrow of `table` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `table` until the borrow ends
main.rs:55         for (index, value) in table.iter_mut().enumerate() {
                                         ^~~~~
main.rs:59:6: 59:6 note: previous borrow ends here
main.rs:55         for (index, value) in table.iter_mut().enumerate() {
main.rs:56             let (row, column) = table.get_row_column(index);
main.rs:57             *value = get_input(row, column);
main.rs:58         }
main.rs:59     }
               ^

完成我在这里要做的事情的正确方法是什么我可以添加一个set方法,该方法获取行和列的编号,并显式地在行和列索引上循环,但用户必须担心行主排序与列主排序:

impl<T> Table<T> {        
    fn get_index(&self, row: uint, column: uint) -> uint {
        row * self.width + column
    }
    pub fn set(&mut self, row: uint, column: uint, value: T) {
        let index = self.get_index(row, column);
        self.data[index] = value;
    }
}
fn main() {
    let mut table = Table::from_elem(640, 480, 0u);
    for row in range(0, table.height) {
        for column in range(0, table.width) {
            table.set(row, column, get_input(row, column));
        }
    }
}

是否有一种约定或最佳实践可以在允许访问不可变成员和方法的同时更改结构的内部成员?或者这完全违反了安全保障?

Matthieu M.有一个正确的想法:使用迭代器返回rowcolumn,而不是直接公开index。不幸的是,Rust中的闭包目前似乎是堆栈分配的,不能扩展到创建它们的堆栈框架之外,所以他提出的解决方案没有编译。

虽然使用迭代器适配器非常方便和简洁,但我们仍然可以通过创建新的迭代器对象来实现这一点。

关键是创建一个迭代器来跟踪我们需要的上下文,在这种情况下,它是表的维度:

pub struct TableItems<'a, T: 'a> {
    iter: Items<'a, T>,
    width: uint,
    height: uint
}
impl<'a, T> Iterator<&'a T> for TableItems<'a, T> {
    #[inline]
    fn next(&mut self) -> Option<&'a T> {
        self.iter.next()
    }
}

此结构包含slice模块提供的Items迭代器,以及表中的widthheight。实现Iterator特性就像将对next的调用传递给内部迭代器一样简单。

TableItems结构只返回不可变的引用,但我们可以为可变引用创建一个类似的引用:

pub struct MutTableItems<'a, T: 'a> {
    iter: MutItems<'a, T>,
    width: uint,
    height: uint
}
impl<'a, T> Iterator<&'a mut T> for MutTableItems<'a, T> {
    #[inline]
    fn next(&mut self) -> Option<&'a mut T> {
        self.iter.next()
    }
}

然后,我们只需要添加一种将维度从Table对象传递到迭代器的方法:

impl<T> Table<T> {
    pub fn iter<'a>(&'a self) -> TableItems<'a, T> {
        TableItems {
            iter: self.data.iter(),
            width: self.width,
            height: self.height
        }
    }
    pub fn iter_mut<'a>(&'a mut self) -> MutTableItems<'a, T> {
        MutTableItems {
            iter: self.data.iter_mut(),
            width: self.width,
            height: self.height
        }
    }
}

现在,这些迭代器本身并没有真正为我们带来任何好处;它们返回Table的值,但我们仍然没有rowcolumn。为了实现这一点,我们可以添加我们自己的迭代器适配器,通过增加当前行和列的单独计数来模拟iter模块中的Enumerate特性:

impl<'a, A, T: Iterator<A>> Iterator<((uint, uint), A)> for TableEnumerate<T> {
    fn next(&mut self) -> Option<((uint, uint), A)> {
        match self.iter.next() {
            Some(value) => {
                let ret = Some(((self.row_count, self.column_count), value));
                self.column_count += 1;
                if self.column_count == self.width {
                    self.row_count += 1;
                    self.column_count = 0;
                }
                ret
            },
            None => None
        }
    }
    #[inline]
    fn size_hint(&self) -> (uint, Option<uint>) {
        self.iter.size_hint()
    }
}

这个适配器是通用的,所以它可以用于TableItemsMutTableItems(或者我们将来选择的任何其他适配器)。

最后一步是创建返回TableEnumerate:实例的方法

impl<'a, T> TableItems<'a, T> {
    pub fn enumerate_2d(self) -> TableEnumerate<TableItems<'a, T>> {
        let width = self.width;
        let height = self.height;
        TableEnumerate {
            iter: self,
            width: width,
            height: height,
            row_count: 0,
            column_count: 0
        }
    }
}
impl<'a, T> MutTableItems<'a, T> {
    pub fn enumerate_2d(self) -> TableEnumerate<MutTableItems<'a, T>> {
        let width = self.width;
        let height = self.height;
        TableEnumerate {
            iter: self,
            width: width,
            height: height,
            row_count: 0,
            column_count: 0
        }
    }
}

我很想将这些命名为enumerate,但编译器似乎在这之前找到了enumerateIterator实现。

有了这个完整的解决方案,表格可以这样访问:

fn get_input(row: uint, column: uint) -> uint {
    row * 10 + column / 2
}
fn main() {
    let mut table = Table::from_elem(640, 480, 0u);
    for ((row, column), value) in table.iter_mut().enumerate_2d() {
        *value = get_input(row, column);
    }
}

这不是一个可变的、不可变的问题,它只是一个双重借用。如果内部方法调用是&mut self方法,则会出现同样的问题。您没有失去对不可变方法的访问权限,只要value在作用域中,您就失去了对所有方法的访问权,因为value是表中的借位。

虽然在这种特殊情况下不会发生这种情况,但对正在迭代的片段使用多个别名可能会导致迭代器失效。

在这种情况下,使用map进行计算:

fn main() {
    let mut table = Table::from_elem(640, 480, 0u);
    let width = table.width;
    for (value, row, column) in table.iter_mut().enumerate().map(|(i,v)| (v, i / width, i % width) ) {
        *value = get_input(row, column);
    }
}

playpen

get_row_column作为一个单独的函数也会有所帮助。

我认为这里的问题是泄漏抽象之一:index一开始就不应该向用户公开。

因此,在迭代时,您需要更改接口以直接提供(row, column),而不是index,这样使用就很简单了。

类似于:

use std::iter::{Enumerate, Map}
impl<T> Table<T> {
    // Additions
    pub fn iter_enum<'a>(&'a self) -> Map<'a, (uint, &'a T), ((uint, uint), &'a T), Enumerate<Items<'a, T>>> {
        self.iter().enumerate().map(|(i, v)| ((i / self.width, i % self.width), v)
    }
    pub fn iter_mut_enum<'a>(&'a mut self) -> Map<'a, (uint, &'a mut T), ((uint, uint), &'a mut T), Enumerate<MutItems<'a, T>>> {
        self.iter_mut().enumerate().map(|(i, v)| ((i / self.width, i % self.width), v)
    }
}

注意:我希望C++模板别名功能很多,在这里

最新更新