Проблемный Rust или указатели на очищенную память

от
Прочие языки    rust

Текст и код статьи предоставлен на https://github.com/White-Oak/rust_articles

Когда впервые начинаешь программировать, или писать на языке с незнакомой парадигмой, или даже использовать неизученный фреймворк, то очень часто задаешься вопросом: "Как это сделать?".
Когда учишься программировать на Rust в голове чаще возникает вопрос: "Почему так нельзя сделать?".

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

Одной из самых частых и раздражающих ошибок новичка является cannot borrow `vec` as mutable because it is also borrowed as immutable , простейший пример ее приведу здесь:
  1. fn main(){
  2.     let mut vec = vec!["a", "b"]; // Создание динамического массива (списка, вектора)
  3.     // Цикл по всем элементам в массиве
  4.     for a in &vec {              
  5.         // ^Тут мы неизменяемо заимствуем vec
  6.         // (то есть, используем &vec -- не планируем изменять, используя эту ссылку)
  7.  
  8.         println!("{}", a);      // Простой вывод значения в консоль
  9.         // Ошибка! insert требует *изменяемого заимствования* (&mut vec)
  10.         vec.insert(0, "c");
  11.     }
  12. }
Регулятор заимствований в Rust требует соблюдений всего двух правил:
1. Любое заимствование не может пережить оригинальное значение
2. Можно заимствовать значение одним из двух способов, но не сразу двумя:
     1. Заимствовать неизменяемо (&vec). Таких ссылок (синоним заимствования) одновременно можно иметь сколько угодно.
     2. Заимствовать изменяемо (&mut vec). Лишь одну такую ссылку можно иметь в любой момент времени. То есть предыдущая ссылка должна быть уничтожена, прежде, чем мы снова сможем изменяемо заимствовать значение.
Такие, на первый взгляд, запутанные правила позволяют полностью избавиться от гонок данных (data race).

Настоящая проблема
Однако на днях я, лицом к лицу, встретился с проблемой, решения (или объяснения) которой, я пока не нашел.
Задача: написать функцию, которая принимает два хранилища и запрос, а возвращает указатель на значение, содержащееся в одном из этих хранилищ.

  1. ///Store1 -- хранилище первого типа. Возвращает Option<&String> (Some(&String) если есть значение и None, если его нет).
  2. ///Store2 -- хранилище второго типа, к нему мы обращаемся, если в хранилище первого типа не нашли нужную запись. Это хранилише возвращает одно из двух значений: Store2Value::String и Store2Value::U32 (строку и число, соотвественно)
  3. ///На 'a в определении функции предлагаю пока внимания не обращать, это довольно сложная тема, а тут они особого значения не имеют
  4. fn resolve_call<'a>(store1: &'a Store1, store2: &'a Store2, call: &str) -> &'a String {
  5.     if let Some(s) = store1.get(call){
  6.         //Если хранилище первого типа содержит значение, то возвращаем его
  7.         s
  8.     } else {
  9.         match store2.get(call){
  10.             &Store2Value::String(ref s) => s,
  11.             //Следующая строка не скомпилируется: borrowed value does not live long enough
  12.             &Store2Value::U32(ref u) => &u.to_string()
  13.         }
  14.     }
  15. }

Разберем ошибку. Сама ошибка означает, что мы нарушили первое из двух (см. выше) правил заимствования в Rust. А именно: попытались вернуть из функции ссылку на значение, которое умирает при возврате из функции.
Тут надо разъяснить, что все переменные (данные) могут храниться либо на стеке, либо в куче (heap). И те данные, которые хранятся непосредственно в стеке при выходе из своей области видимости (scope) -- высвобождаются. Поэтому Rust расценивает наш код, как попытку передать ссылку на несуществующие данные в памяти.

  1. &Store2Value::U32(ref u) => { // Получаем ссылку на число (&u32)
  2.     let string = u.to_string(); // Создаем новое строковое значение, содержащее представление числа (String)
  3.     // Важно понять, что это значение действительно только для текущей области видимости
  4.     let ref_string = &u; // Заимствуем значение (&String)
  5.     return ref_string;  // Возвращаем заимствование и выходим из области видимости
  6. }
  7. // ^ Выйдя из этой области видимости, теряем string, а ref_string начинает ссылаться на несуществующее значение -- ошибка

Иными словами проблема состоит в том, что просто так свежесозданное значение из функции не вернуть.

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

Дисклеймер

Способ 01: Только копирование данных, только медленный код!
Напрашивающееся решение: забить на ссылки и возвращать сами данные. Так как выдрать данные из хранилищ мы не можем, то эти данные нам придется склонировать

