Нарушение внутренней изменчивости в дизайне API?

Aug 19 2020

Из-за моего опыта работы с C ++ меня не устраивает внутренняя изменчивость . Приведенный ниже код - это мое исследование по этой теме.

Я согласен с тем, что с точки зрения средства проверки заимствований невозможно иметь дело со многими ссылками на каждую структуру, внутреннее состояние которой может быть изменено рано или поздно; здесь явно может помочь внутренняя изменчивость.

Кроме того, в главе 15.5 «RefCell и интерьер Изменчивость Узор» из ржавчины языка программирования , пример о Messengerчерте и ее реализации на MockMessengerструктуры заставляют меня думать , что это общий дизайн API систематически предпочитает &selfболее , &mut selfдаже если его совершенно очевидны , что какая-то изменчивость рано или поздно станет обязательной. Как могла реализация Messengerне изменять свое внутреннее состояние при отправке сообщения? Исключением является просто печать сообщения, что согласуется с &self, но общий случай , вероятно, будет заключаться в записи в какой-то внутренний поток, что может подразумевать буферизацию, обновление флагов ошибок ... Все это, безусловно, требует &mut self, например,impl Write for File.

Полагаться на внутреннюю изменчивость для решения этой проблемы для меня звучит так, как в C ++, const_castзлоупотребление или злоупотребление mutableчленами только потому, что в другом месте приложения мы не были последовательны const(распространенная ошибка изучающих C ++).

Итак, вернемся к моему примеру кода ниже, я должен:

  • использовать &mut self(компилятор не жалуется, даже если это не обязательно) от change_e()до change_i(), чтобы соответствовать тому факту, что я изменяю значения сохраненных целых чисел?
  • продолжать использовать &self, потому что внутренняя изменчивость позволяет это, даже если я действительно изменю значения сохраненных целых чисел?

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

Я не могу найти ответ на этот вопрос в Rust API Guidelines . Есть ли другая документация на Rust, похожая на C ++ CoreGuidelines ?

/*
    $ rustc int_mut.rs && ./int_mut
     initial:   1   2   3   4   5   6   7   8   9
    change_a:  11   2   3   4   5   6   7   8   9
    change_b:  11  22   3   4   5   6   7   8   9
    change_c:  11  22  33   4   5   6   7   8   9
    change_d:  11  22  33  44   5   6   7   8   9
    change_e:  11  22  33  44  55   6   7   8   9
    change_f:  11  22  33  44  55  66   7   8   9
    change_g:  11  22  33  44  55  66  77   8   9
    change_h:  11  22  33  44  55  66  77  88   9
    change_i:  11  22  33  44  55  66  77  88  99
*/

struct Thing {
    a: i32,
    b: std::boxed::Box<i32>,
    c: std::rc::Rc<i32>,
    d: std::sync::Arc<i32>,
    e: std::sync::Mutex<i32>,
    f: std::sync::RwLock<i32>,
    g: std::cell::UnsafeCell<i32>,
    h: std::cell::Cell<i32>,
    i: std::cell::RefCell<i32>,
}

impl Thing {
    fn new() -> Self {
        Self {
            a: 1,
            b: std::boxed::Box::new(2),
            c: std::rc::Rc::new(3),
            d: std::sync::Arc::new(4),
            e: std::sync::Mutex::new(5),
            f: std::sync::RwLock::new(6),
            g: std::cell::UnsafeCell::new(7),
            h: std::cell::Cell::new(8),
            i: std::cell::RefCell::new(9),
        }
    }

    fn show(&self) -> String // & is enough (read-only)
    {
        format!(
            "{:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3}",
            self.a,
            self.b,
            self.c,
            self.d,
            self.e.lock().unwrap(),
            self.f.read().unwrap(),
            unsafe { *self.g.get() },
            self.h.get(),
            self.i.borrow(),
        )
    }

    fn change_a(&mut self) // &mut is mandatory
    {
        let target = &mut self.a;
        *target += 10;
    }

    fn change_b(&mut self) // &mut is mandatory
    {
        let target = self.b.as_mut();
        *target += 20;
    }

    fn change_c(&mut self) // &mut is mandatory
    {
        let target = std::rc::Rc::get_mut(&mut self.c).unwrap();
        *target += 30;
    }

    fn change_d(&mut self) // &mut is mandatory
    {
        let target = std::sync::Arc::get_mut(&mut self.d).unwrap();
        *target += 40;
    }

    fn change_e(&self) // !!! no &mut here !!!
    {
        // With C++, a std::mutex protecting a separate integer (e)
        // would have been used as two data members of the structure.
        // As our intent is to alter the integer (e), and because
        // std::mutex::lock() is _NOT_ const (but it's an internal
        // that could have been hidden behind the mutable keyword),
        // this member function would _NOT_ be const in C++.
        // But here, &self (equivalent of a const member function)
        // is accepted although we actually change the internal
        // state of the structure (the protected integer).
        let mut target = self.e.lock().unwrap();
        *target += 50;
    }

