Как интерпретировать неизменяемые ссылки на изменяемые типы в Rust?

Кажется, я не могу ничего изменить, если в моей цепочке разыменования есть какая-либо неизменная ссылка. Образец:

fn main() {
    let mut x = 42;
    let y: &mut i32 = &mut x; // first layer
    let z: &&mut i32 = &y; // second layer
    **z = 100; // Attempt to change `x`, gives compiler error.

    println!("Value is: {}", z);
}

Я получаю ошибку компилятора:

error[E0594]: cannot assign to `**z` which is behind a `&` reference
 --> src/main.rs:5:5
  |
4 |     let z: &&mut i32 = &y; // second layer
  |                        -- help: consider changing this to be a mutable reference: `&mut y`
5 |     **z = 100; // Attempt to change `x`, gives compiler error.
  |     ^^^^^^^^^ `z` is a `&` reference, so the data it refers to cannot be written

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

Однако при взгляде на типы семантика кажется нелогичной:

  • Переменная y имеет тип &mut i32, или, говоря простым языком, "изменяемая ссылка на целое число".
  • Переменная z имеет тип &&mut i32, или, говоря простым языком, "неизменяемая ссылка на изменяемую ссылку на целое число".
  • Путем разыменования z один раз (т.е. *z) я получу что-то типа &mut i32, то есть что-то того же типа, что и y. Однако разыменование этого снова (т.е. **z) дает мне что-то типа i32, но мне не разрешено изменять это целое число.

По сути, типы ссылок в некотором смысле лгут мне, поскольку на самом деле они не делают того, что, как они утверждают, делают. Как мне правильно читать типы ссылок в этом случае, или как еще я могу восстановить веру в эту концепцию?

Тестирование с этим образцом:

fn main() {
    let mut x = 42;
    let y: &mut i32 = &mut x; // first layer
    let m: &&mut i32 = &y; // second layer
    let z: &&&mut i32 = &m; // third layer
    compiler_builtin_deref_first_layer(*z);
}

fn compiler_builtin_deref_first_layer(v: &&mut i32) {
    compiler_builtin_deref_second_layer(*v);
}

fn compiler_builtin_deref_second_layer(w: &mut i32) {
    println!("Value is: {}", w);
}

Типы параметров этих двух последних функций верны. Если я изменю что-либо из этого, компилятор будет жаловаться на несоответствие типов. Однако, если я скомпилирую пример как есть, я получу эту ошибку:

error[E0596]: cannot borrow `**v` as mutable, as it is behind a `&` reference

Почему-то звонок compiler_builtin_deref_first_layer кажется нормальным, а звонок compiler_builtin_deref_second_layer - нет. Ошибка компилятора говорит о **v, но я вижу только *v.