Копирование? Клонирование?

  1. ///Параметры остаются такими же
  2. fn resolve_call(store1: &Store1, store2: &Store2, call: &str) -> String {
  3.     if let Some(s) = store1.get(call){
  4.         s.clone() // Клонирование
  5.     } else {
  6.         match store2.get(call){
  7.             &Store2Value::String(ref s) => s.clone(), // Клонирование
  8.             &Store2Value::U32(ref u) => u.to_string() // Свежее значение, незачем его клонировать
  9.         }
  10.     }
  11. }

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

Способ 02: Абстракции лепятся, коровы мычат
В Rust есть специальный тип Cow, который расшифровывается как copy-on-write.
В общем, это такой тип, который может содержать и значение, и ссылку на него (не одновременно), причем оба варианта будут одинаково работоспособны, благодаря тому, что он реализует трейт Deref:
  1. fn main(){
  2.     {
  3.         let vec = vec![1, 2, 3];
  4.         let cow = Cow::Owned(vec); // Обертываем само значение vec
  5.         // Следующая строка работает благодаря тому, что &cow == &vec (из-за реализации трейта Deref)
  6.         let result = cow.iter().fold(0, |acc, &item| acc + item);
  7.         assert_eq!(result, 6);
  8.     }
  9.     {
  10.         let vec = vec![1, 2, 3];
  11.         let cow = Cow::Borrowed(&vec); // Заимствуем &vec
  12.         let result = cow.iter().fold(0, |acc, &item| acc + item);
  13.         assert_eq!(result, 6);
  14.     }
  15. }
Еще о Deref

А название такое, потому что при попытке изменения начальное значение склонируется, если в Cow содержится ссылка, а не значение.

Наша функция:
  1. use std::borrow::Cow;
  2. fn resolve_call_02<'a>(store1: &'a Store1, store2: &'a Store2, call: &str) -> Cow<'a, String> {
  3.     if let Some(s) = store1.get(call){
  4.         Cow::Borrowed(s)
  5.     } else {
  6.         match store2.get(call){
  7.             &Store2Value::String(ref s) => Cow::Borrowed(s),
  8.             &Store2Value::U32(ref u) => Cow::Owned(u.to_string())
  9.         }
  10.     }
  11. }
Несмотря на то, что такой вариант тоже выделяет память -- он ее выделяет фиксировано, независимо от размера возвращаемых данных.

Способ 03: Следуя гайдлайнам
Официальный FAQ (обновленный, кстати, дня три назад) предлагает правильное решение, но, по-моему, не предлагает более корректного примера, чем указанный. Однако такое решение мы тоже разберем.

По ссылке предлагается каким-либо образом ассоциировать жизненое время возвращаемого значения с одним из входных параметров.

Ликбез по временам жизни

FAQ предлагает использование TypedArena, который позволяет выделить объекты в памяти (или выделить место под много объектов сразу).

  1. use arena::TypedArena;
  2. type Pool = TypedArena<String>; // Ассоциируем тип аналогично typedef в Си
  3. ///Заметьте, что в параметрах появился pool -- через него мы будем перемещать наше многострадальное значение из стека в память и возвращать на него указатель.
  4. ///Функция pool.alloc помещает значение в память и возвращает указатель на него.
  5. fn resolve_call<'a>(store1: &'a Store1, store2: &'a Store2, call: &str, pool: &'a Pool) -> &'a String { //'
  6.     if let Some(s) = store1.get(call){
  7.         s
  8.     } else {
  9.         match store2.get(call){
  10.             &Store2Value::String(ref s) => s,
  11.             &Store2Value::U32(ref u) => pool.alloc(u.to_string()) // Связываем значения
  12.         }
  13.     }
  14. }
Не считая того, что мы добавляем еще один параметр в функцию, выглядит довольно симпатично и быстро.

Способ 04: Следите за руками
Читерский способ:
  1. ///Обращаю внимание на то, что store1 заимствуется изменяемо!
  2. fn resolve_call<'a>(store1: &'a mut Store1, store2: &'a Store2, call: &str) -> &'a String {
  3.     if store1.contains(call){
  4.         store1.get(call).unwrap()
  5.     } else {
  6.         match store2.get(call){
  7.             &Store2Value::String(ref s) => s,
  8.             &Store2Value::U32(ref u) => {
  9.                 //Вставляем значение в хранилище, этим самым, связывая эти два значения
  10.                 store1.insert(call, u.to_string());
  11.                 //... И возвращаем на него указатель
  12.                 store1.get(call).unwrap()
  13.             }
  14.         }
  15.     }
  16. }
Идеальный вариант, если нам позволено менять изначальное хранилище. Но его специфика не позволила мне применить этот способ.

Заключение
У меня больше нет идей! Я сам использую способ 02, потому что метод вызывается очень часто и лишних выделений памяти мне хотелось бы избежать.
Возможно, есть еще способы, до которых я не смог дойти своим умом, также использовав официальные источники.

Такая вот первая проблема, которой я не смог найти элегантного решения.
  • +5
  • views 4726