#capture #closures #macro #directive #express #variables #list

无 std captures

提供宏以表达更强大的闭包捕获

1 个不稳定版本

0.1.0 2021年11月26日

#20#directive

每月 27 下载

MIT/Apache

26KB
417

更强大的闭包捕获

此包提供了简单的宏,让您能够表达更强大的闭包捕获。例如,您可以捕获值的克隆

use std::rc::Rc;

let my_val = Rc::new(1);
captures::capture!(clone my_val, move || {
    // `my_val` is cloned here!
});

您还可以捕获任意表达式并覆盖 Edition-2021 捕获语义。最好的是,您甚至可以指定您的闭包不应该捕获您列出的任何变量之外的变量

let a = 1;
let b = 2;
captures::capture_only!(clone a, move || {
    a + b // errors: `b` is unknown
})

有关详细信息,请参阅 完整文档


lib.rs:

提供两个宏以实现更强大的闭包捕获。

背景

Rust 中的闭包功能强大,但并没有太多选项来修改它们捕获输出的方式。一个痛点往往是需要为闭包捕获 .clone() 一个 Arc<T>Rc<T>。这种模式无法编译

fn needs_static<T: FnOnce() -> i32 + 'static>(f: T) -> i32 {
    f()
}

let local: Rc<i32> = Rc::new(1);
// Try and capture a clone of the `Rc`
let mut f = || {
    let in_closure = local.clone();
    *in_closure.as_ref()
};
// `f` is not `'static`!
assert_eq!(needs_static(f), 1);
// `local` has not been captured!
assert_eq!(*local.as_ref(), 1);

这是因为当在闭包体中编写 local.clone() 时,这个 clone 调用不会执行直到闭包被调用;这意味着闭包实际上捕获了一个 &local,因此它不是 'static!将 f 设为 move 闭包并不能解决这个问题,因为在这种情况下,local 将被按值捕获,后面的 local.as_ref() 语句将失败。我们想要的相反的是,当闭包创建时执行 .clone()

fn needs_static<T: FnOnce() -> i32 + 'static>(f: T) -> i32 {
    f()
}

let local: Rc<i32> = Rc::new(1);
// Actually capture a clone of the `Rc`
let cloned = local.clone();
let f = move || {
    let in_closure = cloned;
    *in_closure.as_ref()
};
// `f` is now `'static`!
assert_eq!(needs_static(f), 1);
// `local` has not been captured!
assert_eq!(*local.as_ref(), 1);

用法

captures::capturecaptures::capture_only 使用逗号分隔的 "捕获指令" 列表,最后是一个闭包表达式。一个捕获指令的例子是 clone x 指令,它表示应该捕获 x 的一个副本来代替 x。因此,上面的例子可以重写为

use captures::capture;
fn needs_static<T: FnOnce() -> i32 + 'static>(f: T) -> i32 {
    f()
}

let local: Rc<i32> = Rc::new(1);
// Actually capture a clone of the `Rc`
let f = capture!(clone local,
    move || {
        let in_closure = local;
        *in_closure.as_ref()
    }
);
// `f` is still `'static`!
assert_eq!(needs_static(f), 1);
// `local` has not been captured!
assert_eq!(*local.as_ref(), 1);

捕获指令

目前支持以下捕获指令

  • clone x 捕获 x 的一个副本。
  • with x = expr 捕获从 expr 计算得到的值 x
  • all x 捕获 x 的所有部分。从 Rust 2021 开始,在闭包中写入 x.y 将只会捕获 xy 字段。指定 all x 将导致捕获 x 的所有部分。这不会影响 x 是按值还是按引用捕获 - 如果闭包是 move 闭包,它仍然会被按值捕获,如果它是一个非 move 闭包,编译器的标准推断算法将允许做出决定。

为了避免意外和编译错误,如果您指定了 clonewith 指令,那么此宏会将您的闭包转换为移动闭包,如果它尚未是移动闭包的话。因此,如果您的闭包是移动闭包 - 要么您明确将其标记为移动闭包,要么您使用了 withclone 指令 - 您还可以指定这些指令

  • ref x 以不可变引用方式捕获 x
  • ref mut x 以可变引用方式捕获 x

所有这些指令中的 x 必须仅是本地变量的名称。未来可能会支持更复杂的事情。目前也不支持组合指令。我将在我找到一种漂亮且一致的方式来处理它之后添加这个功能。

可变性

在 Rust 中,按值捕获的捕获变量继承它们所引用值的可变性。例如,

let a = 1; // immutable
let _ = move || {
    a += 1;
    a
};

无法编译,但如果 a 被标记为可变

let mut a = 1; // now mutable
let _ = move || {
    a += 1;
    a
};

则可以编译。

遗憾的是,这个包没有足够的信息来在一般情况下重现这种行为。 clonewith 指令创建新的变量,其可变性尚不明确。当前的政策是它们都默认为不可变。如果确定这不是最佳选择,将来可能会改变(显然要遵守 semver)。如果您希望这些值是可变的,可以通过在变量前加上 mut 来请求。例如,

let mut v = vec![1, 2]; // despite being mutable here
let _ = capture!(clone v, || {
    v.push(3); // we cannot push to `v`, since it is not mutable
    v
});

我们可以通过以下方式修复这个问题

let mut v = vec![1, 2];
let _ = capture!(clone mut v, || {
    v.push(3); // we can push now
    v
});

这仍然会发出警告,因为闭包外部的变量 v 的可变性未被使用。改写为 let v = vec![1, 2]; 代码将继续编译,并且不会发出警告。

all 指令不受此影响。在这种指令下捕获的变量,如果按值捕获,会正确继承其可变性。因此,这些指令不支持使用 mut 前缀。

捕获仅限

capture_only 宏的行为与 capture 宏完全相同,不同之处在于它还阻止了没有关联捕获指令的任何变量被捕获。例如,

let a = 1;
let mut b = 10;
let mut f = capture_only!(all a, || {
    b += 1; // error
    a + 1
});
assert_eq!(f(), 2);
assert_eq!(b, 11);

无法编译,错误信息指出没有本地变量 b。将 capture_only 改为 capture 将允许上述代码编译。如果您想表示 b 也可能被捕获,但又不想添加任何限制,可以添加一个 all 指令

let a = 1;
let mut b = 10;
let mut f = capture_only!(all a, all b, || {
    b += 1; // compiles
    a + 1
});
assert_eq!(f(), 2);
assert_eq!(b, 11);

依赖关系

~1.5MB
~34K SLoC