person domin    schedule 06.05.2019    source источник
comment
help: подумайте о том, чтобы сделать ссылку изменяемой: &mut y   -  person hellow    schedule 06.05.2019
comment
Да, это устранит ошибку, но у меня проблема не в этом. Я хочу понять обещания, которые дают типы.   -  person domin    schedule 06.05.2019
comment
Разыменовав z один раз (т.е. *z), я получу что-то типа &mut i32 Нет, это будет изменяемый deref, чего нельзя сделать с неизменяемой ссылкой. В лучшем случае вы можете получить &i32 оттуда.   -  person E_net4 the curator    schedule 06.05.2019
comment
@ E_net4: Это интересно ... Так есть несколько типов дерефов? Итак, если у меня есть &&mut i32 и я выполняю на нем неизменяемый deref, какой тип я получу? &i32? Каким будет правило вывода этого?   -  person domin    schedule 06.05.2019
comment
Это будут Deref и _ 2_. Документация по каждому признаку объясняет, в каком контексте используется.   -  person E_net4 the curator    schedule 06.05.2019
comment
Ах, как приятно это знать! Подхожу ближе ... Однако я не знаю, как компилятор реализует трейт для встроенных ссылок. Итак, возвращаясь к моему вопросу, подпись fn deref(&self) -> &Self::Target, как мне создать экземпляр этого с моим &&mut i32? Является ли цель i32 и, следовательно, я верну &i32? Как так получилось, что целью &&mut i32 является i32? Разве это не пропуск уровня?   -  person domin    schedule 06.05.2019
comment
На это отвечает этот вопрос.   -  person E_net4 the curator    schedule 06.05.2019
comment
@domin Нет, тип <&&mut i32 as Deref>::Target - &mut i32, как вы можете видеть из документации, указанной выше. Просмотр определений признака Deref здесь не очень помогает, поскольку соответствующий шаг встроен в компилятор, а не в стандартную библиотеку.   -  person Sven Marnach    schedule 06.05.2019
comment
Черт возьми, это непростая задача для новичка. ;) Итак, какова в данном случае семантика встроенного компилятора-deref? Во что &&mut i32 преобразуется после выполнения одного встроенного в компилятор deref?   -  person domin    schedule 06.05.2019
comment
Иногда полезно подумать о эксклюзивных и общих ссылках, а не о изменяемых и неизменяемых ссылках. Итак, y - это эксклюзивная ссылка на целое число, а z - это общая ссылка на эксклюзивную ссылку на целое число, но вы не можете разыменовать z, чтобы получить y-подобный эксклюзивный доступ к целому числу, потому что это нарушает собственный договор z о совместном использовании.   -  person trentcl    schedule 06.05.2019
comment
@trentcl Отличный ответ, спасибо! Это означает, что я вроде как должен рассматривать несколько шагов deref как одну операцию, так как очевидно, что я не могу просто структурно разложить такой тип, как &&mut i32 шаг за шагом.   -  person domin    schedule 06.05.2019
comment
@domin Вы можете рассматривать несколько шагов разыменования как отдельные операции. Тип *z действительно &mut i32. Код let foo: &mut i32 = *z; отлично пройдет проверку типов, но программа проверки заимствований будет жаловаться на то, что вы создаете изменяемое заимствование для чего-то, что находится за общей ссылкой. Вам не нужно разбираться в деталях, как работает программа проверки заимствований; достаточно понимать, что он гарантирует, что ничто, стоящее за общей ссылкой, не может быть изменено и что изменяемые ссылки не могут быть псевдонимами.   -  person Sven Marnach    schedule 07.05.2019
comment
@SvenMarnach Это еще одно своеобразное поведение: в let foo: &mut i32 = *z; я ожидал, что это будет равно let foo = *z;, поскольку оно просто делает тип явным. Однако последний вызывает другую ошибку компилятора (невозможно выйти из заимствования). Таким образом, указание типа здесь, похоже, действительно делает следующее: let foo = &mut **z;. В любом случае: я думаю, что ключевым моментом является то, что средство проверки заимствований не выводит свои правила исключительно из типов, но у него есть дополнительная информация, над которой можно поработать.   -  person domin    schedule 07.05.2019
comment
@domin Как я уже говорил, программа проверки заимствований - сложное чудовище, но нет необходимости понимать, как она работает. Вы можете доверять ему поддерживать инварианты, которые он должен поддерживать. И хорошее наблюдение о различных ошибках - добавление типа здесь явно меняет семантику. Без типа привязка let перемещает изменяемую ссылку, а с явной аннотацией типа привязка создает неявный повторный заимствование. Это происходит только для изменяемых ссылок и является довольно тонкой разницей.   -  person Sven Marnach    schedule 07.05.2019


Ответы (1)


По сути, типы ссылок в некотором смысле лгут мне, поскольку на самом деле они не делают того, что, как они утверждают, делают. Как мне правильно читать типы ссылок в этом случае, или как еще я могу восстановить веру в эту концепцию?

Правильный способ чтения ссылок в Rust - это разрешения.

Право собственности на объект, когда он не заимствован, дает вам право делать с объектом все, что вы хотите; создать его, уничтожить, переместить из одного места в другое. Вы владелец, вы можете делать то, что хотите, вы контролируете жизнь этого объекта.

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

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

И изменяемые (или исключительные) ссылки, и неизменяемые (или общие) ссылки могут быть сделаны на принадлежащие объектам, но это не означает, что вы владеете объектом, когда вы обращаетесь к нему через ссылку. То, что вы можете делать с объектом, зависит от того, по какой ссылке вы к нему обращаетесь.

