如何在Rust中编写修改结构的函数

btqmn9zl  于 2023-03-02  发布在  其他
关注(0)|答案(2)|浏览(118)

以一个超级简单的结构体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?),因此编译器被迫假设它可能持有。

z9zf31ra

z9zf31ra1#

  • 能 * 这是工作吗?

没有。但是根据您的用例,可能有。
如果满足以下任一条件,则可以使其工作:

  • 您可以将函数类型约束为Copy(如果您使用函数项(fn),它们总是Copy,但是对于闭包,如果它们捕获非Copy类型,这可能是个问题)。
  • 您可以每晚使用。
  • 您可以更改compose!()main())的用户。
  • 可以将compose!()限制为引用(确切地说,是可变引用,但是也可以为共享引用创建一个版本,当然,如果你想为引用和拥有的类型创建单独的版本,这是可以的)。

这里有三个因素,它们结合在一起使编译器x可以在其生存期后使用。如果我们破坏其中一个,它将工作。其中两个实际上是假的,但编译器不知道(或不想依赖于此)。这些因素是:
1.编译器相信返回的闭包可以捕获它的参数。这是假的,我马上会解释,但编译器并不知道这一点。
1.编译器认为闭包有一个drop实现,并且可以在这个drop中使用x(在步骤1中捕获)。事实上,编译器知道它没有,但是因为我们使用了impl Trait,它被迫将其视为实现了drop,所以添加一个drop不会是一个破坏性的更改。

  1. xbaz之前被丢弃。这是真的(变量以与它们的声明相反的顺序被丢弃),并且结合编译器的两个先前的信念,这意味着当baz将(潜在地)在其丢弃中使用其捕获的x时,它将在x的生存期之后。
    让我们从最后一个声明开始,它是最容易被打破的,因为你只需要交换xbaz的顺序:
fn main() {
    let mut x = 3;
    let baz = compose_two(foo, bar);
    let y = baz(&mut x);
    println!("{:?}", y);
}

但是并不总是可以更改main(),或者可能无法在baz之前声明x
让我们回到第二个声明,编译器认为闭包有一个Drop impl,因为它在impl Trait中,如果它没有呢?
不幸的是,这需要每天晚上,因为手动编写闭包需要fn_traitsunboxed_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)
}

另一种打破这种假设的方法是使返回的闭包为CopyCopy类型永远不能实现Drop,编译器知道这一点,并假设它们不实现。不幸的是,由于闭包捕获fg,它们也需要为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被定义时(我们必须决定使用什么样的生存期),我们仍然不知道什么会被传递给闭包,所以我们不知道应该使用什么样的生存期!
这个知识,本质上是“闭包可以用 * 任意 * 生存期调用”,在Rust中通过 * 高级特征边界(Higher-Ranked Trait Bounds,HRTB)* 传递,拼写为for<'lifetime>,所以Acompose_two()中应该是HRTB。
但问题就在这里:泛型参数不能是HRTB。它们必须用一个 concrete 生存期示例化。因此,编译器为baz选择某个生存期'x,并且这个生存期必须大于baz本身-否则它将包含一个悬空生存期-因此理论上它可以有一个具有该生存期的成员。因此编译器相信X1 M42 N1 X可以存储对X1 M43 N1 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 trait边界的默认值:

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))
}
ee7vknir

ee7vknir2#

首先,使用rust-analyzer快速修复(vscode中的keybind ctrl+.)将宏内联几次会有所帮助:

fn main() {
    let baz = compose_two(foo, bar);
    let mut x = Foo { a: 3 };
    let y = baz(&mut x);
    println!("{:?}", y.a);
}

在这种情况下,它只需要调用compose_two的给定函数。编译器对错误的完整诊断如下:

error[E0597]: `x` does not live long enough
  --> src/main.rs:34:17
   |
34 |     let y = baz(&mut x);
   |                 ^^^^^^ borrowed value does not live long enough
35 |     println!("{:?}", y.a);
36 | }
   | -
   | |
   | `x` dropped here while still borrowed
   | borrow might be used here, when `baz` is dropped and runs the destructor for type `impl Fn(&mut Foo) -> &mut Foo`
   |
   = note: values in a scope are dropped in the opposite order they are defined

我不完全确定这里发生了什么,但我认为问题在于编译器无法推断出在调用baz之后,由于某种原因,可变引用没有被使用。如果内联调用compose_two,它就可以工作:

let baz = move |x| bar(foo(x));

如果您将baz定义为一个单独的函数,则该函数有效:

fn baz(x: &mut Foo) -> &mut Foo{
    compose_two(foo, bar)(x)
}

fn main() {
    let bazz = baz;
    let mut x = Foo { a: 3 };
    let y = bazz(&mut x);
    println!("{:?}", y.a);
}

但我不知道为什么它不能从baz(变量)的impl Fn(&mut Foo) -> &mut Foo类型中推断出这一点。
编辑:问题注解中的一些讨论在这里提供了一些启示。正如Chayim Friedman所描述的,编译器不能从impl Fn(&mut Foo) -> &mut Foo类型中假设太多,所以它假设它可能持有引用(但它没有)。这就是为什么如果你内联闭包或提取baz到它自己的函数,它会工作,因为这样编译器就有更多的信息。
在这种情况下,可以通过先声明x来修复,这样baz(编译器认为它持有对x的可变引用)就可以在x之前删除:

fn main() {
    let mut x = Foo { a: 3 };
    let baz = compose_two(foo,bar);
    let y = baz(&mut x);
    println!("{:?}", y.a);
}

相关问题