2017-02-04 81 views
8

我有一些像这样的代码:防止忘记调用一个方法

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

这真的很重要,我的那些操作最终的同时执行,但我常常忘记做第二第一个之后。它导致了很多错误,我想知道是否有一种习惯性的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!如果在没有调用该方法的情况下丢弃该结构。这不是很好,因为它是一个运行时检查,并且这是非常不可取的。

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

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

只是为了澄清:两个方法必须在封闭函数内的某个点被调用?订单是否相关?或者你只是想确保,*如果*'move_right()'被调用,'move_left()'也被调用?另外:你可以描述封闭功能吗?它会返回任何东西吗? –

+0

OP,如果在调用move_right()后必须调用'move_left()',则可以将这些功能打包以返回自动调用'move_left()'的Drop类型的自定义类型 - 尽管如果你的第二个FN必须返回值等等,这可能是不可能的......这可能是一个想法,但它可能会很快变得丑陋。 – musicmatze

+1

如果你可以表示一系列调用作为一个状态机来执行,那么状态可以被编码为类型,并在'self'上进行方法调用(消耗当前状态并产生一个新状态)。这是可能的,还是不够灵活? –

回答

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); 
} 
+0

感谢您的回答!不幸的是,关闭方法并不适用,因为它需要比这更灵活一点。我在问题中增加了另一个例子来澄清。第二种方法很有趣,但如果我想明确地执行反操作,该怎么办?很难依靠编译器来降低时间。 –

+2

@SunjayVarma在Rust中,由于所有权的工作原理,下降应该是完全确定性的;你也可以用'drop()'方法包装你的原始'Foo',并且你可以确保这个包装器对象的所有权在适当的时候超出了范围。对我来说,感觉就像基于drop的解决方案可能是正确的路要走。 – djc

+2

我同意放弃是正确的方式。如果你真的需要在范围结束之前发生掉落,你也可以直接调用en空值函数来获取值的所有权,如下所示:https://gist.github.com/silmeth/f1d66c819862b418fd8862e3dc36875a - 但是当你忘了手动完成,最终会在范围的末尾发生,所以你不会错过调用'move_right()'方法。 – silmeth

12

您可以使用幻象类型携带额外的信息,这些信息可以用来进行类型检查。限制是move_left_bymove_right_by必须返回一个新的拥有对象,因为它们需要更改类型,但通常这不会成为问题。

此外,如果您实际上没有在结构中使用类型,编译器会发出抱怨,因此您必须添加使用它们的字段。 Rust的std为此提供了PhantomData。这种类型的值没有运行时间成本。

它可以是一个有点笨拙,但你的约束,可以像这样编码:

use std::marker::PhantomData; 

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

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


// private 
fn new_thing<S>(position: i32) -> Thing<S> { 
    Thing { 
     position: position, 
     phantom: PhantomData, 
    } 
} 

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

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

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

pub fn main() { 
    let thing = Thing::new() 
      .move_right_by(4) 
      .move_left_by(1); 
    do_something(&thing); 
} 

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

pub 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>` 
+0

这是一个有趣的方法!参与编译器的好方法是确保它发生。 –

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/将被使用。

+0

这与我的答案类似,但实际上更简单一些。我认为有一些类似的方法,甚至可能创建一个可以生成类型级别状态和转换的宏。 –