Can个这样能行得通吗?
不是的.但根据您的用例,可能是这样的.
如果满足以下任一条件,则可以使其正常工作:
- 您可以将函数类型限制为
Copy
(如果您使用的是函数项(fn
),则它们始终为Copy
,但对于闭包,如果它们捕获非Copy
类型,则这可能是一个问题).
- 你可以每晚使用.
- 您可以更改
compose!()
(main()
)的用户.
- 您可以将
compose!()
限制为引用(准确地说,是可变引用,但您也可以为共享引用创建一个版本.当然,如果您想为引用和拥有的类型创建单独的版本,这是很好的).
这里有三个因素,它们结合在一起使编译器x
在其生命周期之后可以使用.如果我们打破其中一个,它会起作用的.其中两个实际上是假的,但是编译器不知道这一点(或者不想依赖它).这些因素包括:
- 编译器相信返回的闭包可以捕获它的参数.这是假的,我将立即解释,但编译器不知道这一点.
- 编译器认为闭包有一个Drop实现,并且可以在这个Drop中使用
x
(在步骤1中捕获).事实上,编译器知道它不知道,但因为我们使用了impl Trait
,所以它被迫将其视为实现了Drop,所以添加一个不会是一个 destruct 性的更改.
- 在
baz
之前,x
下降了.这是正确的(变量以与其声明相反的顺序被删除),再结合编译器之前的两个信念,这意味着当baz
将(潜在地)在其删除中使用其捕获的x
时,它将在x
的生命周期之后.
让我们从最后一个索赔开始.这是最容易打破的,因为你只需要交换x
和baz
的顺序:
fn main() {
let mut x = 3;
let baz = compose_two(foo, bar);
let y = baz(&mut x);
println!("{:?}", y);
}
但并不总是可以更改main()
,或者在baz
之前声明x
可能是不可能的.
因此,让我们回到第二个主张.编译器认为闭包有Drop
Iml,因为它在impl Trait
中.如果情况并非如此呢?
不幸的是,这需要每晚执行一次,因为手动编写闭包需要功能fn_traits
和unboxed_closures
.但这绝对是可能的(一个很好的附带好处是,根据其输入函数的不同,该函数可以有条件地为FnOnce
/FnMut
/Fn
):
#![feature(fn_traits, unboxed_closures)]
struct ComposeTwo<G, F>(F, G);
impl<A, B, C, G, F> std::ops::FnOnce<(A,)> for ComposeTwo<G, F>
where
F: FnOnce(A) -> B,
G: FnOnce(B) -> C,
{
type Output = C;
extern "rust-call" fn call_once(self, (x,): (A,)) -> Self::Output {
(self.1)((self.0)(x))
}
}
impl<A, B, C, G, F> std::ops::FnMut<(A,)> for ComposeTwo<G, F>
where
F: FnMut(A) -> B,
G: FnMut(B) -> C,
{
extern "rust-call" fn call_mut(&mut self, (x,): (A,)) -> Self::Output {
(self.1)((self.0)(x))
}
}
impl<A, B, C, G, F> std::ops::Fn<(A,)> for ComposeTwo<G, F>
where
F: Fn(A) -> B,
G: Fn(B) -> C,
{
extern "rust-call" fn call(&self, (x,): (A,)) -> Self::Output {
(self.1)((self.0)(x))
}
}
fn compose_two<G, F>(f: F, g: G) -> ComposeTwo<G, F> {
ComposeTwo(f, g)
}
打破这一假设的另一种方式是通过使返回的闭包Copy
.Copy
类型永远不能实现Drop
,编译器知道这一点,并假定它们不实现.不幸的是,因为闭包捕获了f
和g
,所以它们也需要是Copy
:
fn compose_two<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C + Copy
where
F: Fn(A) -> B + Copy,
G: Fn(B) -> C + Copy,
{
move |x| g(f(x))
}
最后一种方式是最难解释的.首先,我需要解释为什么编译器认为闭包可以捕获x
,而实际上它不能.
让我们首先想一想为什么闭包不能做到这一点:它将在多长时间内取代下面的'?
?
struct Closure {
f: some_function_type,
g: some_function_type,
captured_x: Option<&'? mut Foo>,
}
当baz
被定义时(我们必须决定我们将使用哪个生命周期),我们仍然不知道什么将被传递给闭包,所以我们不知道我们应该使用哪个生命周期!
这一知识本质上是"any年后才能调用闭包",在Rust中通过了Higher-Ranked Trait Bounds (HRTB),拼写为for<'lifetime>
.所以,compose_two()
人中的A
人应该是HRTB.
但问题就在这里:泛型参数不能是HRTB.它们必须实例化为concrete个生命周期.因此,编译器为baz
Select 了某个生存期'x
,这个生存期必须大于baz
本身-否则它将包含一个悬垂的生存期-因此理论上它可以有一个具有该生存期的成员,因此编译器认为baz
可以存储对x
的引用,而实际上它不能.
如果我们能成为HRTB就好了.
我们可以的!如果我们不将其完全泛型,而是将其指定为引用:
fn compose_two<A, B, C, G, F>(f: F, g: G) -> impl for<'a> Fn(&'a mut A) -> &'a mut C
where
F: for<'a> Fn(&'a mut A) -> &'a mut B,
G: for<'a> Fn(&'a mut B) -> &'a mut C,
{
move |x| g(f(x))
}
或者,使用省略形式,因为HRTB是Fn
个性状界限的默认值:
fn compose_two<A, B, C, G, F>(f: F, g: G) -> impl Fn(&mut A) -> &mut C
where
F: Fn(&mut A) -> &mut B,
G: Fn(&mut B) -> &mut C,
{
move |x| g(f(x))
}
不幸的是,它也需要B: 'static
,因为编译器不能得出B
是否足够长的结论(该语言的另一个限制),但它仍然有效!
fn compose_two<A, B: 'static, C, G, F>(f: F, g: G) -> impl Fn(&mut A) -> &mut C
where
F: Fn(&mut A) -> &mut B,
G: Fn(&mut B) -> &mut C,
{
move |x| g(f(x))
}