表的定义

Lua的表,对外表现为统一的散列表,其索引可以是数字、字符串、或者除了Nil和Nan以外的其他所有Value类型。但为了性能考虑,对于数字类型又有特殊的处理,即使用数组来存储连续数字索引的项。所以在实现里,表其实是由两部分组成:数组和散列表。为此我们定义表:

pub struct Table {
    pub array: Vec<Value>,
    pub map: HashMap<Value, Value>,
}

后续为了支持元表的特性,还会增加其他字段,这里暂且忽略。

Lua语言中的表(以及以后介绍的线程、UserData等)类型并不代表对象数据本身,而只是对象数据的引用,所有对表类型的操作都是操作的引用。比如表的赋值,只是拷贝了表的引用,而非“深拷贝”整个表的数据。所以在Value中定义的表类型就不能是Table,而必须是引用或者指针。在上一章定义字符串类型时,引入了Rc并讨论了引用和指针。基于同样的原因,这次也采用指针RcTable进行封装。除此之外,这里还需要引入RefCell以提供内部可变性。综上,表类型定义如下:

pub enum Value {
    Table(Rc<RefCell<Table>>),

Table中散列表部分的定义是HashMap<Value, Value>,即索引和值的类型都是Value。而HashMap的索引类型是要求实现EqHash这两个trait的。这也好理解,散列表的工作原理就是在插入和查找时,通过计算索引的哈希值(Hash)来快速定位,并通过比较索引(Eq)来处理哈希冲突。接下来就实现这两个trait。

Eq trait

我们之前已经为Value实现了PartialEq trait,即比较两个Value是否相等,或者说可以对Value类型使用==操作符。而Eq的要求更高,是在PartialEq的基础上再要求自反性,即要求对于此类型的任意值x,都满足x==x。大部分情况下都是满足自反性的,但也有反例,比如浮点数中,Nan != Nan,所以浮点数类型虽然实现了PartialEq但并没有实现Eq。我们的Value类型中虽然包括了浮点数类型,但由于Lua语言禁止使用Nan作为索引(具体说来,我们会在虚拟机执行表插入操作时,判断索引是否为Nan),所以可以认为Value类型满足自反性。对于满足自反性的类型,只要告诉Rust满足即可,而不需要特别的实现:

impl Eq for Value {}

Hash trait

Rust中的大部分基础类型都已经实现了Hash trait,我们这里只需要针对每种类型按照语义调用.hash()即可。

实现Hash trait的代码如下:

impl Hash for Value {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            Value::Nil => (),
            Value::Boolean(b) => b.hash(state),
            Value::Integer(i) => i.hash(state),
            Value::Float(f) => // TODO try to convert to integer
                unsafe {
                    mem::transmute::<f64, i64>(*f).hash(state)
                }
            Value::ShortStr(len, buf) => buf[..*len as usize].hash(state),
            Value::MidStr(s) => s.1[..s.0 as usize].hash(state),
            Value::LongStr(s) => s.hash(state),
            Value::Table(t) => Rc::as_ptr(t).hash(state),
            Value::Function(f) => (*f as *const usize).hash(state),
        }
    }
}

很多类型,如boolRc的指针等,都已经实现了哈希方法,但浮点类型f64并没有,原因也是因为Nan,这里有详细的讨论。在Eq trait一节已经说明,Lua禁止使用Nan作为索引,我们就可以忽略Nan而默认浮点类型可以做哈希。一个方法是把浮点数看做是一块内存,来做哈希。我们这里选择了转换为更简单的整型i64来做哈希。

这个转换用到标准库的mem::transmute()函数,而这个函数是unsafe的。我们这里可以明确知道这个转换是安全的(真的吗?),所以可以放心使用这个unsafe

刚学Rust语言时,看到一些库的描述中明确说明“不含unsafe代码”,就感觉这是一个很自豪的特征。于是在开始这个项目时,我也希望不用任何unsafe代码。不过现在看来unsafe并不是洪水猛兽,也许类似C语言里的goto,只要使用合理,就可以带来很大便利。

对于字符串类型,需要对字符串内容计算hash。对于表类型,只需要对指针计算hash,而忽略表的内容。这是因为字符串的比较是内容的比较,而表的比较就是对表引用的比较

DebugDisplay trait

因为Rust的match是穷尽的,所以编译器会提醒我们在Debug trait里也增加表Table类型:

    Value::Table(t) => {
        let t = t.borrow();
        write!(f, "table:{}:{}", t.array.len(), t.map.len())
    }

代码块中一共2行。第1行用到borrow(),就是对RefCell类型的动态引用,确保没有其他可变引用。相对于Rust语言中大部分的编译期间的检查,这种动态引用会带来额外的运行时开销。

Lua的官方实现里,表类型的输出格式是表的地址,可以用来做简单的调试。我们这里增加了表中数组和散列表部分的长度,更加方便调试。另外,我们为Value实现Display trait,用于print的正式的输出:

impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match self {
            Value::Table(t) => write!(f, "table: {:?}", Rc::as_ptr(t)),