Что мне делать, если я хочу передать один изменяемый объект в несколько параметров функции?

#rust #borrow-checker #mutability

Вопрос:

Я написал программу на Rust, которая воспроизводит музыку с помощью шаговых двигателей, и теперь я хотел бы добавить несколько поддельных объектов, чтобы я мог выполнить автоматическое тестирование. Однако я не знаю хорошего способа определить эти поддельные объекты таким образом, чтобы моя программа действительно могла их использовать.

Вы можете запустить мой код на игровой площадке Rust.

Та часть, которая работает

Основной цикл программы использует два объекта признаков. Один объект реализует называемую черту Timer , которая представляет собой таймер, который можно использовать для того, чтобы события происходили через регулярные промежутки времени. Другой объект реализует свойство , называемое Motor и представляющее сам шаговый двигатель. Определения этих черт приведены в моем посте на игровой площадке Rust.

Основной цикл просто ждет 500 микросекунд, поднимает штифт «шаг» ВЫСОКО, ждет еще 500 микросекунд, опускает штифт «шаг» НИЗКО и повторяется в общей сложности 1000 циклов. Это приводит к тому, что двигатель делает 1000 шагов в секунду в течение одной секунды.

 fn spin<T: Timer, M: Motor>(timer: amp;mut T, motor: amp;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) рабочая реализация Timer , и рабочая реализация Motor , и spin функция заставляет двигатель вращаться, и это звучит красиво.

Проблемная часть

Я хочу иметь возможность проводить автоматическое тестирование, поэтому я написал «поддельный» объект, который реализует Motor и Timer таким образом полезен для тестирования. Сам тип — это просто структура:

 /// 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,
}
 

The implementations of set_step_high and set_step_low just set is_on to true and false (respectively), and the implementation of wait_microseconds just checks whether or not is_on is true and adds the given amount of time to time_high if so. Those implementations are in my Rust Playground post.

Naively, I would expect to be able to pass a DummyTimerMotor object as both parameters of spin , and then look at time_high afterward, and see that it has a value of 500000. However, that is, of course, not allowed:

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

This gives an error message: «cannot borrow dummy as mutable more than once at a time.»

I know exactly why I’m getting that error message, and it makes sense. What’s a good way to get the behavior I’m trying to get?

I only have one reasonable idea: change spin so that instead of taking an object which implements Timer , and another object which implements Motor , it takes a single object which implements both Timer and Motor . However, that seems inelegant to me (as a Rust newbie). Conceptually, a timer is one thing and a motor is another thing; having spin take a single object which is both a timer and a motor is quite unintuitive. It doesn’t seem like I should change the way that spin is implemented merely to accommodate the details of how the timer and motor are implemented.


Полный список кодов

На случай, если игровая площадка Rust когда-нибудь выйдет из строя, ниже приведена вся программа, которая у меня там есть, вместе со всеми выводами ошибок.

 /// 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(amp;mut self);
    /// Advance the timer's reference time by the given number of microseconds,
    /// then wait until the reference time.
    fn wait_microseconds(amp;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(amp;mut self);
    /// Pull the "step" pin LOW, in preparation for pulling it HIGH again.
    fn set_step_low(amp;mut self);
}

fn spin<T: Timer, M: Motor>(timer: amp;mut T, motor: amp;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(amp;mut self) { }
    
    fn wait_microseconds(amp;mut self, duration: u64) {
        if self.is_on {
            self.time_high  = duration;
        }
    }
}

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

fn main() {
    let mut dummy: DummyTimerMotor = DummyTimerMotor {
        is_on: false, time_high: 0
    };
    
    spin(amp;mut dummy, amp;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(amp;mut dummy, amp;mut dummy); // Oops, not allowed!
   |     ---- ----------  ^^^^^^^^^^ second mutable borrow occurs here
   |     |    |
   |     |    first mutable borrow occurs here
   |     first borrow later used by call
 

Комментарии:

1. Может быть, глупый вопрос, но почему бы вам не реализовать DummyTimer и DummyMotor ? Почему вы объединили их в одно целое ?>

2. doc.rust-lang.org/nomicon/borrow-splitting.html

3. @SvetlinZarev Моя реализация Timer смотрит на is_on переменную, чтобы знать, добавлять ли заданное количество времени time_high или нет, и моя реализация Motor устанавливает is_on переменную. Я не вижу очевидного способа реализовать их отдельно, учитывая, что они взаимодействуют таким образом.

4. «таймер-это одно, а двигатель-совсем другое». Тогда с точки зрения дизайна один объект не должен быть a Timer и a Motor одновременно или, по крайней мере, не использоваться в двух местах, ведущих себя как они оба в двух разных местах (я имею в виду аргументы функций).

5. Очень небезопасно и, вероятно, страдает от UB: play.rust-lang.org/…

Ответ №1:

У вас может быть отдельное DummyTimer и DummyMotor общее состояние с помощью Rc<RefCell<State>> :

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

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

impl Timer for DummyTimer {
    fn reset(amp;mut self) { }
    
    fn wait_microseconds(amp;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(amp;mut self) {
        self.state.borrow_mut().is_on = true;
    }
    
    fn set_step_low(amp;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 (amp;state), };
    let mut timer = DummyTimer { state: Rc::clone (amp;state), };

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

Игровая площадка

Ответ №2:

По понятным причинам у вас не может быть двух изменяемых ссылок на ваш DummyTimerMotor , но вы можете попробовать некоторые unsafe хакерские методы, чтобы добиться чего-то подобного. Этот подход вдохновлен slice::split_at_mut и tokio::io::split

В принципе, вы можете создать два прокси-объекта, один реализующий Timer и один реализующий Motor :

 impl DummyTimerMotor {
    pub fn split<'a>(amp;'a mut self) -> (impl Timer   'a, impl Motor   'a) {
        let ptr_is_on = amp;mut self.is_on as *mut bool;
        let ptr_time_high = amp;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<amp;'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(amp;mut self) {
        //
    }

    fn wait_microseconds(amp;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<amp;'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(amp;mut self) {
        unsafe { *self.is_on = true }
    }

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

Правила, касающиеся необработанных указателей, очень расплывчаты, и я не смог найти ни одного авторитетного
документа о том, разрешено ли вам разыменовывать 2 изменяемых необработанных указателя на одни и те
же данные. Все, что я смог найти, это то, что нельзя разыменовывать исходный указатель mut
, пока есть ссылка mut, потому что это нарушило бы правила псевдонимирования.

Ну, в таком случае здесь вообще нет ссылок, так что я думаю, что это нормально. Я провел несколько экспериментов с МИРИ, и она не сообщила о каких-либо нарушениях, но я не уверен, что это какая-либо гарантия поведения без UB.

Тогда вы можете использовать его так (игровая площадка):

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

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

    spin(amp;mut t, amp;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 лучше подходит для вашего варианта использования, но, поскольку я потратил довольно много времени на его изучение, я все равно решил опубликовать его в надежде, что кто-нибудь прольет свет на небезопасные аспекты моего подхода