Rust无成本抽象
Rust中抽象基石是trait:
1,Trait是Rust中唯一的接口概念.多个类型可实现一个特征,事实上,可为现有类型提供新的特征实现.另一方面,想抽象未知类型时,找特征就行了.
2,与C++模板一样,可静态分发特征.
3,可动态分发特征.有时确实需要间接,所以不必运行时"擦除"抽象.想运行时分发时,可使用接口或特征.
背景:Rust中的方法
Rust提供了方法和自由函数,它们密切相关:
cpp
struct Point {
x: f64,
y: f64,
}
//把(借用的)点转换为串的`自由`函数
fn point_to_string(point: &Point) -> String { ... }
//"固有的`impl"`块,直接在`类型`上定义了可用的方法
impl Point {
//此方法在`Point`上都可用,并自动借用`Point`值
fn to_string(&self) -> String { ... }
}
上述to_string方法叫"固有"方法,因为它们:
1,(通过impl)绑定到单个具体的"self"类型.
2,在该类型值上自动可用.也即,与函数不同,内置方法总是"在域内".
方法的第一个参数总是是显式的"self",根据期望的所有权级别,它是self,&mut self或&self.使用.调用方法.
且self参数,按方法中使用的self形式隐式借用:
cpp
let p = Point { x: 1.2, y: -3.7 };
let s1 = point_to_string(&p); //显式借用,调用自由函数.
let s2 = p.to_string(); //按`&p`隐式借用,来调用方法
如下,流式生成进程的API:
cpp
let child = Command::new("/bin/cat")
.arg("rusty-ideas.txt")
.current_dir("/Users/aturon")
.stdout(Stdio::piped())
.spawn();
特征是接口
接口允许每个代码自由切换.对特征,规范主要围绕方法展开.
如,以下用来哈希的简单特征:
cpp
trait Hash {
fn hash(&self) -> u64;
}
为了为给定类型实现此特征,必须提供匹配签名的哈希方法:
cpp
impl Hash for bool {
fn hash(&self) -> u64 {
if *self { 0 } else { 1 }
}
}
impl Hash for i64 {
fn hash(&self) -> u64 {
*self as u64
}
}
与Java,C#或Scala等语言中的接口不同,可为现有类型实现新特征(如上面的Hash).即可在事后创建抽象,并应用至现有库.
与内置方法不同,仅当特征在域时,特征方法才在域中.但是假设Hash在域内,你可编写true.hash(),因此实现一个特征扩展了类型上可用的方法集.
定义和实现特征不过是抽象出多个类型满足的通用接口.
静态分发
一般通过泛型消费特征:
cpp
fn print_hash<T: Hash>(t: &T) {
println!("The hash is {}", t.hash())
}
在未知T类型上,print_hash函数是泛型函数,但要求T实现Hash特征.即可与bool和i64值一起,使用它:
cpp
print_hash(&true); //实例化`T=bool`
print_hash(&12_i64); //实例化`T=i64`
静态分发中编译掉泛型.也即,与C++模板一样,编译器生成print_hash方法的两个副本来处理上述代码,每个副本对应一个具体参数类型.
反之表明内部调用t.hash()(实际使用抽象点)的成本为零:按直接静态调用相关实现编译它:
cpp
//编译后的代码:直接调用特化`bool`版本
__print_hash_bool(&true); //
__print_hash_i64(&12_i64);
//直接调用特化`i64`版本
对像print_hash类函数,该编译模型不是很有用,但对更实际的哈希使用,却非常有用.假设还引入了一个相等比较特征:
cpp
trait Eq {
fn eq(&self, other: &Self) -> bool;
}
这里按实现trait的类型解析Self的引用;在impl Eq for bool中,它引用bool.
然后,可定义一个在实现哈希和Eq的T类型上是都通用的哈希映射:
cpp
struct HashMap<Key: Hash + Eq, Value> { ... }
泛型静态编译模型有几个好处:
1,对具体的Key和Value类型,每次使用HashMap都会产生不同的具体HashMap类型,即HashMap可在其存储桶中内联(无间接)布局键和值.
来可节省空间和间接,并提高缓存局部性.
2,HashMap上的每个方法同样会生成特化代码.即,如上,调用哈希和Eq,不会产生额外成本.表明优化器可用最具体(也即没有抽象)的代码.
特别是,静态分发允许在泛型用法间内联.
总之,与在C++模板一样,你可用泛型编写无成本的相当高级的抽象.
但是,与C++模板不同的是,会提前完全类型检查特征客户.也即,单独编译HashMap时,会根据抽象Hash和Eq特征检查一次代码类型正确性,而不是在每当应用具体类型时的重复检查.
即库作者可更早,更清晰地出现编译错误,而客户的类型检查成本更少(即编译速度更快).
动态分发
有时,抽象不仅是重用或模块化,有时在运行时不能去掉抽象.
如,GUI框架一般涉及响应事件(如点击鼠标)的回调:
cpp
trait ClickCallback {
fn on_click(&self, x: i64, y: i64);
}
GUI元素,常见的是,允许为单个事件注册多个回调.对泛型,可想象这样写:
cpp
struct Button<T: ClickCallback> {
listeners: Vec<T>,
...
}
但问题立即显现出来:即每个按钮都按ClickCallback的一个实现特化,且按钮类型反映了该类型.这不是想要的!
相反,想要一个带一组每个都可能是不同具体类型,但都实现了ClickCallback的异构监听器的Button类型.
难点是,如果是一组异构类型,则每个类型都有不同的大小,则如何才能布局内部向量?答案一般是:间接.在向量中存储回调指针:
cpp
struct Button {
listeners: Vec<Box<ClickCallback>>,
...
}
在此,就像它是一个类型一样,使用ClickCallback特征.在Rust中,特征是类型,但它们是"无大小的",只允许出现在Box(指向堆)或&(可任意指向)等指针后面.
在Rust中,像&ClickCallback或Box<ClickCallback>的类型叫"trait对象",它包括指向实现ClickCallback的T类型实例的指针,及一个虚表:一个指向T对trait中每个方法实现的指针(这里,只是on_click).
可在运行时用该信息正确分发调用方法,并确保统一表示T.因此,只编译一次Button.
多用途
1,闭包.
类似ClickCallback特征,Rust中的闭包只是特定特征.深入
2,条件API.泛型可有条件地实现特征:
cpp
struct Pair<A, B> { first: A, second: B }
impl<A: Hash, B: Hash> Hash for Pair<A, B> {
fn hash(&self) -> u64 {
self.first.hash() ^ self.second.hash()
}
}
在此,仅当组件实现Hash时,Pair类型才实现Hash,但允许在不同环境中使用单个Pair类型,这样最大化支持每个环境时API的可用性.
这在Rust中很常见,因此内置了.
cpp
#[derive(Hash)]
struct Pair<A, B> { .. }
3,扩展方法.可用Traits使用新方法来扩展(在其他地方定义的)现有类型,类似C#的扩展方法.只需在特征中定义新方法,为相关类型提供实现,就可用该方法.
4,标记.Rust有一些"标记类型":发送,同步,复制,调整(Send, Sync, Copy, Sized).这些标记只是带空体的特征,然后可在泛型和特征对象中使用.
可在库中定义标记,它们会自动提供#[derive]风格实现:如,如果所有子类型都是Send,则类型也是.如前,这些标记可能非常强大:发送(Send)标记是Rust保证线安的方式.
5,重载.Rust不支持用多个签名定义相同方法的传统重载.但是trait提供了重载的大部分好处:如果在trait上泛型定义了方法,则实现该trait的类型都可调用它.
与传统重载相比,有两个优点.首先,即重载不是临时的:一旦理解了一个特征,就会立即理解使用它的API的重载模式.
其次,它是可扩展的:通过提供新的特征实现,可有效地在方法下游提供新的重载.
6,符号.Rust允许在自己类型上重载+等符号.由相应标准库特征定义每个符号,实现该特征类型也会自动提供符号.