Язык программирования Rust
Скачать 7.02 Mb.
|
Способы взаимодействия переменных и данных: перемещение Несколько переменных могут по-разному взаимодействовать с одними и теми же данными в Rust. Давайте рассмотрим пример использования целого числа в листинге 4- 2. Листинг 4-2: присвоение целочисленного значения переменной x к переменной y Мы можем догадаться, что делает этот код: «привязать значение 5 к x ; затем сделать копию значения в x и привязать его к y ». Теперь у нас есть две переменные: x и y , и обе равны 5 . Это то, что происходит на самом деле, потому что целые числа — это простые значения с известным фиксированным размером, и эти два значения 5 помещаются в стек. Теперь рассмотрим версию с типом String : Это выглядит очень похоже, поэтому мы можем предположить, что происходит то же самое: вторая строка сделает копию значения в s1 и привяжет его к s2 . Но это не совсем так. Взгляните на рисунок 4-1, чтобы увидеть, что происходит со String под капотом. String состоит из трёх частей, показанных слева: указатель на память, в которой хранится содержимое строки, длина и ёмкость. Эта группа данных хранится в стеке. Справа — память в куче, которая хранит содержимое. let x = 5 ; let y = x; let s1 = String ::from( "hello" ); let s2 = s1; s1 name value ptr len 5 capacity 5 index value 0 h 1 e 2 l 3 l 4 o Рисунок 4-1: представление в памяти String , содержащей значение "hello" , привязанное к s1 Длина — это количество байт памяти, которое использует содержимое String в данный момент. Ёмкость — это общее количество байт памяти, которое String получил от операционной системы. Разница между длиной и ёмкостью имеет значение, но не в данном контексте — сейчас можно игнорировать ёмкость. Когда мы присваиваем s1 значение s2 , данные String копируются, то есть мы копируем указатель, длину и ёмкость, которые находятся в стеке. Мы не копируем данные в куче, на которую указывает указатель. Другими словами, представление данных в памяти выглядит так, как показано на рисунке 4-2. s1 name value ptr len 5 capacity 5 index value 0 h 1 e 2 l 3 l 4 o s2 name value ptr len 5 capacity 5 Рисунок 4-2: представление в памяти переменной s2 , имеющей копию указателя, длины и ёмкости s1 Представление не похоже на рисунок 4-3, как выглядела бы память, если бы вместо этого Rust также скопировал данные кучи. Если бы Rust сделал это, операция s2 = s1 могла бы быть очень дорогой с точки зрения производительности во время выполнения, если бы данные в куче были большими. s2 name value ptr len 5 capacity 5 index value 0 h 1 e 2 l 3 l 4 o s1 name value ptr len 5 capacity 5 index value 0 h 1 e 2 l 3 l 4 o Рисунок 4-3: другой вариант того, что может сделать s2 = s1 , если Rust также скопирует данные кучи Ранее мы сказали, что когда переменная выходит из области видимости, Rust автоматически вызывает функцию drop и очищает память в куче для данной переменной. Но картинка 4-2 показывает, что теперь оба указателя указывают на одно и тоже место. Это проблема: когда переменная s2 и переменная s1 выходят из области видимости, они обе будут пытаться освободить одну и ту же память в куче. Это известно как «ошибка двойного освобождения» (double free), и является одной из ошибок безопасности памяти, упоминаемых ранее. Освобождение памяти дважды может привести к повреждению памяти, что потенциально может привести к уязвимостям безопасности. Чтобы обеспечить безопасность памяти, после строки let s2 = s1 Rust считает s1 более недействительным. Следовательно, Rust не нужно ничего освобождать, когда s1 выходит за пределы области видимости. Посмотрите, что происходит, когда вы пытаетесь использовать s1 после создания s2 ; это не сработает: Вы получите похожую ошибку, потому что Rust не даст использовать недействительную ссылку s1 : let s1 = String ::from( "hello" ); let s2 = s1; println! ( "{}, world!" , s1); Если вы слышали термины поверхностное копирование и глубокое копирование при работе с другими языками, концепция копирования указателя, длины и ёмкости без копирования данных, вероятно, звучит как создание поверхностной копии. Но поскольку Rust также аннулирует первую переменную, вместо того, чтобы называть её поверхностной копией, это называется перемещением. В этом примере мы бы сказали, что s1 был перемещён в s2 . Что происходит на самом деле, показано на рисунке 4-4. s1 name value ptr len 5 capacity 5 index value 0 h 1 e 2 l 3 l 4 o s2 name value ptr len 5 capacity 5 Рисунок 4-4: представление в памяти после того, как s1 был признан недействительным Это решает нашу проблему! Действительной остаётся только переменная s2 . Когда она выходит из области видимости, то она одна будет освобождать память в куче. $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0382]: borrow of moved value: `s1` --> src/main.rs:5:28 | 2 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait 3 | let s2 = s1; | -- value moved here 4 | 5 | println!("{}, world!", s1); | ^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0382`. error: could not compile `ownership` due to previous error Дополнительно присутствует дизайнерский выбор, который подразумевает следующее: Rust никогда не будет автоматически создавать «глубокие» копии ваших данных. Следовательно любое такое автоматическое копирование можно считать недорогим с точки зрения производительности во время выполнения. Способы взаимодействия переменных и данных: клонирование Если мы хотим глубоко скопировать данные кучи String , а не только данные стека, мы можем использовать общий метод, называемый clone . Мы обсудим синтаксис методов в главе 5, но поскольку методы являются общей чертой многих языков программирования, вы, вероятно, уже встречались с ними. Вот пример работы метода clone : Код работает отлично и явно выполняет поведение, показанное на картинке 4-3, где данные в куче действительно скопированы. Когда вы видите вызов clone , вы знаете о выполнении некоторого кода, который может быть дорогим. В то же время использование clone является визуальным индикатором того, что тут происходит что-то нестандартное (глубокое копирование вместо обыденного перемещения). Стековые данные: копирование Это ещё одна особенность о которой мы ранее не говорили. Этот код, часть которого была показа ранее в листинге 4-2, использует целые числа. Он работает без ошибок: Но это, кажется, противоречит тому, что мы только что изучили: тут не нужно вызывать clone , но x является все ещё действительной переменной и не перемещена в y Причина в том, что такие типы, как целые числа, размер которых известен во время компиляции, полностью хранятся в стеке, поэтому копии фактических значений создаются быстро. Это означает, что нет причин, по которым мы хотели бы предотвратить допустимость x после того, как создадим переменную y . Другими словами, здесь нет разницы между глубоким и поверхностным копированием, поэтому вызов clone ничем не отличается от обычного поверхностного копирования, и мы можем его опустить. let s1 = String ::from( "hello" ); let s2 = s1.clone(); println! ( "s1 = {}, s2 = {}" , s1, s2); let x = 5 ; let y = x; println! ( "x = {}, y = {}" , x, y); В Rust есть специальная аннотация, называемая типажом Copy , которую мы можем размещать на типах, хранящихся в стеке, как и целые числа (подробнее о типах мы поговорим в главе 10 ). Если тип реализует типаж Copy , переменные, которые его используют, не перемещаются, а тривиально копируются, что делает их действительными после присвоения другой переменной. Rust не позволит нам аннотировать тип с помощью Copy , если тип или любая из его частей реализует Drop . Если для типа нужно, чтобы произошло что-то особенное, когда значение выходит за пределы области видимости, и мы добавляем аннотацию Copy к этому типу, мы получим ошибку времени компиляции. Чтобы узнать, как добавить аннотацию Copy к вашему типу для реализации типажа, смотрите раздел «Производные типажи» в приложении С. Но какие же типы имеют типаж Copy ? Можно проверить документацию любого типа для уверенности, но как правило любая группа простых скалярных значений может быть с типажом Copy , и ничего из типов, которые требуют выделения памяти в куче или являются некоторой формой ресурсов, не имеет типажа Copy . Вот некоторые типы, которые реализуют типаж Copy : Все целочисленные типы, такие как u32 , Логический тип данных bool , возможные значения которого true и false , Все числа с плавающей запятой, такие как f64 , Символьный тип char , Кортежи, но только если они содержат типы, которые также реализуют Copy Например, (i32, i32) будет с Copy , но кортеж (i32, String) уже нет. Владение и функции Механика передачи значения функции аналогична тому, что происходит при присвоении значения переменной. Передача переменной в функцию приведёт к перемещению или копированию, как и присваивание. В листинге 4-3 есть пример с некоторыми аннотациями, показывающими, где переменные входят в область видимости и выходят из неё. Файл: src/main.rs Листинг 4-3: функции с аннотированными владением и областью видимости Если бы мы попытались использовать s после вызова takes_ownership , Rust выдал бы ошибку во время компиляции. Эти статические проверки защищают нас от ошибок. Попробуйте добавить в main код, который использует s и x , чтобы увидеть, где вы можете их использовать, а где правила владения не позволяют вам это сделать. Возвращение значений и область видимости Возвращаемые значения также могут передавать право владения. В листинге 4-4 показан пример функции, возвращающей некоторое значение, с такими же аннотациями, как в листинге 4-3. Файл: src/main.rs fn main () { let s = String ::from( "hello" ); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5 ; // x comes into scope makes_copy(x); // x would move into the function, // but i32 is Copy, so it's okay to still // use x afterward } // Here, x goes out of scope, then s. But because s's value was moved, nothing // special happens. fn takes_ownership (some_string: String ) { // some_string comes into scope println! ( "{}" , some_string); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy (some_integer: i32 ) { // some_integer comes into scope println! ( "{}" , some_integer); } // Here, some_integer goes out of scope. Nothing special happens. Листинг 4-4: передача права владения на возвращаемые значения Владение переменной каждый раз следует одному и тому же шаблону: присваивание значения другой переменной перемещает его. Когда переменная, содержащая данные в куче, выходит из области видимости, содержимое в куче будет очищено функцией drop , если только данные не были перемещены во владение другой переменной. Хотя это работает, получение права владения, а затем возвращение владения каждой функцией немного утомительно. Что, если мы хотим, чтобы функция использовала значение, но не становилась владельцем? Очень раздражает, что всё, что мы передаём, также должно быть передано обратно, если мы хотим использовать это снова, в дополнение к любым данным, полученным из тела функции, которые мы также можем захотеть вернуть. Rust позволяет нам возвращать несколько значений с помощью кортежа, как показано в листинге 4-5. Файл: src/main.rs fn main () { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String ::from( "hello" ); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership () -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String ::from( "yours" ); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns one fn takes_and_gives_back (a_string: String ) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function } Листинг 4-5: возврат права владения на параметры Но это слишком высокопарно и многословно для концепции, которая должна быть общей. К счастью для нас, в Rust есть возможность использовать значение без передачи права владения, называемая ссылками. fn main () { let s1 = String ::from( "hello" ); let (s2, len) = calculate_length(s1); println! ( "The length of '{}' is {}." , s2, len); } fn calculate_length (s: String ) -> ( String , usize ) { let length = s.len(); // len() returns the length of a String (s, length) } Ссылочные переменные и заимствование Проблема с кодом кортежа в листинге 4-5 заключается в том, что мы должны вернуть String из вызванной функции, чтобы использовать String после вызова calculate_length , потому что String была перемещена в calculate_length . Вместо этого мы можем предоставить ссылку на значение String . Ссылка похожа на указатель в том смысле, что это адрес, по которому мы можем проследовать, чтобы получить доступ к данным, хранящимся по этому адресу; эти данные принадлежат какой-то другой переменной. В отличие от указателя, ссылка гарантированно указывает на допустимое значение определённого типа в течение всего срока существования этой ссылки. Вот как вы могли бы определить и использовать функцию calculate_length , имеющую ссылку на объект в качестве параметра, вместо того, чтобы брать на себя ответственность за значение: Файл: src/main.rs Во-первых, обратите внимание, что весь код кортежа в объявлении переменной и возвращаемое значение функции исчезли. Во-вторых, обратите внимание, что мы передаём &s1 в calculate_length и в его определении берём &String а не String . Эти амперсанды представляют собой ссылки, и они позволяют вам ссылаться на некоторое значение, не принимая владение им. Рисунок 4-5 изображает эту концепцию. fn main () { let s1 = String ::from( "hello" ); let len = calculate_length(&s1); println! ( "The length of '{}' is {}." , s1, len); } fn calculate_length (s: & String ) -> usize { s.len() } s name value ptr s1 name value ptr len 5 capacity 5 index value 0 h 1 e 2 l 3 l 4 o Рисунок 4-5: диаграмма для &String s , указывающей на String s1 Примечание: противоположностью ссылки с использованием & является разыменование, выполняемое с помощью оператора разыменования * . Мы увидим некоторые варианты использования оператора разыменования в главе 8 и обсудим детали разыменования в главе 15. Давайте подробнее рассмотрим механизм вызова функции: &s1 позволяет нам создать ссылку, которая ссылается на значение s1 , но не владеет им. Поскольку она не владеет им, значение, на которое она указывает, не будет удалено, когда ссылка перестанет использоваться. Точно так же сигнатура функции использует & для указания на то, что тип параметра s является ссылкой. Добавим несколько поясняющих аннотаций: Область действия s такая же, как и область действия любого параметра функции, но значение, на которое указывает ссылка, не удаляется, когда s перестаёт использоваться, потому что s не является его владельцем. Когда функции имеют ссылки в качестве параметров вместо фактических значений, нам не нужно возвращать значения, чтобы вернуть право владения, потому что мы никогда не владели ими. let s1 = String ::from( "hello" ); let len = calculate_length(&s1); fn calculate_length (s: & String ) -> usize { // s is a reference to a String s.len() } // Here, s goes out of scope. But because it does not have ownership of what // it refers to, it is not dropped. Мы называем процесс создания ссылки заимствованием. Как и в реальной жизни, если человек чем-то владеет, вы можете это у него позаимствовать. Когда вы закончите, вы должны вернуть это законному владельцу. Так что же произойдёт, если мы попытаемся изменить что-то, что мы заимствуем? Попробуйте запустить код из листинга 4-6. Спойлер: это не сработает! Файл: src/main.rs Листинг 4-6: попытка модификации заимствованной переменной Вот ошибка: Как переменные неизменяемы по умолчанию, так и ссылки. Нам не разрешено изменять то, на что у нас есть ссылка. Изменяемые ссылочные переменные Мы можем исправить код из листинга 4-6, чтобы позволить себе изменять заимствованное значение, с помощью нескольких небольших настроек, которые используют изменяемую ссылку: Файл: src/main.rs fn main () { let s = String ::from( "hello" ); change(&s); } fn change (some_string: & String ) { some_string.push_str( ", world" ); } $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference --> src/main.rs:8:5 | 7 | fn change(some_string: &String) { | ------- help: consider changing this to be a mutable reference: `&mut String` 8 | some_string.push_str(", world"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `ownership` due to previous error Сначала мы меняем s на mut . Затем мы создаём изменяемую ссылку с помощью &mut s , у которой вызываем change и обновляем сигнатуру функции, чтобы принять изменяемую ссылку с помощью some_string: &mut String . Это даёт понять, что change изменит значение, которое заимствует. Изменяемые ссылки имеют одно большое ограничение: если у вас есть изменяемая ссылка на значение, у вас не может быть других ссылок на это значение. Код, который пытается создать две изменяемые ссылки на s , завершится ошибкой: Файл: src/main.rs Описание ошибки: Эта ошибка говорит о том, что код недействителен, потому что мы не можем заимствовать s как изменяемые более одного раза в один момент. Первое изменяемое заимствование находится в r1 и должно длиться до тех пор, пока оно не будет использовано в println! , но между созданием этой изменяемой ссылки и её fn main () { let mut s = String ::from( "hello" ); change(& mut s); } fn change (some_string: & mut String ) { some_string.push_str( ", world" ); } let mut s = String ::from( "hello" ); let r1 = & mut s; let r2 = & mut s; println! ( "{}, {}" , r1, r2); $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:5:14 | 4 | let r1 = &mut s; | ------ first mutable borrow occurs here 5 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here 6 | 7 | println!("{}, {}", r1, r2); | -- first borrow later used here For more information about this error, try `rustc --explain E0499`. error: could not compile `ownership` due to previous error использованием мы попытались создать другую изменяемую ссылку в r2 , которая заимствует те же данные, что и r1 Ограничение, предотвращающее одновременное использование нескольких изменяемых ссылок на одни и те же данные, допускает изменение, но очень контролируемым образом. Это то, с чем борются новые Rustaceans, потому что большинство языков позволяют изменять значение в любой момент. Преимущество этого ограничения заключается в том, что Rust может предотвратить гонку данных во время компиляции. Гонка данных похожа на состояние гонки и происходит, когда возникают следующие три сценария: Два или больше указателей используют одни и те же данные в одно и то же время, Минимум один указатель используется для записи данных, Отсутствуют механизмы для синхронизации доступа к данным. Гонки данных вызывают неопределённое поведение, и их может быть сложно диагностировать и исправить, когда вы пытаетесь отследить их во время выполнения. Rust предотвращает такую проблему, отказываясь компилировать код с гонками данных! Как всегда, мы можем использовать фигурные скобки для создания новой области видимости, позволяющей использовать несколько изменяемых ссылок, но не одновременно: Rust применяет аналогичное правило для комбинирования изменяемых и неизменяемых ссылок. Этот код приводит к ошибке: Ошибка: let mut s = String ::from( "hello" ); { let r1 = & mut s; } // r1 goes out of scope here, so we can make a new reference with no problems. let r2 = & mut s; let mut s = String ::from( "hello" ); let r1 = &s; // no problem let r2 = &s; // no problem let r3 = & mut s; // BIG PROBLEM println! ( "{}, {}, and {}" , r1, r2, r3); Вау! У нас также не может быть изменяемой ссылки, пока у нас есть неизменяемая ссылка на то же значение. Пользователи неизменяемой ссылки не ожидают, что значение внезапно изменится из- под них! Однако разрешены множественные неизменяемые ссылки, потому что никто, кто просто читает данные, не может повлиять на чтение данных кем-либо ещё. Обратите внимание, что область действия ссылки начинается с того места, где она была введена, и продолжается до последнего использования этой ссылки. Например, этот код будет компилироваться, потому что последнее использование неизменяемых ссылок println! , происходит до того, как вводится изменяемая ссылка: Области неизменяемых ссылок r1 и r2 заканчиваются после println! , где они использовались в последний раз — то есть до создания изменяемой ссылки r3 . Эти области не пересекаются, поэтому этот код разрешён. Способность компилятора сообщить, что ссылка больше не используется в точке до конца области видимости, называется нелексическим временем жизни (сокращённо NLL), и вы можете прочитать об этом больше в The Edition Guide Несмотря на то, что ошибки заимствования могут иногда вызывать разочарование, помните, что компилятор Rust заранее указывает на потенциальную ошибку (во время компиляции, а не во время выполнения) и точно показывает, в чем проблема. Тогда вам не придётся выяснять, почему ваши данные оказались не такими, как вы ожидали. $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:6:14 | 4 | let r1 = &s; // no problem | -- immutable borrow occurs here 5 | let r2 = &s; // no problem 6 | let r3 = &mut s; // BIG PROBLEM | ^^^^^^ mutable borrow occurs here 7 | 8 | println!("{}, {}, and {}", r1, r2, r3); | -- immutable borrow later used here For more information about this error, try `rustc --explain E0502`. error: could not compile `ownership` due to previous error let mut s = String ::from( "hello" ); let r1 = &s; // no problem let r2 = &s; // no problem println! ( "{} and {}" , r1, r2); // variables r1 and r2 will not be used after this point let r3 = & mut s; // no problem println! ( "{}" , r3); |