2017-02-04 38 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

這與我的答案類似,但實際上更簡單一些。我認爲有一些類似的方法,甚至可能創建一個可以生成類型級別狀態和轉換的宏。 –