如果我想将单个可变对象传递给函数的多个参数,我该怎么办?

What should I do if I would like to pass a single mutable object into multiple parameters of a function?

我用 Rust 编写了一个使用步进电机播放音乐的程序,现在我想添加一些假对象以便进行自动化测试。但是,我不知道如何以我的程序可以实际使用它们的方式来定义这些假对象。

你可以run my code on the Rust Playground.

有效的部分

程序的主循环使用了两个特征对象。一个对象实现了一个名为 Timer 的特征,它表示一个计时器,可以用来让事情定期发生。另一个对象实现了一个名为 Motor 的特征,代表步进电机本身。这些特征的定义在 my Rust Playground post.

主循环只需等待 500 微秒,将“步进”引脚拉高,再等待 500 微秒,将“步进”引脚拉低,并重复总共 1,000 个周期。这会导致电机每秒步进 1,000 次。

fn spin<T: Timer, M: Motor>(timer: &mut T, motor: &mut M) {
    timer.reset();
    for _ in 0..1000 {
        timer.wait_microseconds(500);
        motor.set_step_high();
        timer.wait_microseconds(500);
        motor.set_step_low();
    }
}

到目前为止,一切正常。我有(在我的真实代码中,不是 Rust Playground post)Timer 的工作实现,Motor 的工作实现,spin 函数使电机旋转,听起来很美。

有问题的部分

我希望能够进行自动化测试,所以我编写了一个“假”对象,它以对测试有用的方式实现了 MotorTimer。类型本身只是一个结构:

/// A mock timer and motor which simply tracks the amount of simulated time that
/// the motor driver has its "step" pin pulled HIGH.
struct DummyTimerMotor {
    is_on: bool,
    time_high: u64,
}

set_step_highset_step_low的实现只是将is_on设置为truefalse(分别),而[=27=的实现] 只是检查 is_on 是否为 true,如果是,则将给定的时间添加到 time_high。这些实现在 my Rust Playground post.

天真地,我希望能够传递一个DummyTimerMotor对象作为spin的两个参数,然后再查看time_high,看到它有一个值的500000。不过,那是当然不允许的:

fn main() {
    let mut dummy: DummyTimerMotor = DummyTimerMotor {
        is_on: false, time_high: 0
    };
    
    spin(&mut dummy, &mut dummy); // Oops, not allowed!
    
    print!("The 'step' pin was HIGH for {} microseconds", dummy.time_high);
}

这给出了一条错误消息:“不能一次多次借用 dummy 作为可变的。”

我很清楚为什么会收到该错误消息,这是有道理的。获得我想要获得的行为的好方法是什么?

我只有一个合理的想法:更改 spin,而不是采用一个实现 Timer 的对象和另一个实现 Motor 的对象,而是采用一个对象,它同时实现 TimerMotor。然而,这对我来说似乎不够优雅(作为 Rust 新手)。从概念上讲,定时器是一回事,电机是另一回事。让 spin 取一个既是计时器又是电机的对象是非常不直观的。我似乎不应该改变 spin 的实现方式,只是为了适应定时器和电机的实现方式的细节。


完整代码清单

万一 Rust Playground 出现故障,下面是我在那里的整个程序,以及整个错误输出。

/// A timer which can be used to make things happen at regular intervals.
trait Timer {
    /// Set the timer's reference time to the current time.
    fn reset(&mut self);
    /// Advance the timer's reference time by the given number of microseconds,
    /// then wait until the reference time.
    fn wait_microseconds(&mut self, duration: u64);
}

/// The interface to a stepper motor driver.
trait Motor {
    /// Pull the "step" pin HIGH, thereby asking the motor driver to move the
    /// motor by one step.
    fn set_step_high(&mut self);
    /// Pull the "step" pin LOW, in preparation for pulling it HIGH again.
    fn set_step_low(&mut self);
}

fn spin<T: Timer, M: Motor>(timer: &mut T, motor: &mut M) {
    timer.reset();
    for _ in 0..1000 {
        timer.wait_microseconds(500);
        motor.set_step_high();
        timer.wait_microseconds(500);
        motor.set_step_low();
    }
}

/// A mock timer and motor which simply tracks the amount of simulated time that
/// the motor driver has its "step" pin pulled HIGH.
struct DummyTimerMotor {
    is_on: bool,
    time_high: u64,
}

impl Timer for DummyTimerMotor {
    fn reset(&mut self) { }
    
    fn wait_microseconds(&mut self, duration: u64) {
        if self.is_on {
            self.time_high += duration;
        }
    }
}

impl Motor for DummyTimerMotor {
    fn set_step_high(&mut self) {
        self.is_on = true;
    }
    
    fn set_step_low(&mut self) {
        self.is_on = false;
    }
}

fn main() {
    let mut dummy: DummyTimerMotor = DummyTimerMotor {
        is_on: false, time_high: 0
    };
    
    spin(&mut dummy, &mut dummy); // Oops, not allowed!
    
    print!("The 'step' pin was HIGH for {} microseconds", dummy.time_high);
}
error[E0499]: cannot borrow `dummy` as mutable more than once at a time
  --> src/main.rs:61:22
   |
