一个关键的抽象Druid随着数据是Lens trait。本页解释它们是什么,以及如何使用它们。一开始,Lens可能看起来很复杂,但是它们也非常强大,允许你编写可重用、简洁和易懂的代码(一旦你理解了lens本身)。
基本原理
让我们从镜头的定义开始:
pub trait Lens<T, U> {
fn with<F: FnOnce(&U)>(&self, data: &T, f: F);
fn with_mut<F: FnOnce(&mut U)>(&self, data: &mut T, f: F);
}
我从Druid的源代码中复制了这个定义,然后通过删除返回类型来简化它,因为它们不是Lens工作方式的基础。
首先要注意的是Lens本身的泛型。Lens有三种类型: Lens、LensT 和Lens U。这两个类型的参数表示Lens所解决的不匹配: 我们有一个操作于 U 的函数,和一个 T 类型的对象,所以我们需要以某种方式将 T 转换成 U。
实现
举个例子。让我们手动实现和使用镜头,这样我们就可以看到发生了什么。
struct Container {
inner: String,
another: String,
}
// Here the lens doesn't have any data, but there are cases where
// it might, for example it might contain an index into a collection.
struct InnerLens;
// Our lens will apply functions that operate on a `String` to a `Container`.
impl Lens<Container, String> for InnerLens {
fn with<F: FnOnce(&String)>(&self, data: &Container, f: F) {
f(&data.inner);
}
fn with_mut<F: FnOnce(&mut String)>(&self, data: &mut Container, f: F) {
f(&mut data.inner);
}
}
这个案子很简单。我们需要做的就是将函数投射到字段上。请注意,这不是我们可以制作的从 Container 到 String 的唯一有效镜头-我们也可以从 Container 投影到另一个镜头。在实现 Lens 时,我们选择了如何将 Container 转换为 String。
你还会注意到,这两个方法都接受对 self 的不可变引用,甚至是 mut 变量。Lens本身应该被认为是一个固定的东西,知道如何进行映射。在上述情况下,它不包含任何数据,甚至很可能不会出现在最终编译/优化的代码中。
现在来看一个稍微复杂一点的例子
struct Container2 {
first_name: String,
last_name: String,
age: u16, // in the future maybe people will live past 256?
}
struct Name {
first: String,
last: String,
}
struct NameLens;
impl Lens<Container2, Name> for NameLens {
fn with<F: FnOnce(&Name)>(&self, data: &Container2, f: F) {
let first = data.first_name.clone();
let last = data.last_name.clone();
f(&Name { first, last });
}
fn with_mut<F: FnOnce(&mut Name)>(&self, data: &mut Container2, f: F) {
let first = data.first_name.clone();
let last = data.last_name.clone();
let mut name = Name { first, last };
f(&mut name);
data.first_name = name.first;
data.last_name = name.last;
}
}
现在我相信你已经意识到了,以上这些是非常低效的。考虑到我们将经常遍历我们的数据,我们需要它是便宜的。(这在以前不是问题,因为当我们不需要构建内部类型时,我们可以只使用引用。如果我们的数据复制/克隆成本很低,例如任何基本数字类型 u8,… f64,那也不是问题。)幸运的是,这正是生锈所擅长的。让我们重写上面的例子,以快速!
struct Container2 {
first_name: Rc<String>,
last_name: Rc<String>,
age: u16,
}
struct Name {
first: Rc<String>,
last: Rc<String>,
}
struct NameLens;
impl Lens<Container2, Name> for NameLens {
// .. identical to previous example
}
正如您将看到的,我们已经介绍了 Rc: 引用计数指针。您将在示例中看到这个函数及其多线程表兄 Arc 被广泛使用。现在,我们实际复制内存的唯一时间是在 f in with _ mut 中调用 Rc: : make _ mut 时。这意味着,在没有任何变化的情况下,我们所要做的就是递增和递减引用计数。此外,我们还为编译器提供了内联 f 和/with _ mut 的机会,使得这个抽象可能成本为零
折衷之处在于,我们在 Name 类型中引入了更多的复杂性: 为了对数据进行更改,我们必须使用 Rc: : make _ mut 来获得对 String 的可变访问权。(镜头中的代码将确保 Rc 数据的较新副本保存到外部类型。)这意味着快速编写德鲁伊代码需要了解铁锈指针类型(Rc/Arc,也可能是 RefCell/Mutex)。
我们可以做得更好。假设我们处理的是数据向量而不是字符串。我们可以导入入口箱来获得使用结构共享的集合,这意味着即使载体发生变异,我们也只克隆需要克隆的部分。因为 IM 非常有用,所以它包含在Druid(在 IM 特性后面)中。
struct Container2 {
// Pretend that it's the 1980s and we store only ASCII names.
first_name: im::Vector<u8>,
last_name: im::Vector<u8>,
age: u16,
}
struct Name {
first: im::Vector<u8>,
last: im::Vector<u8>,
}
struct NameLens;
impl Lens<Container2, Name> for NameLens {
// .. identical to previous example
}
现在,除了几乎免费的克隆,我们还可以对数据本身进行廉价的增量更新。对于名称,这并不重要,但是如果向量有1 _ 000 _ 000 _ 000个元素,我们仍然可以在 O (log (n))时间内进行更改(在这种情况下,1 _ 000 _ 000 _ 000和30之间的差异非常大!).
好了,现在你知道Lens是怎么工作的了。恭喜你,你已经完成了最困难的部分!