    fn change_f(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e)
        let mut target = self.f.write().unwrap();
        *target += 60;
    }

    fn change_g(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f)
        let target = self.g.get();
        unsafe { *target += 70 };
    }

    fn change_h(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g)
        self.h.set(self.h.get() + 80);
    }

    fn change_i(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g, h)
        let mut target = self.i.borrow_mut();
        *target += 90;
    }
}

fn main() {
    let mut t = Thing::new();
    println!(" initial: {}", t.show());
    t.change_a();
    println!("change_a: {}", t.show());
    t.change_b();
    println!("change_b: {}", t.show());
    t.change_c();
    println!("change_c: {}", t.show());
    t.change_d();
    println!("change_d: {}", t.show());
    t.change_e();
    println!("change_e: {}", t.show());
    t.change_f();
    println!("change_f: {}", t.show());
    t.change_g();
    println!("change_g: {}", t.show());
    t.change_h();
    println!("change_h: {}", t.show());
    t.change_i();
    println!("change_i: {}", t.show());
}

Ответы

7 trentcl Aug 19 2020 at 15:59

Полагаться на внутреннюю изменчивость для решения этой проблемы для меня звучит так, как в C ++, const_castзлоупотребление или злоупотребление mutableчленами только потому, что в другом месте приложения мы не были последовательны const(распространенная ошибка изучающих C ++).

Это вполне понятная мысль в контексте C ++. Причина неточности в том, что C ++ и Rust имеют разные концепции изменчивости.

В некотором смысле mutключевое слово Rust имеет два значения. В шаблоне это означает «изменяемый», а в ссылочном типе - «эксклюзивный». Разница между &selfи &mut selfзаключается не в том, selfможно ли мутировать или нет, а в том, можно ли использовать псевдоним .

В этом Messengerпримере, сначала давайте не будем относиться к этому слишком серьезно; он предназначен для иллюстрации особенностей языка, не обязательно для проектирования системы. Но мы можем представить себе , почему &selfмогут быть использованы: Messengerпредназначена для быть реализованы структуры, которые совместно , поэтому различные части кода может содержать ссылки на тот же объект и использовать его для sendоповещения без согласования друг с другом. Если sendбы взять &mut self, это было бы бесполезно для этой цели, потому что &mut selfединовременно может существовать только одна ссылка. Было бы невозможно отправлять сообщения в общий доступ Messenger(без добавления внешнего уровня внутренней изменчивости через Mutexили что-то в этом роде).

С другой стороны, каждая ссылка и указатель C ++ могут иметь псевдонимы. Таким образом, в терминах Rust вся изменчивость в C ++ - это «внутренняя» изменчивость! Rust не имеет эквивалента mutableв C ++, потому что Rust не имеет constчленов (крылатая фраза здесь: «изменчивость - это свойство привязки, а не тип»). Ржавчина делает имеет эквивалент const_cast, но только для сырых указателей, потому что это необоснованное превратить общую &ссылку на эксклюзивную &mutссылку. И наоборот, C ++ не имеет ничего подобного Cellили RefCellпотому что каждое значение уже неявно стоит за UnsafeCell.

Итак, вернемся к моему примеру кода ниже, я должен [...]

Это действительно зависит от предполагаемых семантики в Thing. Является ли это характером Thingсовместного использования, как конечная точка канала или файл? Имеет ли смысл change_eвызывать общую ссылку (с псевдонимом)? Если да, то используйте внутреннюю изменчивость, чтобы раскрыть метод &self. Это в Thingпервую очередь контейнер для данных? Имеет ли смысл делиться им иногда, а иногда эксклюзивно? Тогда, Thingвероятно, не следует использовать внутреннюю изменчивость и позволить пользователю библиотеки решать, как поступить с общей мутацией, если это необходимо.

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

  • В чем разница между размещением «mut» перед именем переменной и после «:»?
  • Почему изменчивость переменной не отражается в сигнатуре ее типа в Rust?
  • Требуется целостное объяснение типов ячеек и подсчета ссылок в Rust

¹ На самом деле, C ++ делает есть функция , которая делает указатели работают аналогично ссылки в Русте. Вид. restrictявляется нестандартным расширением C ++, но является частью C99. &Ссылки shared ( ) в Rust похожи на const *restrictуказатели, а &mutссылки на эксклюзивные ( ) похожи на не const *restrictуказатели. См. Что означает ключевое слово restrict в C ++?

Когда вы в последний раз сознательно использовали указатель restrict(или __restrictи т. Д.) В C ++? Не думайте об этом; ответ - «никогда». restrictобеспечивает более агрессивную оптимизацию, чем обычные указатели, но очень трудно использовать ее правильно, потому что вы должны быть очень осторожны с псевдонимами, а компилятор не предлагает никакой помощи. По сути, это массивное ножное ружье, и почти никто не использует его. Чтобы сделать целесообразным restrictповсеместное использование того же способа, который вы используете constв C ++, вам необходимо иметь возможность аннотировать функции, которым разрешено использовать указатели в качестве псевдонимов для других, в какое время, создать некоторые правила относительно того, когда указатели допустимы для выполнения, и иметь проход компилятора, который проверяет, соблюдаются ли правила в каждой функции. Вроде какая-то ... шашка.