特征对象式是一种模式,它可以帮助我们更好地理解和处理复杂的数据结构。它的基本思想是将一个复杂的对象分解成一系列特征对象,这些特征对象可以独立地表示该对象的不同部分。
特征对象式有助于减少代码量,使代码更易于阅读和理解。它可以帮助我们将复杂的数据结构分割成独立的部分,使其易于理解和修改。例如,如果要表示一个人,可以使用三个特征对象来表示其姓名、年龄和性别。这样,当需要修改其中一个特征时,就不必修改整个人的数据结构。
此外,特征对象式还有助于减少代码耦合度。因为它将复杂的数据结构分割成独立的部分,所以当需要修改其中一部分时,不会影响其他部分。例如,如果要修改一个人的姓名,就不必修改其年龄和性别。
class Person { private String name; private int age; private String gender; public Person(String name, int age, String gender) { this.name = name; this.age = age; this.gender = gender; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } }
当代码涉及多态,需要有一种机制来确定实际上是哪些特定版本在运行。这就是所谓的“调度”。调度的主要形式有两种:静态调度和动态调度。Rust 不仅支持静态调度,它还通过“特征对象”机制支持动态调度。
在本章的其余部分,我们将需要一个特征和一些实现。让我们先从一个简单的开始,Foo。它有一个方法,该方法将返回一个字符串。
trait Foo {
fn method(&self) -> String;
}
我们将在 u8 和 String 里面实现这个特征:
impl Foo for u8 {
fn method(&self) -> String { format!("u8: {}", *self) }
}
impl Foo for String {
fn method(&self) -> String { format!("string: {}", *self) }
}
我们可以利用这一特性的特征边界来执行静态调度:
fn do_something<T: Foo>(x: T) {
x.method();
}
fn main() {
let x = 5u8;
let y = "Hello".to_string();
do_something(x);
do_something(y);
}
在这里 Rust 使用 “monomorphization” 来执行静态调度。这意味着 Rust 将为 u8 和 String 创建一个特殊版本的 do_something()函数,然后调用这些具体的函数来取代调用 sites 。换句话说,Rust 生成是这样的:
fn do_something_u8(x: u8) {
x.method();
}
fn do_something_string(x: String) {
x.method();
}
fn main() {
let x = 5u8;
let y = "Hello".to_string();
do_something_u8(x);
do_something_string(y);
}
这样会有很大的好处:静态调度允许内联函数调用,因为被调用的函数在编译时是已知的,并且内联是进行良好的优化的关键。静态调度虽然快,但它是折衷之后的结果:代码膨胀,由于许多相同的函数都有二进制副本,并且每种类型的函数都对应一个二进制副本。
另外,编译器并不完美,可能“优化”反而使代码变得更慢。例如,内联函数过于急切地将膨胀指令缓存(缓存规定我们周围的一切)。这是 #[inline] 和 #[inline(always)] 应该被谨慎使用的部分原因,另一个使用动态调度的原因是它有时效率更高。
然而,常见的情况是使用静态调度更高效,并且你总是可以用一个简练的 statically-dispatched 包装器函数进行动态调度,但是用动态调度却不是这样,这意味着静态调用更灵活。可能因为这个原因,标准库更多的使用静态调度。
Rust 通过一个称为特征对象的特性提供动态调度。特征对象,如 &Foo or Box<Foo>
可以是正常值,这个值存储一个任意类型的值,而该类型实现了给定的特征,至于究竟是哪种类型在运行时才能知道。
特征对象可以从指针获得,这个指针指向一个具体的类型,而该类型通过 cast(例如 &x as &Foo)或 coerce(例如,使用 &x 作为函数的参数,函数里面带有 &foo) 实现了特征。
特征对象 coercions 和 casts 也服务于指针像 &mut T to &mut Foo
与 Box<T> to Box<Foo>
但都只是在此刻有效。而 Coercions 和 casts 是相同的。
这个操作可以被视为“擦除”编译器对特定类型的指针的认知,因此特征对象有时也被称为类型擦除。
回到上面的例子,我们可以使用相同的特征来执行动态调度与特征的对象的 cast:
fn do_something(x: &Foo) {
x.method();
}
fn main() {
let x = 5u8;
do_something(&x as &Foo);
}
或者通过coerce::
fn do_something(x: &Foo) {
x.method();
}
fn main() {
let x = "Hello".to_string();
do_something(&x);
}
每种实现 Foo 的类型并不专门指定一个带有个特征对象的函数:只有当一个副本生成时,才会(但不总是如此)导致更少的代码膨胀。然而,这是需要以更慢的虚拟函数调用作为代价的,并且极大地抑制了任何内联的机会和相关优化的发生。
在默认情况下,Rust 不给指针赋予初值,不像许多其他管理语言那样,所以类型可以有不同的大小。在编译时知道值的大小是很重要的,比如把它作为参数传递给一个函数,又或者把它放进堆栈并且在存储它的堆里面分配(或取消分配)空间给它。
对于 Foo,我们需要有一个值,这个值至少要是一个字符串(24 字节)或 u8(1 个字节),或者是任意其他依赖于箱来实现他们的Foo的类型(任意的字节数)。如果没有存储的值没有指针来指向,就没有办法保证这最后一点行得通,因为其他那些类型可以是任意大小的。
让指针指向一个值意味着,当我们抛出一个特征对象时,值的大小是无关紧要的,我们只关心指针本身的大小。
特征的方法可以通过特征对象来调用。特征对象是通过函数指针的一个特殊的记录(由编译器创建和管理)来调用方法的,该记录传统上也被称为“虚表”。
特征对象既简单的又复杂:其核心表示和布局其实很简单,但也有一些复杂的错误消息和令人意外的结果。
让我们以简单的特征对象的运行时间的表示来开始。std::raw
模块包含与内置类型一样复杂的布局结构,包括特征对象:
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}
即特征对象 &Foo 包含数据指针和一个虚表指针。
数据指针指向特征对象所存储的数据(一些未知类型T),而虚表指针指向对应的虚表(“虚拟方法表”)对应于 T 的 Foo 实现。
vtable 本质上是一个函数指针结构体,指向每个实现方法的具体的机器代码。调用方法 trait_object.method()
将检索虚表外部的正确的指针,然后做一个动态调用。例如:
struct FooVtable {
destructor: fn(*mut ()),
size: usize,
align: usize,
method: fn(*const ()) -> String,
}
// u8:
fn call_method_on_u8(x: *const ()) -> String {
// the compiler guarantees that this function is only called
// with `x` pointing to a u8
let byte: &u8 = unsafe { &*(x as *const u8) };
byte.method()
}
static Foo_for_u8_vtable: FooVtable = FooVtable {
destructor: ,
size: 1,
align: 1,
// cast to a function pointer
method: call_method_on_u8 as fn(*const ()) -> String,
};
// String:
fn call_method_on_String(x: *const ()) -> String {
// the compiler guarantees that this function is only called
// with `x` pointing to a String
let string: &String = unsafe { &*(x as *const String) };
string.method()
}
static Foo_for_String_vtable: FooVtable = FooVtable {
destructor: ,
// values for a 64-bit computer, halve them for 32-bit ones
size: 24,
align: 8,
method: call_method_on_String as fn(*const ()) -> String,
};
每个 vtable 的析构函数的字段都指向一个函数,这个函数将清理 vtable 类型的任何资源,对 u8 来说这几乎是没有什么用处的,但对字符串来说却可以释放内存。如果想要拥有 Box<Foo>
那样的特征对象,这个操作就是必不可少的,当超出范围时,就需要清理 Box 分配以及内部类型。size 和 align 字段存储着被删除类型的大小,及其一致性要求;由于信息被嵌入到了析构函数,目前这些实际上都未使用,但将来会得到应用,因为特征对象逐渐变得更加灵活。
假设我们有一些实现 Foo 的值,然后你会发现 Foo 的显式形式的构建和 Foo 特征对象的使用看起来有点类似(忽略类型不匹配:无论如何,它们只是指针而已):
let a: String = "foo".to_string();
let x: u8 = 1;
// let b: &Foo = &a;
let b = TraitObject {
// store the data
data: &a,
// store the methods
vtable: &Foo_for_String_vtable
};
// let y: &Foo = x;
let y = TraitObject {
// store the data
data: &x,
// store the methods
vtable: &Foo_for_u8_vtable
};
// b.method();
(b.vtable.method)(b.data);
// y.method();
(y.vtable.method)(y.data);
如果 b 或 y 拥有特征对象 (Box < Foo >
) 当他们超出范围时,就会有一个(b.vtable.destructor
)(b.data) (分别 y)调用。
'Deref'强制转换标准库提供了一个特殊的特征,Deref。它通常用于重载 * ,取消引用运算符:use std::ops::Deref;struct ...
向量 v 中的元素 v1, v2, v3, …, vn,下式给出其幅度:|v| = √(v1^2+ v2^2+ v3^2+ … + vn^2)MATLAB中需要采按照下述步骤进行...
与其它语言的区别与 MATLAB 的区别Julia 的语法和 MATLAB 很像。但 Julia 不是简单地复制 MATLAB ,它们有很多句法和功能上的区...
本节介绍如何通过 PHP 编写Web Pages。PHP 开发人员请注意,Web Pages 可以用 PHP 编写。 WebMatrix 支持 PHP 乍一看,认为 WebM...
本节介绍ASP.NET MVC 的安全问题。为了学习 ASP.NET MVC,我们将构建一个 Internet 应用程序。第 8 部分:添加安全。 MVC 应用程...