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

时间:2017-02-04 05:26:51

标签: rust api-design type-safety

我有一些像这样的代码:

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!。这不是很好,因为它是一个运行时检查,这是非常不受欢迎的。

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

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 个答案:

答案 0 :(得分: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)
    }
}

你可以像这样使用它:

// 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>`

答案 1 :(得分: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);
}

答案 2 :(得分:1)

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

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

foo

此时MustGoLeft无法向右移动,因为CanGoRight不允许,但它可以向左移动或打开框。如果它向左移动得足够远,它会再次回到<div class="scrollLink"> <ul> <li class="link"><a href="#one">link</a></li> <li class="link"><a href="#two">link</a></li> <li class="link"><a href="#three">link</a></li> </ul> </div> <section id="one">content</section> <section id="two">content</section> <section id="three">content</section> var sections = $('section') , nav = $('li') , nav_height = nav.outerHeight(); $(window).on('scroll', function () { var cur_pos = $(this).scrollTop(); sections.each(function() { var top = $(this).offset().top - nav_height, bottom = top + $(this).outerHeight(); if (cur_pos >= top && cur_pos <= bottom) { nav.find('a').removeClass('active'); sections.removeClass('active'); $(this).addClass('active'); nav.find('a[href="#'+$(this).attr('id')+'"]').addClass('active'); } }); }); 状态。但如果它打开盒子,则适用全新的规则。无论哪种方式,你都必须处理这两种可能性。

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

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