使用Rust编译器来防止忘记调用方法

Sun*_*rma 8 api-design type-safety rust

我有一些像这样的代码:

foo.move_right_by(10);
//do some stuff
foo.move_left_by(10);
Run Code Online (Sandbox Code Playgroud)

最终我执行这两个操作非常重要,但我经常忘记在第一个之后执行第二个操作.它会导致很多错误,我想知道是否有一种习惯性的Rust方法来避免这个问题.当我忘记时,有没有办法让Rust编译器让我知道?

我的想法可能是某种类似的东西:

// must_use will prevent us from forgetting this if it is returned by a function
#[must_use]
pub struct MustGoLeft {
    steps: usize;
}

impl MustGoLeft {
    fn move(&self, foo: &mut Foo) {
        foo.move_left_by(self.steps);
    }
}

// If we don't use left, we'll get a warning about an unused variable
let left = foo.move_left_by(10);

// Downside: move() can be called multiple times which is still a bug
// Downside: left is still available after this call, it would be nice if it could be dropped when move is called
left.move();
Run Code Online (Sandbox Code Playgroud)

有没有更好的方法来实现这一目标?

另一个想法是实现Drop并且panic!如果在没有调用该方法的情况下删除结构.这不是很好,因为它是运行时检查,这是非常不受欢迎的.

编辑:我意识到我的例子可能太简单了.所涉及的逻辑可能变得非常复杂.例如,我们有这样的事情:

foo.move_right_by(10);
foo.open_box(); // like a cardboard box, nothing to do with Box<T>
foo.move_left_by(10);
// do more stuff...
foo.close_box();
Run Code Online (Sandbox Code Playgroud)

请注意如何以良好的,正确嵌套的顺序执行操作.唯一重要的是事后总是调用逆操作.有时需要以某种方式指定顺序,以使代码按预期工作.

我们甚至可以这样:

foo.move_right_by(10);
foo.open_box(); // like a cardboard box, nothing to do with Box<T>
foo.move_left_by(10);
// do more stuff...
foo.move_right_by(10);
foo.close_box();
foo.move_left_by(10);
// do more stuff...
Run Code Online (Sandbox Code Playgroud)

Pet*_*all 14

您可以使用幻像类型来携带附加信息,这些信息可用于类型检查而无需任何运行时成本.一个限制是move_left_bymove_right_by,因为他们需要改变的类型必须返回一个新的拥有对象,但通常这不会是一个问题.

此外,如果您实际上没有在结构中使用类型,编译器会抱怨,因此您必须添加使用它们的字段.Rust std提供零大小的PhantomData类型作为此目的的便利.

您的约束可以像这样编码:

use std::marker::PhantomData;

pub struct GoneLeft;
pub struct GoneRight;
pub type Completed = (GoneLeft, GoneRight);

pub struct Thing<S = ((), ())> {
    pub position: i32,
    phantom: PhantomData<S>,
}


// private to control how Thing can be constructed
fn new_thing<S>(position: i32) -> Thing<S> {
    Thing {
        position: position,
        phantom: PhantomData,
    }
}

impl Thing {
    pub fn new() -> Thing {
        new_thing(0)
    }
}

impl<L, R> Thing<(L, R)> {
    pub fn move_left_by(self, by: i32) -> Thing<(GoneLeft, R)> {
        new_thing(self.position - by)
    }

    pub fn move_right_by(self, by: i32) -> Thing<(L, GoneRight)> {
        new_thing(self.position + by)
    }
}
Run Code Online (Sandbox Code Playgroud)

你可以像这样使用它:

// This function can only be called if both move_right_by and move_left_by
// have been called on Thing already
fn do_something(thing: &Thing<Completed>) {
    println!("It's gone both ways: {:?}", thing.position);
}

fn main() {
    let thing = Thing::new()
          .move_right_by(4)
          .move_left_by(1);
    do_something(&thing);
}
Run Code Online (Sandbox Code Playgroud)

如果您错过了所需的方法之一,

fn main(){
    let thing = Thing::new()
          .move_right_by(3);
    do_something(&thing);
}
Run Code Online (Sandbox Code Playgroud)

然后你会得到一个编译错误:

error[E0308]: mismatched types
  --> <anon>:49:18
   |
49 |     do_something(&thing);
   |                  ^^^^^^ expected struct `GoneLeft`, found ()
   |
   = note: expected type `&Thing<GoneLeft, GoneRight>`
   = note:    found type `&Thing<(), GoneRight>`
Run Code Online (Sandbox Code Playgroud)


Eri*_*aas 6

#[must_use]在这种情况下,我不认为你真正想要的是什么.这是解决问题的两种不同方法.第一个是在封闭中包含你需要做的事情,并抽象掉直接调用:

#[derive(Debug)]
pub struct Foo {
    x: isize,
    y: isize,
}

impl Foo {
    pub fn new(x: isize, y: isize) -> Foo {
        Foo { x: x, y: y }
    }

    fn move_left_by(&mut self, steps: isize) {
        self.x -= steps;
    }

    fn move_right_by(&mut self, steps: isize) {
        self.x += steps;
    }

    pub fn do_while_right<F>(&mut self, steps: isize, f: F)
        where F: FnOnce(&mut Self)
    {
        self.move_right_by(steps);
        f(self);
        self.move_left_by(steps);
    }
}

fn main() {
    let mut x = Foo::new(0, 0);
    println!("{:?}", x);
    x.do_while_right(10, |foo| {
        println!("{:?}", foo);
    });
    println!("{:?}", x);
}
Run Code Online (Sandbox Code Playgroud)

第二种方法是创建一个包装类型,它在删除时调用该函数(类似于Mutex::lock生成一个MutexGuard解锁Mutex时删除的函数):

#[derive(Debug)]
pub struct Foo {
    x: isize,
    y: isize,
}

impl Foo {
    fn new(x: isize, y: isize) -> Foo {
        Foo { x: x, y: y }
    }

    fn move_left_by(&mut self, steps: isize) {
        self.x -= steps;
    }

    fn move_right_by(&mut self, steps: isize) {
        self.x += steps;
    }

    pub fn returning_move_right(&mut self, x: isize) -> MovedFoo {
        self.move_right_by(x);
        MovedFoo {
            inner: self,
            move_x: x,
            move_y: 0,
        }
    }
}

#[derive(Debug)]
pub struct MovedFoo<'a> {
    inner: &'a mut Foo,
    move_x: isize,
    move_y: isize,
}

impl<'a> Drop for MovedFoo<'a> {
    fn drop(&mut self) {
        self.inner.move_left_by(self.move_x);
    }
}

fn main() {
    let mut x = Foo::new(0, 0);
    println!("{:?}", x);
    {
        let wrapped = x.returning_move_right(5);
        println!("{:?}", wrapped);
    }
    println!("{:?}", x);
}
Run Code Online (Sandbox Code Playgroud)

  • Rust中的@SunjayVarma,由于所有权如何运作,掉线应该是完全确定的; 你也可以将原始的`Foo`包装在某个带有`drop()`方法的类型中,你可以确保这个包装器对象的所有权在适当的时候超出了范围.对我来说,感觉就像基于drop的解决方案可能是正确的方法. (2认同)
  • 我同意放弃是正确的方法.如果你真的需要在范围结束之前发生drop,你也可以调用en empty函数来获取值的所有权,如下所示:https://gist.github.com/silmeth/f1d66c819862b418fd8862e3dc36875a - 但是当你忘记手动完成,最终会在一个范围的末尾发生丢弃,所以你不会错过调用`move_right()`方法. (2认同)