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