Так что не думайте о &&mut T ссылке как о «неизменяемой ссылке на изменяемую ссылку на T», а затем думайте «ну, я не могу изменить внешнюю ссылку, но я должен иметь возможность изменить внутреннюю ссылку».

Вместо этого подумайте об этом так: «Кто-то владеет T. Они предоставили эксклюзивный доступ, поэтому прямо сейчас есть кто-то, кто имеет право изменять T. Но пока что этот человек предоставил общий доступ к &mut T, Это означает, что они обещали не изменять его в течение определенного периода времени, и все пользователи могут использовать общую ссылку на &mut T, включая разыменование базового T, но только для вещей, которые вы обычно можете делать с общей ссылкой, которая означает чтение, но не письмо ".

Последнее, что нужно иметь в виду, это то, что изменяемая или неизменяемая часть на самом деле не является фундаментальной разницей между ссылками. Это действительно эксклюзивная и общая часть. В Rust вы можете изменить что-либо через общую ссылку, если есть какой-то механизм внутренней защиты, который гарантирует, что только один человек делает это одновременно. Есть несколько способов сделать это, например Cell, RefCell или Mutex.

Итак, то, что предоставляют &T и &mut T, на самом деле не является неизменяемым или изменяемым доступом, хотя они названы так, потому что это уровень доступа по умолчанию, который они предоставляют на уровне языка при отсутствии каких-либо функций библиотеки. Но на самом деле они предоставляют общий или эксклюзивный доступ, а затем методы для типов данных могут предоставлять вызывающим абонентам различные функции в зависимости от того, принимают ли они собственное значение, эксклюзивную ссылку или общую ссылку.

Так что думайте о ссылках как о разрешениях; и именно ссылка, с помощью которой вы достигаете чего-либо, определяет, что вам разрешено с этим делать. А когда у вас есть право собственности или эксклюзивная ссылка, выдача эксклюзивной или общей ссылки временно препятствует изменяемому доступу к объекту, пока эти заимствованные ссылки все еще живы.

person Brian Campbell    schedule 08.05.2019
comment
Большое спасибо за подробный ответ! Таким образом, несколько составных ссылок по-прежнему следует читать как ссылки, указывающие - прямо или косвенно - на одно и то же базовое собственное значение (T)? Если это так, то почему я могу взять let foo = &mut &mut x; (где x : T), а затем разыменовать его на полпути и изменить среднюю ссылку, например: *foo = &mut y;, где y - другое значение типа T? Эта операция не будет иметь ничего общего с исходным T, поэтому ссылка должна быть объектом (с владельцем) сама по себе ... - person domin; 08.05.2019
comment
Да, ссылка - это объект, у которого есть собственник. Но разрешение, которое вы получаете, зависит от пути, по которому вы его достигнете. Если вы достигнете &mut ссылки через & ссылку, у вас будет общий доступ только для чтения к самой ссылке и ко всему, на что она ссылается. - person Brian Campbell; 08.05.2019
comment
Я, вероятно, должен уточнить: несколько составных ссылок не читаются просто как ссылки, указывающие на базовое значение. Они указывают на то, на что указывают. Но разрешение, которое вы получаете, - это минимум разрешений, предоставляемых каждой ссылкой на пути, по которому вы ее достигли. Отредактирую свой ответ, чтобы он был более ясным. - person Brian Campbell; 08.05.2019
comment
Замечательно, спасибо за разъяснения. Я просто немного поэкспериментировал с этими знаниями и придумал этот образец: play.rust-lang.org/ Вы согласны с моими комментариями? - person domin; 08.05.2019
comment
@domin Да, верно. Средство проверки заимствований работает статически, и хотя можно было бы статически проанализировать этот конкретный фрагмент кода, если бы вы вместо этого передали их в функцию, он не смог бы определить, на какую ссылку foo ссылаются после того, как функция вернула (play.rust-lang.org/). Таким образом, компилятор применяет консервативные правила; он определяет, что foo может ссылаться на x или y, поэтому, пока foo жив, вы не можете делать другие ссылки на x или y. - person Brian Campbell; 09.05.2019
comment
Верно. Итак, типичный анализ объединения множеств! Спасибо большое за вашу помощь! - person domin; 09.05.2019