СТАТЬЯ

Заимствование Rust на примере

Из книги Тима Макнамары Rust в действии

___________________________________________________________________

Сэкономьте 37% при покупке Rust в действии. Просто введите fccmcnamara в поле кода скидки при оформлении заказа на manning.com.
________________________________________________________________________________

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

«Внедрение» имитации наземной станции CubeSat

Учебным примером для статьи является созвездие CubeSat. CubeSats — это миниатюрные искусственные спутники, которые все чаще используются для повышения доступности космических исследований. Имея массу около 1,3 кг, их значительно дешевле отправить на орбиту, чем обычные спутники. Наземная станция является посредником между операторами и спутниками. Он слушает радио, проверяет состояние каждого спутника в созвездии и передает сообщения туда и обратно.

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

let sat_a = 0;  
let sat_b = 1;  
let sat_c = 2;

Чтобы проверить состояние каждого из наших спутников, мы будем использовать функцию-заглушку и enum для представления потенциальных сообщений об ошибках.

#[derive(Debug)]  
enum StatusMessage {
  Ok,               ❶  
}     
fn check_status(sat_id: u64) -> StatusMessage {    
  StatusMessage::Ok ❶  
}

❶ На данный момент все наши спутники CubeSats работают безотказно все время.

Функция check_status() чрезвычайно сложна в производственной системе. Однако для наших целей вполне достаточно каждый раз возвращать одно и то же значение. Собрав эти два фрагмента в целую программу, дважды «проверяющую» наши спутники, мы получим что-то вроде этого:

Листинг 1. Проверка состояния наших CubeSats на основе целых чисел (ch4/ch4-check-sats-1.rs)

#![allow(unused_variables)]
  
#[derive(Debug)]
enum StatusMessage {
  Ok,
}
  
fn check_status(sat_id: u64) -> StatusMessage {
  StatusMessage::Ok
}
  
fn main () {
  let sat_a = 0;
  let sat_b = 1;
  let sat_c = 2;
  
  let a_status = check_status(sat_a);
  let b_status = check_status(sat_b);
  let c_status = check_status(sat_c);
  println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);
  
  // "waiting" ...
  let a_status = check_status(sat_a);
  let b_status = check_status(sat_b);
  let c_status = check_status(sat_c);
  println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);
}

Выполнение листинга 1 должно пройти без происшествий. Код компилируется неохотно. Мы сталкиваемся со следующим выводом нашей программы:

Листинг 2. Вывод листинга 1

a: Ok, b: Ok, c: Ok  
a: Ok, b: Ok, c: Ok

Столкнемся с нашей первой проблемой в жизни

Давайте приблизимся к идиоматичному Rust, введя безопасность типов. Вместо целых чисел давайте создадим тип для моделирования наших спутников. Реальная реализация типа CubeSat, вероятно, будет включать в себя много информации о его местоположении, диапазоне радиочастот и многом другом. Мы будем придерживаться только записи идентификатора.

Листинг 3. Моделирование CubeSat как собственного типа

#[derive(Debug)]  
struct CubeSat { 
  id: u64;  
}

Теперь, когда у нас есть определение struct, давайте добавим его в наш код. Листинг 4 не будет компилироваться. Понимание деталей того, почему нет, является задачей большей части этой главы.

Листинг 4. Проверка состояния наших целочисленных CubeSats (ch4/ch4-check-sats-2.rs)

#[derive(Debug)]                                       ❶
struct CubeSat {
  id: u64,
}
  
#[derive(Debug)]
enum StatusMessage {
  Ok,
}
  
fn check_status(sat_id: CubeSat) -> StatusMessage {    ❷
  StatusMessage::Ok
}
  
fn main() {
  let sat_a = CubeSat { id: 0 };                       ❸
  let sat_b = CubeSat { id: 1 };                       ❸
  let sat_c = CubeSat { id: 2 };                       ❸
  
  let a_status = check_status(sat_a);
  let b_status = check_status(sat_b);
  let c_status = check_status(sat_c);
  println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);
  
  // "waiting" ...
  let a_status = check_status(sat_a);
  let b_status = check_status(sat_b);
  let c_status = check_status(sat_c);
 println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);
}

❶ Модификация 1: добавить определение

