类型转换
上一节在Value
类型中引入了3个字符串类型,在创建字符串类型时需要根据长度来生成不同类型。这个判断不应该交给调用者,而应该自动完成。比如现有的语句:
self.add_const(Value::String(var));
就应该改成:
self.add_const(str_to_value(var));
其中str_to_value()
函数就把字符串var
转换成Value
对应的字符串类型。
From trait
这种从一种类型转换(或者称为生成)另外一种类型的功能非常常见,所以Rust标准库中为此定义了From
和Into
trait。这两个互为相反操作,一般只需要实现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);
}