61 |     spin(&mut dummy, &mut dummy); // Oops, not allowed!
   |     ---- ----------  ^^^^^^^^^^ second mutable borrow occurs here
   |     |    |
   |     |    first mutable borrow occurs here
   |     first borrow later used by call

您可以有不同的 DummyTimerDummyMotor,他们通过 Rc<RefCell<State>> 共享状态:

struct State {
    is_on: bool,
    time_high: u64,
}

struct DummyTimer {
    state: Rc<RefCell<State>>,
}

impl Timer for DummyTimer {
    fn reset(&mut self) { }
    
    fn wait_microseconds(&mut self, duration: u64) {
        let mut t = self.state.borrow_mut();
        if t.is_on {
            t.time_high += duration;
        }
    }
}

struct DummyMotor {
    state: Rc<RefCell<State>>,
}

impl Motor for DummyMotor {
    fn set_step_high(&mut self) {
        self.state.borrow_mut().is_on = true;
    }
    
    fn set_step_low(&mut self) {
        self.state.borrow_mut().is_on = false;
    }
}

fn main() {
    let state = Rc::new (RefCell::new (State { is_on: false, time_high: 0, }));
    let mut motor = DummyMotor { state: Rc::clone (&state), };
    let mut timer = DummyTimer { state: Rc::clone (&state), };

    spin(&mut timer, &mut motor); // Now it's allowed
    
    print!("The 'step' pin was HIGH for {} microseconds", state.borrow().time_high);
}

Playground

出于显而易见的原因,您不能对 DummyTimerMotor 有两个可变引用, 但您可以尝试一些 unsafe hackery 来实现类似的目标。这种方法 灵感来自 slice::split_at_muttokio::io::split

基本上你可以创建两个代理对象,一个实现 Timer 和一个 实施 Motor:

impl DummyTimerMotor {
    pub fn split<'a>(&'a mut self) -> (impl Timer + 'a, impl Motor + 'a) {
        let ptr_is_on = &mut self.is_on as *mut bool;
        let ptr_time_high = &mut self.time_high as *mut u64;

        (
            TimerHalf::<()>::new(ptr_is_on, ptr_time_high),
            MotorHalf::<()>::new(ptr_is_on),
        )
    }
}

struct TimerHalf<'a, T: 'a> { // the dummy parameters are needed for the PhantomData
    is_on: *mut bool, // or *const bool instead
    time_high: *mut u64,
    // we need the phantom data in order to prevent someone from creating another 
    // borrow on the DummyTimerMotor and to track where it's safe to use this 
    // TimerHalf instance
    _phantom: PhantomData<&'a T>,
}

impl<'a, T> TimerHalf<'a, T> {
    fn new(is_on: *mut bool, time_high: *mut u64) -> TimerHalf<'a, ()> {
        TimerHalf {
            time_high,
            is_on,
            _phantom: PhantomData,
        }
    }
}

impl<'a, T> Timer for TimerHalf<'a, T> {
    fn reset(&mut self) {
        //
    }

    fn wait_microseconds(&mut self, duration: u64) {
        unsafe {
            if *self.is_on { // how safe is this ? 
                *self.time_high += duration;
            }
        }
    }
}

struct MotorHalf<'a, T: 'a> { // the dummy parameters are needed for the PhantomData
    is_on: *mut bool,
    // we need the phantom data in order to prevent someone from creating another 
    // borrow on the DummyTimerMotor and to track where it's safe to use this 
    // MotorHalf instance
    _phantom: PhantomData<&'a T>,
}

impl<'a, T> MotorHalf<'a, T> {
    fn new(is_on: *mut bool) -> MotorHalf<'a, ()> {
        MotorHalf {
            is_on,
            _phantom: PhantomData,
        }
    }
}

impl<'l, T> Motor for MotorHalf<'l, T> {
    fn set_step_high(&mut self) {
        unsafe { *self.is_on = true }
    }

    fn set_step_low(&mut self) {
        unsafe { *self.is_on = false }
    }
}

关于原始指针的规则非常模糊,我找不到任何权威的 有关是否允许取消引用 2 个可变原始指针的文档 相同的数据。我所能找到的就是不允许取消引用 mut 原始指针 只要有 mut 引用,因为那样会违反别名规则。

嗯,在那种情况下根本没有参考,所以我猜测这没问题。 我已经 运行 对 MIRI 进行了一些实验,它没有报告任何违规行为,但我 不确定这是否是无 UB 行为的任何保证。

然后就可以这样使用了(playground):

fn main() {
    let mut dummy: DummyTimerMotor = DummyTimerMotor {
        is_on: false,
        time_high: 0,
    };

    let (mut t, mut m) = dummy.split();

    spin(&mut t, &mut m);


    // let (mut t, mut m) = dummy.split(); - not allowed - already borrowed (that's fine!)
    // unless you drop the timer/motor halves :)

    drop(t); 
    drop(m);

    
    print!(
        "The 'step' pin was HIGH for {} microseconds",
        dummy.time_high
    );
}

相关资源:

PS:@Jmb 的解决方案 IS 更适合你的用例,但由于我花了很多时间研究这个,我决定post 无论如何,希望有人能阐明我方法的不安全方面