以一个超级简单的结构体Foo为例:
#[derive(Debug)]
struct Foo {
a: i32
}
还有一个合成宏here:
macro_rules! compose {
( $last:expr ) => { $last };
( $head:expr, $($tail:expr), +) => {
compose_two($head, compose!($($tail),+))
};
}
fn compose_two<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(A) -> B,
G: Fn(B) -> C,
{
move |x| g(f(x))
}
我可以定义一个简单的函数,它接受一个可变的引用,修改结构体并返回传递给它的引用:
fn foo(x: &mut Foo) -> &mut Foo {
x.a = x.a * 2;
x
}
它按预期工作:
fn main() {
let mut x = Foo { a: 3 };
let y = foo(&mut x);
println!("{:?}", y.a); // prints 6
y.a = 7;
println!("{:?}", x); // prints Foo { a: 7 }
}
当我尝试定义第二个简单函数并组合这两个函数时,问题出现了:
fn bar(x: &mut Foo) -> &mut Foo {
x.a = x.a + 1;
x
}
fn main() {
let baz = compose!(foo, bar);
let mut x = Foo { a: 3 };
let y = baz(&mut x);
println!("{:?}", y.a);
}
我得到一个错误,在主let y = baz(&mut x);
中x的可变借位没有足够长的时间。我认为我对编写宏的理解还不够好,不足以理解出了什么问题。
同样,当我在第一个版本中打印绑定到x
的结构体时,它可以工作,因为它是在可变borrow y
的最后一次使用之后,所以我可以不变地borrow x
来打印它;但是在第二个版本中,如果我试图在末尾打印x
,它会说它仍然是可变borrow。x
的可变借词吗
我该怎么做?* 能 * 做得到吗?
Playground
根据评论编辑:
看起来compose_two
中的闭包实际上并不持有对结构体的可变引用,而返回类型也没有指定它不持有(closures close over captured variables right?),因此编译器被迫假设它可能持有。
2条答案
按热度按时间z9zf31ra1#
没有。但是根据您的用例,可能有。
如果满足以下任一条件,则可以使其工作:
Copy
(如果您使用函数项(fn
),它们总是Copy
,但是对于闭包,如果它们捕获非Copy
类型,这可能是个问题)。compose!()
(main()
)的用户。compose!()
限制为引用(确切地说,是可变引用,但是也可以为共享引用创建一个版本,当然,如果你想为引用和拥有的类型创建单独的版本,这是可以的)。这里有三个因素,它们结合在一起使编译器
x
可以在其生存期后使用。如果我们破坏其中一个,它将工作。其中两个实际上是假的,但编译器不知道(或不想依赖于此)。这些因素是:1.编译器相信返回的闭包可以捕获它的参数。这是假的,我马上会解释,但编译器并不知道这一点。
1.编译器认为闭包有一个drop实现,并且可以在这个drop中使用
x
(在步骤1中捕获)。事实上,编译器知道它没有,但是因为我们使用了impl Trait
,它被迫将其视为实现了drop,所以添加一个drop不会是一个破坏性的更改。x
在baz
之前被丢弃。这是真的(变量以与它们的声明相反的顺序被丢弃),并且结合编译器的两个先前的信念,这意味着当baz
将(潜在地)在其丢弃中使用其捕获的x
时,它将在x
的生存期之后。让我们从最后一个声明开始,它是最容易被打破的,因为你只需要交换
x
和baz
的顺序:但是并不总是可以更改
main()
,或者可能无法在baz
之前声明x
。让我们回到第二个声明,编译器认为闭包有一个
Drop
impl,因为它在impl Trait
中,如果它没有呢?不幸的是,这需要每天晚上,因为手动编写闭包需要
fn_traits
和unboxed_closures
特性,但这绝对是可能的(一个很好的附带好处是,函数可以有条件地为FnOnce
/FnMut
/Fn
,这取决于它的输入函数是什么):另一种打破这种假设的方法是使返回的闭包为
Copy
。Copy
类型永远不能实现Drop
,编译器知道这一点,并假设它们不实现。不幸的是,由于闭包捕获f
和g
,它们也需要为Copy
:最后一种方法解释起来最复杂,首先,我需要解释为什么编译器认为闭包可以捕获
x
,而实际上它不能。让我们先想想为什么闭包不能做到这一点:它将代替下面的
'?
使用多长时间?当
baz
被定义时(我们必须决定使用什么样的生存期),我们仍然不知道什么会被传递给闭包,所以我们不知道应该使用什么样的生存期!这个知识,本质上是“闭包可以用 * 任意 * 生存期调用”,在Rust中通过 * 高级特征边界(Higher-Ranked Trait Bounds,HRTB)* 传递,拼写为
for<'lifetime>
,所以A
在compose_two()
中应该是HRTB。但问题就在这里:泛型参数不能是HRTB。它们必须用一个 concrete 生存期示例化。因此,编译器为
baz
选择某个生存期'x
,并且这个生存期必须大于baz
本身-否则它将包含一个悬空生存期-因此理论上它可以有一个具有该生存期的成员。因此编译器相信X1 M42 N1 X可以存储对X1 M43 N1 X的引用,而实际上它不能。如果我们能把它变成HRTB...
我们可以!如果我们不把它完全泛型化,而是把它指定为一个引用:
或者,使用省略形式,因为HRTB是
Fn
trait边界的默认值:不幸的是,它还需要
B: 'static
,因为编译器无法断定B
是否会存活足够长的时间(这是该语言的另一个限制),但它可以正常工作!ee7vknir2#
首先,使用
rust-analyzer
快速修复(vscode中的keybindctrl+.
)将宏内联几次会有所帮助:在这种情况下,它只需要调用
compose_two
的给定函数。编译器对错误的完整诊断如下:我不完全确定这里发生了什么,但我认为问题在于编译器无法推断出在调用
baz
之后,由于某种原因,可变引用没有被使用。如果内联调用compose_two
,它就可以工作:如果您将
baz
定义为一个单独的函数,则该函数有效:但我不知道为什么它不能从
baz
(变量)的impl Fn(&mut Foo) -> &mut Foo
类型中推断出这一点。编辑:问题注解中的一些讨论在这里提供了一些启示。正如Chayim Friedman所描述的,编译器不能从
impl Fn(&mut Foo) -> &mut Foo
类型中假设太多,所以它假设它可能持有引用(但它没有)。这就是为什么如果你内联闭包或提取baz到它自己的函数,它会工作,因为这样编译器就有更多的信息。在这种情况下,可以通过先声明
x
来修复,这样baz
(编译器认为它持有对x
的可变引用)就可以在x
之前删除: