Как разделить изменяемый модуль записи между несколькими кодировщиками?

Я реализую интерфейс сжатия данных:

pub trait NumericEncoder<V> {
    fn encode(&mut self, value: V) -> io::Result<()>;
}

Кодер может закодировать какое-то число в каком-то виде вывода, где вывод может быть потоком (файлом), байтовым буфером или даже другим кодировщиком. Можно было бы вызвать такую ​​реализацию:

let f = File::create("out").unwrap();
// Delta encoder whose data is run-length-compressed
let mut enc = DeltaEncoder::new(RunLengthEncoder::new(f));
enc.encode(123).unwrap();

Это все хорошо, но в некоторых случаях мне нужно несколько кодировщиков для одного и того же выходного потока. Что-то вроде (упрощенно):

let f = File::create("out")?;
let mut idEnc = RunLengthEncoder::new(DeltaEncoder::new(f));
let mut dataEnc = LZEncoder::new(f);
for (id, data) in input.iter() {
    idEnc.encode(id);
    dataEnc.encode(data);
}

Здесь два кодировщика будут чередовать свои данные по мере их записи.

Для этого требуется изменяемый доступ к одному и тому же файлу, что невозможно с прямыми ссылками &mut. Насколько я могу судить, это можно сделать только с помощью RefCell; Есть ли способ лучше?

Насколько я могу судить, это сделало бы все реализации кодировщика менее чистыми. Прямо сейчас кодировщик может быть объявлен следующим образом:

pub struct MySpecialEncoder<'a, V, W>
where
    W: io::Write,
{
    w: &'a mut W,
    phantom: std::marker::PhantomData<V>,
}

С RefCell каждая структура кодировщика и конструктор должны иметь дело с Rc<RefCell<W>>, что не так хорошо и пропускает совместное использование модуля записи в кодировщик, которому не нужно знать, что модуль записи является общим.

(Я подумал, могу ли я изменить трейт NumericEncoder, чтобы он принимал аргумент записи, который должен быть std::io::Write. Это не сработает, потому что некоторые кодировщики записывают не в std::io::Write, а в другой NumericEncoder.)


person Alexander Staubo    schedule 14.04.2019    source источник
comment
Почему ваша структура должна содержать ссылку на файл? Почему бы просто не дать им это при вызове encode ? idEnc.encode(f, id); dataEnc.encode(f, data); это обеспечивает большую гибкость.   -  person Stargateur    schedule 14.04.2019
comment
Это не сработает, потому что некоторые кодировщики пишут не в std::io::Write, а в другой NumericEncoder. что не понятно. Для этого может потребоваться минимально воспроизводимый пример.   -  person Stargateur    schedule 14.04.2019
comment
Это не сработает, потому что некоторые кодировщики пишут не в std::io::Write, а в другой NumericEncoder — так почему бы не реализовать NumericEncoder для T: io::Write? Затем измените его подпись, чтобы принять другой NumericEncoder   -  person Laney    schedule 14.04.2019
comment
Idiomatic Rust использует snake_case для переменных, методов, макросов, полей и модулей; UpperCamelCase для типов и вариантов перечисления; и SCREAMING_SNAKE_CASE для статики и констант. Используйте вместо этого id_enc / data_enc, пожалуйста.   -  person Shepmaster    schedule 14.04.2019
comment
Эти вопросы заставили меня понять, что я не продумывал подпись. Несмотря на то, что некоторые кодировщики пишут в другой кодировщик, а не в W, я, конечно, могу сделать W частью подписи ( encode(W, V)), потому что кодировщики могут просто передать аргумент записи своему следующему кодировщику вместо того, чтобы использовать его. Это означает, что структурам кодировщика не нужно нести с собой модуль записи. Спасибо, @Laney и @Stargateur.   -  person Alexander Staubo    schedule 14.04.2019


Ответы (1)


единственный способ сделать это с помощью RefCell

Подойдет любой тип, предоставляющий внутреннюю изменчивость. Например, также достаточно Mutex.

это сделало бы все реализации кодировщика менее чистыми

Я не знаю, почему ты в это веришь. Создайте тип, который использует внутреннюю изменчивость, и используйте этот тип только тогда, когда вам нужна эта дополнительная функциональность:

#[derive(Debug)]
struct Funnel<E>(Rc<RefCell<E>>);

impl<E> Funnel<E> {
    fn new(e: E) -> Self {
        Funnel(Rc::new(RefCell::new(e)))
    }
}

impl<E> Clone for Funnel<E> {
    fn clone(&self) -> Self {
        Funnel(self.0.clone())
    }
}

impl<V, E> NumericEncoder<V> for Funnel<E>
where
    E: NumericEncoder<V>,
{
    fn encode(&mut self, value: V) -> io::Result<()> {
        self.0.borrow_mut().encode(value)
    }
}
fn main() -> io::Result<()> {
    let s = Shared;

    let s1 = Funnel::new(s);
    let s2 = s1.clone();

    let mut e1 = Wrapper(s1);
    let mut e2 = Wrapper(s2);

    e1.encode(1)?;
    e2.encode(2)?;

    Ok(())
}

Вы также должны подумать о том, чтобы брать W по значению, и я не уверен, зачем вам нужен PhantomData — мой код этого не делал.

Смотрите также:

person Shepmaster    schedule 14.04.2019
comment
Спасибо, есть о чем почитать! Я полагаю, что PhantomData не был так полезен в этом случае, но я использовал его в другом месте, чтобы избежать вывода типа возвращаемого типа: я могу создать конкретную переменную SomeDecoder<u32> и знать, что let v = decoder.decode() всегда возвращает io::Result<u32> вместо let v: io::Result<u32> = decoder.decode(). Кажется, что в let v: u32 = decoder.decode()? Rust не может определить внутреннюю переменную типа Result, но я могу ошибаться. Я получаю сообщение об ошибке компиляции, говорящее, что моя функция возвращает (). То же самое с выводом типа в match decoder.decode(). - person Alexander Staubo; 14.04.2019