❷ Модификация 2: используйте новый тип в пределах check_status()

❸ Модификация 3. Создайте три новых экземпляра.

Когда вы попытаетесь скомпилировать код в листинге 4, вы получите сообщение, похожее на следующее (которое было отредактировано для краткости):

Листинг 5. Сообщение об ошибке, выдаваемое при попытке скомпилировать листинг 4

error[E0382]: use of moved value: `sat_a`
  --> code/ch4-check-sats-2.rs:26:31
   |
20 |   let a_status = check_status(sat_a);
   |                               ----- value moved here
...
26 |   let a_status = check_status(sat_a);
   |                               ^^^^^ value used here after move
   |
   = note: move occurs because `sat_a` has type `CubeSat`,
   = which does not implement the `Copy` trait
  
...
 
error: aborting due to 3 previous errors

Для опытных глаз сообщение компилятора полезно. Он сообщает нам, в чем именно заключается проблема, и дает рекомендации по ее устранению. Для менее опытных глаз это значительно менее полезно. Мы используем «перемещенное» значение, и нам настоятельно рекомендуется реализовать трейт Copy для CubeSat. Хм? Оказывается, хотя это и написано на английском языке, термин «перемещение» означает что-то конкретное в Rust. Физически ничего не движется.

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

Каждое значение в rust принадлежит. В листинге 1 и листинге 4 sat_a, sat_b и sat_c владеют данными, на которые они ссылаются. Когда выполняются вызовы check_status(), право собственности на данные переходит от переменных в области действия main() к переменной sat_id внутри функции. Существенное отличие состоит в том, что во втором примере это целое число помещается в структуру CubeSat. Это изменение типа изменяет семантику поведения программы.

Вот урезанная версия функции main() из листинга 4, в которой основное внимание уделяется sat_a и местам перехода прав собственности:

Листинг 6. Выдержка из ‹‹check-sats2› с акцентом на функцию main()

fn main() {    
  let sat_a = CubeSat { id: 0 };      ❶       
  let a_status = check_status(sat_a); ❷       
  let a_status = check_status(sat_a); ❸  
}

❶ Здесь возникает право собственности при создании объекта CubeSat.

❷ Право собственности на объект переходит к check_status(), но не возвращается к main()

❸ На данный момент sat_a больше не является владельцем объекта, что делает доступ недействительным.

На рис. 2 наглядно показаны взаимосвязанные процессы потока управления, владения и жизненного цикла. Во время вызова check_status(sat_a) право собственности переходит к функции check_status(). Когда check_status() возвращает StatusMessage, значение sat_a отбрасывается, и время жизни sat_a заканчивается. К сожалению для main(), sat_a остается в локальной области видимости. Вторая попытка доступа к sat_a во время второго вызова check_status() не удалась. Мы обсудим стратегии преодоления этого типа проблемы позже в статье.

Особое поведение примитивных типов

Прежде чем продолжить, было бы разумно объяснить, почему первый фрагмент кода, представленный в листинге 1, вообще функционировал. Дело в том, что примитивные типы в Rust ведут себя по-особому. Они реализуют Copy. Это означает, что находящиеся в собственности объекты дублируются при доступе в моменты времени, которые в противном случае были бы незаконными. Это обеспечивает некоторое удобство в повседневной жизни за счет несогласованности внутри языка. Формально считается, что примитивные типы обладают семантикой копирования, тогда как все остальные типы обладают семантикой перемещения. К несчастью для изучающих Rust, этот особый случай выглядит как случай по умолчанию, потому что они обычно сначала сталкиваются с примитивными типами.

Владение по своей сути является конструкцией времени компиляции. В обоих примерах кода данные, принадлежащие sat_a, sat_b и sat_c, имеют одинаковую физическую структуру: они представлены в памяти как целые числа (0, 1 и 2). Право собственности проверяется компилятором и не требует затрат времени выполнения.

Кто такой владелец? Есть ли у него какие-либо обязанности?

В мире Rust понятие владения довольно ограничено. В отличие от понятия собственности Локка, собственность не подразумевает контроля или суверенитета. На самом деле «владельцы» ценностей не имеют специального доступа к своим собственным данным и не имеют возможности ограничить вторжение других. Обязанность владельца - убирать.

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

