首页 文章

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

提问于
浏览
8

我有一些像这样的代码:

foo.move_right_by(10);
//do some stuff
foo.move_left_by(10);

最终我执行这两个操作非常重要,但我经常忘记在第一个之后执行第二个操作 . 它会导致很多错误,我想知道是否有一种习惯性的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();

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

如果在没有调用该方法的情况下删除结构,则另一个想法是实现 Droppanic! . 这不是运行时检查,这是非常不受欢迎的 .

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

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();

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

我们甚至可以这样:

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...

3 回答

  • 14

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

    此外,如果您不提供't actually use the types in your struct, so you have to add fields that use them. Rust' s 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)
        }
    }
    

    你可以像这样使用它:

    // 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);
    }
    

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

    fn main(){
        let thing = Thing::new()
              .move_right_by(3);
        do_something(&thing);
    }
    

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

    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>`
    
  • 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);
    }
    

    第二种方法是创建一个包装类型,在删除时调用该函数(类似于 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);
    }
    
  • 1

    我只查看了最初的描述,可能错过了对话中的细节,但强制执行操作的一种方法是使用原始对象(向右)并将其替换为强制您向左移动相同数量的对象,然后才可以做任何你想做的事情来完成任务 .

    新类型可以在进入完成状态之前禁止/要求进行不同的调用 . 例如(未经测试):

    struct CanGoRight { .. }
    impl CanGoRight {
        fn move_right_by(self, steps: usize) -> MustGoLeft {
            // Note: self is consumed and only `MustGoLeft` methods are allowed
            MustGoLeft{steps: steps}
        }
    }
    struct MustGoLeft {
        steps: usize;
    }
    impl MustGoLeft {
        fn move_left_by(self, steps: usize) -> Result<CanGoRight, MustGoLeft> {
            // Totally making this up as I go here...
            // If you haven't moved left at least the same amount of steps,
            // you must move a bit further to the left; otherwise you must
            // switch back to `CanGoRight` again
            if steps < self.steps {
                Err(MustGoLeft{ steps: self.steps - steps })
            } else {
                Ok(CanGoRight{ steps: steps - self.steps })
            }
        }
        fn open_box(self) -> MustGoLeftCanCloseBox {..}
    }
    
    let foo = foo.move_right_by(10); // can't move right anymore
    

    此时 foo 无法再向右移动,因为 MustGoLeft 不允许,但它可以向左移动或打开框 . 如果它向左移动得足够远,它会再次回到 CanGoRight 状态 . 但如果它打开盒子,则适用全新的规则 . 无论哪种方式,你都必须处理这两种可能性 .

    各州之间可能会有一些重复,但应该很容易重构 . 添加自定义特征可能会有所帮助 .

    最后,听起来你正在制作各种各样的状态机 . 也许https://hoverbear.org/2016/10/12/rust-state-machine-pattern/会有用 .

相关问题