表的定义
Lua的表,对外表现为统一的散列表,其索引可以是数字、字符串、或者除了Nil和Nan以外的其他所有Value类型。但为了性能考虑,对于数字类型又有特殊的处理,即使用数组来存储连续数字索引的项。所以在实现里,表其实是由两部分组成:数组和散列表。为此我们定义表:
pub struct Table {
pub array: Vec<Value>,
pub map: HashMap<Value, Value>,
}
后续为了支持元表的特性,还会增加其他字段,这里暂且忽略。
Lua语言中的表(以及以后介绍的线程、UserData等)类型并不代表对象数据本身,而只是对象数据的引用,所有对表类型的操作都是操作的引用。比如表的赋值,只是拷贝了表的引用,而非“深拷贝”整个表的数据。所以在Value
中定义的表类型就不能是Table
,而必须是引用或者指针。在上一章定义字符串类型时,引入了Rc
并讨论了引用和指针。基于同样的原因,这次也采用指针Rc
对Table
进行封装。除此之外,这里还需要引入RefCell
以提供内部可变性。综上,表类型定义如下:
pub enum Value {
Table(Rc<RefCell<Table>>),
Table
中散列表部分的定义是HashMap<Value, Value>
,即索引和值的类型都是Value
。而HashMap
的索引类型是要求实现Eq
和Hash
这两个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),
}
}
}
很多类型,如bool
、Rc
的指针等,都已经实现了哈希方法,但浮点类型f64
并没有,原因也是因为Nan
,这里有详细的讨论。在Eq
trait一节已经说明,Lua禁止使用Nan作为索引,我们就可以忽略Nan而默认浮点类型可以做哈希。一个方法是把浮点数看做是一块内存,来做哈希。我们这里选择了转换为更简单的整型i64
来做哈希。
这个转换用到标准库的mem::transmute()
函数,而这个函数是unsafe
的。我们这里可以明确知道这个转换是安全的(真的吗?),所以可以放心使用这个unsafe
。
刚学Rust语言时,看到一些库的描述中明确说明“不含unsafe代码”,就感觉这是一个很自豪的特征。于是在开始这个项目时,我也希望不用任何unsafe代码。不过现在看来unsafe并不是洪水猛兽,也许类似C语言里的
goto
,只要使用合理,就可以带来很大便利。
对于字符串类型,需要对字符串内容计算hash。对于表类型,只需要对指针计算hash,而忽略表的内容。这是因为字符串的比较是内容的比较,而表的比较就是对表引用的比较。
Debug
和Display
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)),