Чтобы предоставить деструктор для типа, реализуйте Drop. Drop имеет один метод, drop(&mut self) который вы можете использовать для выполнения любых необходимых действий по завершению перед освобождением памяти.

Зачем использовать термин «владение», если переменная является референтным объектом, не являющимся его свойством? Этот термин используется уже несколько десятилетий, и его формулировка несколько устоялась. Это определенно подчеркивает, что есть единственное место, где всегда сохраняется ответственность.

Как меняется право собственности

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

Возвращаясь к исходному коду из листинга 4, мы видим, что sat_a начинает свою жизнь с владения объектом CubeSat.

fn main() {    
  let sat_a = CubeSat { id: 0 };    
  ...

Затем объект CubeSat передается в check_status() в качестве аргумента, передавая право собственности на локальную переменную sat_id.

fn main() {    
  let sat_a = CubeSat { id: 0 };    
  ...    
  let a_status = check_status(sat_a);    
  ...

Другая возможность могла заключаться в том, что sat_a отказывается от своего права собственности в пределах main() на другую переменную. Это будет выглядеть примерно так:

fn main() {    
  let sat_a = CubeSat { id: 0 };    
  ...    
  let new_sat_a = sat_a;    
  ...

Наконец, если бы произошло изменение в сигнатуре функции check_status(), она также могла бы передать право собственности на CubeSat переменной в вызывающей области. Вот наша исходная функция,

fn check_status(sat_id: CubeSat) -> StatusMessage {    
  StatusMessage::Ok  
}

а вот скорректированная функция, которая получает уведомление о сообщении через побочный эффект.

fn check_status(sat_id: CubeSat) -> CubeSat {    
  println!("{:?}: {:?}", sat_id, StatusMessage::Ok); ❶    
  sat_id                                             ❷  
}

❶ Используйте синтаксис форматирования Debug, так как наши типы используют #[derive(Debug)]

❷ Вернуть значение, опуская точку с запятой в конце последней строки.

При использовании в сочетании с новым main() можно увидеть принадлежность объектов CubeSat к их исходным переменным. Новый код

Листинг 7. Возврат права собственности на объекты их исходным переменным с помощью возвращаемых функций значений (ch4/ch4-check-sats-3.rs)

#![allow(unused_variables)]
  
#[derive(Debug)]
struct CubeSat {
  id: u64,
}
 
#[derive(Debug)]
enum StatusMessage {
  Ok,
}
  
fn check_status(sat_id: CubeSat) -> StatusMessage {
  println!("{:?}: {:?}", sat_id, StatusMessage::Ok);
  StatusMessage::Ok
}
  
fn main () {
  let sat_a = CubeSat { id: 0 };
  let sat_b = CubeSat { id: 1 };
  let sat_c = CubeSat { id: 2 };
  
  let sat_a = check_status(sat_a); ❶
  let sat_b = check_status(sat_b);
  let sat_c = check_status(sat_c);
  
  // "waiting" ...
  let sat_a = check_status(sat_a);
  let sat_b = check_status(sat_b);
  let sat_c = check_status(sat_c);
}

❶ Теперь, когда возвращаемое значение check_status() является исходным sat_a, новая привязка let "сбрасывается".

Вывод на консоль меняется, эта обязанность переложена на check_status(). Вывод новой функции main() выглядит следующим образом:

Листинг 8. Вывод листинга 7

CubeSat { id: 0 }: Ok  
CubeSat { id: 1 }: Ok  
CubeSat { id: 2 }: Ok  
CubeSat { id: 0 }: Ok  
CubeSat { id: 1 }: Ok 
CubeSat { id: 2 }: Ok

Визуальный обзор движения прав собственности в листинге 7 представлен ниже.

И это пока все. Если вы хотите узнать больше о книге, загляните на liveBook здесь и посмотрите эту презентацию.

Об авторе:
Тим Макнамара — опытный программист, глубоко интересующийся обработкой естественного языка, анализом текста и более широкими формами машинного обучения и искусственного интеллекта. Он очень активен в сообществах открытого исходного кода, включая Новозеландское общество открытого исходного кода.

Первоначально опубликовано на freecontent.manning.com.