类型转换

上一节在Value类型中引入了3个字符串类型,在创建字符串类型时需要根据长度来生成不同类型。这个判断不应该交给调用者,而应该自动完成。比如现有的语句:

    self.add_const(Value::String(var));

就应该改成:

    self.add_const(str_to_value(var));

其中str_to_value()函数就把字符串var转换成Value对应的字符串类型。

From trait

这种从一种类型转换(或者称为生成)另外一种类型的功能非常常见,所以Rust标准库中为此定义了FromIntotrait。这两个互为相反操作,一般只需要实现From即可。下面就实现了字符串String类型到Value类型的转换:

impl From<String> for Value {
    fn from(s: String) -> Self {
        let len = s.len();
        if len <= SHORT_STR_MAX {
            // 长度在[0-14]的字符串
            let mut buf = [0; SHORT_STR_MAX];
            buf[..len].copy_from_slice(s.as_bytes());
            Value::ShortStr(len as u8, buf)

        } else if len <= MID_STR_MAX {
            // 长度在[15-47]的字符串
            let mut buf = [0; MID_STR_MAX];
            buf[..len].copy_from_slice(s.as_bytes());
            Value::MidStr(Rc::new((len as u8, buf)))

        } else {
            // 长度大于47的字符串
            Value::LongStr(Rc::new(s))
        }
    }
}

然后,本节开头的语句就可以改用into()函数:

    self.add_const(var.into());

泛型

至此,本节开头的需求已经完成。不过既然字符串可以这么做,那其他类型也可以。而且其他类型的转换更直观。下面仅列出两个数字类型到Value类型的转换:

impl From<f64> for Value {
    fn from(n: f64) -> Self {
        Value::Float(n)
    }
}

impl From<i64> for Value {
    fn from(n: i64) -> Self {
        Value::Integer(n)
    }
}

然后,向常量表里添加数字类型的Value也可以通过into()函数:

    let n = 1234_i64;
    self.add_const(Value::Integer(n));  // 旧方式
    self.add_const(n.into());  // 新方式

这么看上去似乎有点小题大做。但如果把所有可能转换为Value的类型都实现From,那么就可以把.into()放到add_const()内部了:

    fn add_const(&mut self, c: impl Into<Value>) -> usize {
        let c = c.into();

这里只列出了这个函数的前2行代码。下面就是添加常量的原有逻辑了,这里省略。

先看第2行代码,把.into()放到add_const()函数内部,那么外部在调用的时候就不用.into()了。比如前面添加字符串和整数的语句可以简写成:

    self.add_const(var);
    self.add_const(n);

现有代码中很多地方都可以这么修改,就会变得清晰很多,那对这些类型实现From trait就很值得了。

然而问题来了:上述的2行代码里,两次add_const()函数调用接受的参数的类型不一致!那函数定义中,这个参数类型怎么写?答案就在上面add_const()函数的定义中:c: impl Into<Value>。其完整写法如下:

    fn add_const<T: Into<Value>>(&mut self, c: T) -> usize {

这个定义的意思是:参数类型为T,其约束为Into<Value>,即这个T需要能够转换为Value,而不能把随便一个什么类型或数据结构加到常量表里。

这就是Rust语言中的泛型!我们并不完整地介绍泛型,很多书籍和文章里已经介绍的很清楚了。这里只是提供了一个泛型的应用场景,来具体体验泛型。其实我们很早就使用了泛型,比如全局变量表的定义:HashMap<String, Value>。大部分情况下,是由一些库来定义带泛型的类型和函数,而我们只是使用。而这里的add_const()定义了一个带泛型的函数。下一节也会再介绍一个泛型的使用实例。

反向转换

上面是把基础类型转换为Value类型。但在某些情况下需要反向的转换,即把Value类型转换为对应的基础类型。比如虚拟机的全局变量表是以字符串类型为索引的,而全局变量的名字是存储在Value类型的常量表中的,所以就需要把Value类型转换为字符串类型才能作为索引使用。其中对全局变量表的读操作和写操作,又有不同,其对应的HashMap的API分别如下:

pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> // 省略了K,Q的约束
pub fn insert(&mut self, k: K, v: V) -> Option<V>

读写的区别是,读get()函数的参数k是引用,而写insert()函数的参数k是索引本身。原因也简单,读时只是用一下索引,而写时是要把索引添加到字典里的,是要消费掉k的。所以我们要实现Value类型对字符串类型本身和其引用的转换,即String&String。但对于后者,我们用更通用的&str来代替。

impl<'a> From<&'a Value> for &'a str {
    fn from(v: &'a Value) -> Self {
        match v {
            Value::ShortStr(len, buf) => std::str::from_utf8(&buf[..*len as usize]).unwrap(),
            Value::MidStr(s) => std::str::from_utf8(&s.1[..s.0 as usize]).unwrap(),
            Value::LongStr(s) => s,
            _ => panic!("invalid string Value"),
        }
    }
}

impl From<&Value> for String {
    fn from(v: &Value) -> Self {
        match v {
            Value::ShortStr(len, buf) => String::from_utf8_lossy(&buf[..*len as usize]).to_string(),
            Value::MidStr(s) => String::from_utf8_lossy(&s.1[..s.0 as usize]).to_string(),
            Value::LongStr(s) => s.as_ref().clone(),
            _ => panic!("invalid string Value"),
        }
    }
}

这里的两个转换调用的函数名不一样,std::str::from_utf8()String::from_utf8_lossy()。前者不带_lossy而后者带。其中原因在于UTF-8等,后续在介绍UTF8时详细介绍。

另外,这个反向转换是可能失败的,比如把一个字符串的Value类型转换为一个整数类型。但这涉及到错误处理,我们在后续统一梳理错误处理后再做修改。这里仍然使用panic!()来处理可能的失败。

后续在支持了环境后,会用Lua的表类型和Upvalue来重新实现全局变量表,届时索引就直接是Value类型了,这里的转换也就没必要了。

在虚拟机执行的代码中,读写全局变量表时,分别通过两次into()就完成Value类型到字符串的转换:

                ByteCode::GetGlobal(dst, name) => {
                    let name: &str = (&proto.constants[name as usize]).into();
                    let v = self.globals.get(name).unwrap_or(&Value::Nil).clone();
                    self.set_stack(dst.into(), v);
                }
                ByteCode::SetGlobal(name, src) => {
                    let name = &proto.constants[name as usize];
                    let value = self.stack[src as usize].clone();
                    self.globals.insert(name.into(), value);
                }