Text
                    Герберт Волверсон

ХОРОШО
ли

ЗНАЕТЕ

Герберт Волверсон Хорошо ли вы знаете Rust?
Rust Brain Teasers Exercise Your Mind Herbert Wo Iverson edited by Tammy Coron
Хорошо ли вы знаете Rust? 24 задачи для разминки ума Герберт Волверсон под редакцией Тэмми Корона Москва, 2025
УДК 004.438Rnst ББК 32.973.2 В67 Герберт Волверсон В67 Хорошо ли вы знаете Rust? / пер. с англ. А. А. Слинкина. - М.: ДМК Пресс, 2025. - 140 с.: ил. ISBN 978-5-93700-361-4 В этой книге на примере подборки из 24 нестандартных задач говорится о причудах языка Rust, а иногда и программирования во- обще. Но не следует рассматривать причуды языка как его недостат- ки. Как правило, посте углубленного знакомства с языком програм- мистам удается с пользой применять его особенности на практике. Предполагается, что читатель имеет опыт создания и выполнения программ на языке Rust, и на компьютере установлена необходимая среда разработки. Задачи в данной книге ориентированы на разработчиков программ Rust начальною и среднего уровня. УДК 004.438Rust ББК 32.973.2 Все права защищены. Любая часть этой книги не может быть воспроизве- дена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. ISBN (анг.) 978-1-68050-917-5 Copyright © 2022 The Pragmatic Programmers, LLC. ISBN (pyc.) 978-5-93700-361-4 © Оформление, издание, перевод, ДМК Пресс, 2025
Оглавление Предисловие от издательства ...............................7 Благодарности..............................................8 Предисловие.. .................9 Об авторе.................................................10 О коде....................................................11 О вас.....................................................12 Будьте открыты к новому...................................13 Задача 1.Три с. небольшим.................................15 Задача 2. Нестандартный ввод..............................19 Задача 3. Преобразование типа ....................... 23 Задача 4. Элементы размером с байт........................29 Задача 5. Какова длина строки?............................33 Задача 6. Перезагрузите Вселенную...................... 37 Задача 7.Туда и обратно...................................41 Задача 8. И ходит как утка, и крякает как утка............45 Задача 9. Не по порядку...................................51 Задача 10, Обманчивый X............................... .55
Задача 11, Стопка боксов...............................59 Задача 12.Амнезия... ................................. 67 Задача 13. Измените полярность потока нейтронов........73 Задача 14. Измерение структур..........................79 Задача 15. И так до бесконечности......................83 Задача 16. Удвой или отстань... ..................... 89 Задача 17. Какой длины вектор?.........................93 Задача 18. Изменить неизменяемое.......................97 Задача 19. Бессонница в Токио.........................101 Задача 20. Хэлло, бонжур..............................111 Задача 21. Завязать гордиев узел......................117 Задача 22. В ожидании Годе............................123 Задача 23. Константные циклы..........................127 Задача 24.Дом на ранчо............................ 131 Литература............................................134 Предметный указатель..................................135
Предисловие от издательства Отзывы и пожелания Мы всегда рады отзывам наших читателей. Расскажите нам, что вы думаете об этой книге - что понравилось или, может быть, не по- нравилось. Отзывы важны для нас, чтобы выпускать книги, которые будут для вас максимально полезны. Вы можете написать отзыв на нашем сайте www.dmkpress.com, зайдя на страницу книги и оставив комментарий в разделе «Отзывы и ре- цензии». Также можно послать письмо главному редактору по адресу dmkpress@gmail.com; при этом укажите название книги в теме письма. Если вы являетесь экспертом в какой-либо области и заинтере- сованы в написании новой книги, заполните форму на нашем сайте по адресу http//dmkpress.com/authors/publish_book/HJiu напишите в издатель- ство по адресу dmkpress@gmail.com. Список опечаток Хотя мы приняли все возможные меры для того, чтобы обеспечить высокое качество наших текстов, ошибки все равно случаются. Если вы найдете ошибку в одной из наших книг - возможно, ошибку в ос- новном тексте или программном коде, - мы будем очень благодар- ны, если вы сообщите нам о ней. Сделав это, вы избавите других чи- тателей от недопонимания и поможете нам улучшить последующие издания этой книги. Если вы найдете какие-либо ошибки в коде, пожалуйста, сообщи- те о них главному редактору по адресу dmkpress@gmail.com, и мы ис- правим это в следующих тиражах. Нарушение авторских прав Пиратство в интернете по-прежнему остается насущной проблемой. Издательство «ДМК Пресс» очень серьезно относится к вопросам за- щиты авторских прав и лицензирования. Если вы столкнетесь в ин- тернете с незаконной публикацией какой-либо из наших книг, пожа- луйста, пришлите нам ссылку на интернет-ресурс, чтобы мы могли применить санкции. Ссылку на подозрительные материалы можно прислать по адресу dmkpress@gmail.com. Мы высоко ценим любую помошь по защите наших авторов, благо- даря которой мы можем предоставлять вам качественные материалы.
Благодарности Эта книга не состоялась бы без терпения, поддержки и любви моей жены, Мел Волверсон. Маргарет Элдридж заслуживает особой благодарности зато, что предло- жила мне написать эту книгу. Я уже наполовину написал «Hands-on Rust», когда она подкатила ко мне с этим названием. После минутного ошелом- ления от такой инициативы со стороны издателя я согласился. Я также благодарен всему коллективу издательства Pragmatic Programmers, в осо- бенности Дэйву Рэнкину и Мики Тебека, которые позволили мне неболь- шие вольности с жанром серии «Хорошо ли вы знаете...». Спасибо редактору Тэм^ми Корону за помощь и неравнодушное отношение к этому проекту. Тэмми - поразительный редактор, ко- торый далеко вышел за пределы своих обязанностей и протести- ровал трудный код на сайте Rust Playground, не давая мне при этом отвлекаться и сопровождая на всем трудном пути к публикации книги. Без него я бы эту7 книгу не закончил. Мои родители - Роберт Волверсон и Дон Макларен - заслуживают безмерной благодарности. Они пробудили во мне любовь к компьютерам в юном возрасте и побуждали экспериментировать и учиться. Сейчас оба они педагоги на пенсии, и именно им я обязан пожизненным стремлением учиться и учить. Отдельное спасибо Кенту Фрешле и Стивену Тэрнеру, моим коллегам по iZones, за постоянную поддержку, предоставление гибкого рабочего дня и терпеливое отношение как к глобальной пандемии, так и к моему писательскому графику. Эта книга не стала бы тем, чем стала, без терпеливой и кропотливой помощи со стороны технических рецензентов: Юргиса Балчиунаса, Фореста Андерсона, Владислава Батыренко, База Залмстра, Ремко Куйпера, Энди Лестера и бесчисленных читателей бета-версий, которые сообщали об опечатках и задавали вопросы. Спасибо также Стиву Коттерилу, который был моим слушателем на протяжении всего творческого процесса.
Rust - очень последовательный язык. Команда Rust Core немало потрудилась над тем, чтобы Rust делал именно то, что вы хотите, и не преподносил сюрпризов, выполняя какие-то дополнительные действия у вас за спиной. Инструментарий Rust - в особенности СИрру и гарантии безопасности - проверяет программу на наличие типичных ошибок и зачастую предлагает улучшения. Программиру- ющие на Rust знают, что написание программы на нем занимает не- много больше врехМени, но, будучи запущена, она работает именно так, как ожидалось. В языке Rust есть свои причуды. Иногда они прячутся в зазорах между системами, а иногда являются осознанным проектным ре- шением, призванным избежать худшего. В этой книге мы рассмо- трим ряд замкнутых программ на Rust для исследования этих при- чуд. Каждая программа заставляет вас напрячь мозги и таким об- разом выучить какой-то аспект Rust, призванный удивить вас. Про- читав код задачи, попробуйте угадать, что она выведет. Возможных ответов три: О программа не откомпилируется; О программа порождает неожиданный вывод (например, «Арифметика все еще работает!»); О программа паникует и завершается с сообщением об ошибке. После каждой задачи объясняется, почему програхмма ведет себя именно так и каким образом подобные проблемы могут повлиять на код ваших программ. Чтобы извлечь .макси.мум пользы из этой книги, старайтесь выполнить код, прежде чем перевернете стра- ницу и начнете читать ответ и обсуждение. Так вы сможете лучше запомнить пройденное. Понимая причуды Rust, вы сможете лучше писать программы на этом языке и, надеюсь, избежите ловушек в своих проектах.
Об авторе Гербер Волверсон - автор книг «Hands-on Rust»1 и «Rust Roguelike Tutorial»1 2. Он разработал и сопровождает библиотеку7 с открытым исходным кодом bracket-lib (вошедшую в состав Amethyst Foun- dation) и на протяжении ряда лет принимал участие во многих проектах с открытым исходным кодом3. Герберт - единоличный владелец компании Bracket Productions. 1 https://pragprog.com/titles/hwrijst/hands-on-rust/. 2 http://bfmghtly.bracketproductions.com/ruslbook/. 3 https://github.com/amethyst/bracket-lib.
О коде Проекты и код максимально краткие и преследуют цель предста- вить минимальный пример для каждой задачи. Примеры являют- ся частью рабочего пространства Rust. Для выполнения каждой программы зайдите в каталог примера в своем терминале и на- берите cargo run. Для некоторых задач необходимы дополнительные библиотеки. В таких случаях рядом с исходным кодом примера отображается файл Cargo, toml.
О вас Предполагается, что на вашем компьютере установлен Rust и что вы знакомы с созданием и выполнением приложений на этом языке. Таким образом, задачи ориентированы на разработчиков начального и среднего уровня. (Если вы член команды Rust Core, то, наверное, знаете обо всех этих причудах больше меня.) Эта книга не учебник; если вы никогда не работали с Rust прежде, го начните с книги «The Rust Programming Language» [KN19] или «Hands-on Rust [W0I2I]1. 1 https://doc.rust-lang.org/book/.
Будьте открыты к новому В этой книге речь идет о причудах Rust, а иногда и программиро- вания вообще. Rust - фантастический язык, несмотря на все свои странности, и слово «причуды» не следует рассматривать как его критику. Напротив, во многих случаях, когда вы узнаете, почему все устроено так, а не иначе, причуды будут выглядеть не столько при- чудливыми, сколько осознанными чертами языка. По мере работы над книгой будьте внимательны и подходите к каждой задаче как сыщик к месту преступления. Все ключи при- сутствуют, и, поняв обсуждение, вы будете лучше понимать, почему все работает как работает, и как самому не попасть в эту конкретную западню Возможно, вы даже пополните свой арсенал какими-то но- выми приемами. Если захотите узнать больше, не стесняйтесь обратиться к Гербер- ту по адресу @herberticus в Twitter или u/thebracket в Reddit.
Задача 1 Три с небольшим ч___________________________________________\ threeanda bit/src/main.rs fn main() { const THREE_AND_A_BIT : f32 = 3.4028236; printinTHREE AND A_BIT); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа печатает следующий результат: 3.4028237 Обсуждение Вы, наверное, ожидали, что программа напечатает 3.4028236. Но, как ни удивительно, результат отличается на 0.0000001 - вы присвои- ли значение 3.4G28236, а результат оказался 3.4028237. Эта разница объясняется тем, как в Rust представляются 32-разрядные числа с плавающей точкой (типа f32). В Rust - как и во многих других язы- ках - числа с плавающей точкой представляются согласно стандарту IEEE-754, который определяет размещение числа в памяти следую- щим образом: 32 бита ◄------------------------------------ - 4----> 4--------------------> 4-------- 1 бит 8 бит 23 бита Показатель степени Мантисса В стандарте также имеется формула для извлечения данных из переменной с плавающей точкой в памяти: /32 = знак (-1 или 1) * 2показательcmen£m‘~ul * 1.мантисса. Rust вычисляет, что самый эффективный способ представить чис- ло 3.4028236 - использовать показатель степени 2 и мантиссу 1.7014118432998657. Это очень точное приближение: 1.7014118, ум- ноженное на 2, дает 3.4028236 - правильный ответ. Но, как выясняется, 7014118 не может быть точно представлено с помощью 32 бит. Отметим, что стандарт IEEE-754 предполагает, что начало числа (1.) существует, но фактически оно не хранится. Ближайшее представление равно 7014118432998ь57, что привносит следующую ошибку: 5.4028237 = 1 х 2'12*-127) х 1.7014118432998657, 3.4028237 = 1 х 2<128~127> х 3.4028236865997314. Цифра, следующая за 6, приводит к округлению с избытком.
Нам необходимо более широкое число с плавающей точкой Иногда проблему точности представления чисел с пла- вающей точкой можно решить, взяв более широкий тип. Число 3.4028237 можно представить типом f64. Если и 64 бит недостаточно, то имеется крейт f 128, пре- доставляющий 128 -разрядные числа с плавающей точ- кой (ценой снижения производительности). Это не па- нацея - некоторые числа упрямо не хотят быть пред- ставленными в виде с плавающей точкой. Некоторые константы, например л, вообще нельзя представить точно, без аппроксимации не обойтись. Другие числа представить можно, но не так, как определяет стандарт IEEE-754. Если вам действительно необходимо идеальное пред- ставление, то Cargo предлагает математические биб- лиотеки (например, гид) с произвольной точностью1. Зачастую их использование сопровождается значи- тельной потерей производительности, так что поду- майте, какая точность вам действительно необходима. Какая точность необходима? Разным программам нужна разная точность представления чисел с плавающей точкой. Например, в видеоиграх небольшие отклоне- ния в расположении графических объектов обычно остаются неза- меченными. Если вы работаете с реальными деньгами, то ошибки округления могут оказаться катастрофическими (обычно использу- ют целый тип, в котором копейки занимают последние два знака, или библиотеку вычислений с фиксированной точкой). Полностью избежать связанных с точностью проблем можно бла- годаря изобретательному дизайну. Предположим, что вы проекти- руете игровой космический конструктор и хотите моделировать конструкции на Земле и Плутоне. В таком случае вряд ли стоит по- мещать обе планеты в одну систему координат. Вместо этого можно использовать «локальную планетную^ систему, которая: О позволяет использовать координаты с гораздо меньшей точ- ностью;
О упрощает учет неприятной привычки небесных тел все время двигаться; О не тратит ценные диапазоны координат на пустынные обла- сти космоса. Как всегда в информатике, имеется компромисс между производи- тельностью и точностью. Поэтому подумайте, какую задачу вы хо- тите решить с помощью своей программы, и выберите точность представления, соблюдая баланс между тем, что вам необходимо, и требуемой скоростью работы программы Числа с плавающей точ- кой непосредственно поддерживаются процессором - и работают очень быстро. Но даже при встроенном представлении тип f32 мо- жет оказаться быстрее, чем f64, потому что 32-разрядные числа за- нимают меньше памяти и, значит, в кеше их помещается больше. Библиотеки с фиксированной точкой и с произвольной точностью хмогут работать быстро, но все равно это будет медленнее по срав- нению со встроенной поддержкой плавающей точки. Вам решать, какое соотношение между точностью и производительностью при- емлемо в вашей программе. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Стандарт IEEE-754 https ://еп. Wikipedia. org/wiki/IEЕЕ_754. RUG - крейт для работы с числами произвольной точности h ttps://l ib.rs/crates/rug. Крейт fl28 https ://lib.rs/crates/fl 28. Крейт fixed https ://docs.rs/fixed/l. 10.0/fixed/.
Задача 2 Нестандартный ввод standard_input/src/main.rs use std::io::stdin; fn nai.n () { printin! ("What is 3+2? Type your answer and press enter."')-, let nut input = String::new(); stdin() .read_line(&mut input) .expect("Urable to read standard input”); if input == "5" { printin!("Correct!"); } else { printin!( 'Incorrect! ); } } Угадайте результат 0 Попробуйте угадать результат, не переворачивая страницу7.
Взаимодействие с программой будет выглядеть следующим образом: < = vJhat is 3+2? Гуре your answer and press enter. => 5 <= Incorrect! Обсуждение Обычно 3 + 2 равно 5, но не тогда, когда за обработку строк берется Rust. Чтобы понять почему, добавим следующую строку в конец программы: printin!("{:#?} input); После этого вы сможете увидеть полную строку, прочитанную Rust из stdin: < = What is 3+2? Туре уоиг answer and press enter. => 5 < = Incorrect! "5\r\n" В системах на основе UNIX вы увидите 5\п. Система стандартного ввода Rust включает управляющие последо- вательности, представляющие клавишу Enter. \г обозначает возврат каретки, а \п - перевод строки. Убрать непечатаемые символы по- зволяет функция trir.(). В следующей программе решение арифметической задачи пра- вильно: use std::io::stdin; fn main() { printin!("What is 3+2? Type your answer and press <enter>"); let mut input = String::new(); stdin() .read_line(&mut input) .expect("Unable to read standard input ); if input.trimO == "5" { printin!("Correct!"); } else { printin!("Incorrect!"); } }
Не доверяйте вводу Есть хорошее правило - никогда не доверять введенной информа- ции. Однако кое-какие действия позволяют свести к минимуму про- блемы в случае, если ввод необходим: О при работе со строкахМИ пользуйтесь функцией trim() для уда- ления пробельных символов; О при сравнении строк пользуйтесь функциями to_lowercase() или to_uppercase(), чтобы гарантированно сравнивать строки в одном регистре. Эти функции знают о регистре в Юникоде1; □ при разборе сложных строк пользуйтесь регулярными выра- жениями для извлечения частей строки. Внедрение SOL-кода Особенно настороженно относитесь к введенным дан- ным, которые передаются базе данных SQL или другим системам, принимающим текстовые команды. Злона- меренный пользователь мог бы ввести свое имя в виде 10; DROP TABLE members; /*. Если ваша программа просто конкатенирует строки, то дело может закончиться вы- полнением в базе данных команды SELECT * FROM members WHERE id=16; DROP TABLE members; /* И это было бы печально, потому что вы только что удали- ли из базы данных таблицу members. Чтобы избежать подоб- ной проблемы, можно использовать параметризованные запросы, которые база данных должна поддерживать. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Строки https://duc.rust-lang.org/std/string/struct.String.html. Функция trimO https://doc.rust-Lang.Org/std/string/struct.String.html#method.trim. Крейт Regex nttps //crates, io/crates/regex/ Шпаргалка no внедрению SOL-кода https://www.netsporker.com/biog/web-security/sql-injection-cheat-sheet/ 1 https://github.com/rust-lang/rust/issues/9363.
Задача 3 Преобразование типа >_____________________________________________) ty peconver sion/s rc/ma i n.rs fn main() { let x : u64 = 4_294_967_296; let у = x as u32; if x == у as u64 { printin! ("x equals y. ); } else { println!("x does not equal y."); } } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа печатает следующий результат: х does not equal у. Обсуждение Ключевое слово as в Rust ведет к потере информации. А когда вы ис- пользуете его для преобразования типов, то есть риск потерять точ- ность без предупреждения. В этом примере переменной у присваивается значение 4_294_967_296, но результат обрезается, потому что число больше максимального значения 32-разрядного целого без знака. Сюр- приз в том, что ни компилятор Rust, ни Clippy, ни среда выполнения не выдают ни ошибки, ни предупреждения о потере данных. Если всс-таки планируете использовать ключевое слово as для преобразования типов, - поскольку чаще всего Rust производит это преобразование без проблем, - то имейте в виду следующее: О преобразование меньшего типа в больший (например, и32 в и64) никогда не приводит к потере точности, так что вы в без- опасности; О при работе с числами, которые гарантированно умещаются в оба типа, потери данных не произойдет. Но будьте осторожны с дан- ными, полученными от пользователей или в результате вычис- лений, - если вы не контролируете данные, то не можете быть уверены в том, что они окажутся в допустимых диапазонах; □ будьте осторожны с преобразованиями чисел с плавающей точкой в целые, потому что Rust всегда округляет с недостат- ком. А раз так, то лучше явно указать желаемое поведение, вы- звав функцию ру.float, floor () для округления с недостатком, ny_float.ceil() для округления с избытком и my_float.round() для обычного численного округления. Если вам нужно окру- гление, то производите его до использования as. По счастью, Rust предлагает кое-какую помощь и другие способы справиться с преобразованием типов. Литеральные и нелитеральные значения Если вы имеете дело с литеральными значениями (например, опре- деленными непосредственно в исходном коде), то компилятор Rust
обладает даром определять, что значение не поместится в тип. На- пример, взгляните на этот код: let х: и32 = 18_446_744_073_769_551_615; Он не компилируется с сообщением об ошибке «the literal 18_446_744_073_709_551_615 does not fit into the type u32 whose range is l. .=4294967295» (литерал 18_446_744_073_709_551_615 не помещается в тип u32, имеющий диапазон ). .=4294967295). Rust может также защитить вас от ошибок вследствие арифмети- ческих операций над литералами. Например, предложение let х = 4_294_967_295 * 2; не откомпилируется. При работе с нелитеральными переменными (например, полу- ченными от пользователя или вычисленными) компилятор Rust не может увидеть значение заранее. Используя as, вы говорите Rust «я знаю, что делаю». Еще один вариант - не использовать as, тогда Rust будет проще защитить вас от неожиданного поведения. Как защититься от потери точности Rust предлагает характеристику (trait) Into для безопасных преобра- зований типов на этапе компиляции. Например, можно следующим образом преобразовать тип и32 в иб4: let у = и32::nax_value(); let z: u64 = y.i.nto(); Характеристика Into решает проблему потенциально невозмож- ных преобразований тем, что не реализует их. Так, выполнить обратное преобразование - u64 в и32 - с помощью Into не получится. Если вы попробуете написать let z : u32 = (12_ u64).intc(), то вызов функции into() не откомпилируется. Для теоретически возможных преобразований Rust предлагает другую характеристику: Trylnto. В следующем коде try_into() пыта- ется преобразовать и64 в и32: use std::convert::Trylnto; let z: u32 = (50l’0_u64).try_into().expect( "Conversion error")', Функция try_into() возвращает тип Result. К его содержимому можно обратиться так же, как к другим значениям типа Result. На- пример, вы можете:
О распаковать содержимое функцией unwrap и «грохнуться», если при преобразовании возникла ошибка; О распаковать содержимое функцией unwrap_;r и подставить значение по умолчанию в случае ошибки; О применить к Result функцию match, чтобы обработать ошибку явно; О использовать expect. В примере выше использована expect. Если заменить 500G числом, не помещающимся в 32-разрядное целое без знака, то программа в панике завершится при попытке выполнить преобразование. Избыточные преобразования типа Очень многие преобразования типа можно квалифици- ровать как «запашок в коде», т. е. указание на какой-то порок в ваших рассуждениях. Если все функции, в кото- рых используется переменная х, ожидают, что ее значе- ние имеет тип и32, то так и объявите ее с самого нача- ла. Если впоследствии появятся функции, ожидающие значения типа usize, то вы сможете сделать свой код го- раздо чище, если выполните преобразование один раз, а не при каждом вызове. Нахождение ошибок преобразования типа с помощью Cuppy Rust включает инструмент Clippy, который помогает находить ошибки в коде. Для его вызова наберите в терминале команду cargo clippy - и в ответ получите список найденных Clippy ошибок. Пара- метры С]ippy по умолчанию заданы так, что в примере выше не бу- дут найдены все ошибки, но более строгий режим pedantic может обнаружить потенциальные проблемы. Чтобы включить режим pe- dantic, добавьте одну строку в начало своего файла main. г s: #[warn(clippy::pedantic)]
Теперь Clippy выдает следующие предупреждения при выполне- нии cargo clippy: warning: casting 'u64' to 'u32' may truncate the value warning: casting u32 t( u64 may become silently lossy if you later change the type В педантичном режиме Clippy радостно сообщает обо всех заме- ченных потенциальных ошибках - даже если они не приводят к про- блемам. Многие разработчики считают такой уровень информиро- ван ия назойливым. Кроме того, педантичная проверка может замед- лять работу7 в больших проектах. Приемлемый компромисс - перио- дически запускать Clippy в педантичном режиме, а после обработки результатов закомментировать эту строку. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ «as» httpsy/doc.rust-lang.org/std/keyword.as.html. Считать ли «as» вредным? https://asers.rustlang.org/t/as-considered-harmful/35338. into https://doc.rust-lang.org/sid/convert/troit.lnto.htmL try_into https//doc. rust-lang org/std/convert/trait. Trylnto.html. округление f32 https://doc.rust-lang.org/std/primitive.f32.htmlttmethod.iound.
Задача 4 Элементы размером с байт >_________________________________________J byte_sized/src/main.rs fn main() { let nut counter : i8 = 0; loop { printin!(counter); counter += 1; } } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Ответ зависит от того, как программа запускалась. О Если вы запускали программу в отладочном режиме коман- дой cargo run, то она напечатает последовательность чисел от до 127, а затем аварийно завершится с сообщением «thread ’nain' panicked at 'attempt to add with overflow', overflow\src\ nain,rs:6:9». Если вы запускали программу в выпускном режиме командой cargo run - - release, то она напечатает последовательность чисел от 0 до 127, а затем от -128 до -1. И будет повторять ее, пока вы не нажме- те Ctrl+C. Если у вас есть опыт работы с C/C++, то такое поведение покажется вам ожидаемым. Обсуждение Современные компьютеры обычно хранят целые со знаком в допол- нительном коде. Первый бит числа (в двоичной форме) знаковый, он показывает, положительно число (0) или отрицательно (1). Если первый бит равен 1, то число равно наименьшему числу, представи- мому имеющимся количеством битов (-128 для типа 18), минус зна- чение остальных битов. Двоичная арифметика работает так же, как действия по столбцам в десятичной арифметике, но перенос в следующий разряд произво- дится не когда результат больше 10, а когда он больше 1. Местополо- жение знакового бита в дополнительном коде приводит к интерес- ным результатам. Рассмотрим следующие два примера операций с 8-битовым чис- лом со знаком: О 1 + 1 = 2, как и следовало ожидать; О 127 + 1 = -128, потому что последняя цифра переносится в зна- ковый бит.
Проверки в отладочном режиме В отладочном режиме Rust включает много проверок, подавляемых в выпускных сборках. Компилятор Rust пытается помочь вам избежать беды, но вы должны по- зволить ему это. Если отладочный режим кажется вам слишком медлен- ным, но вы все-таки хотите повысить безопасность, включив проверки переполнения, то можете активи- ровать отладочный режим с оптимизациями. Для этого добавьте следующий код в файл Cargo.tonl: u. [profile.dev] opt-level = 1 # 1 - минимальная оптимизация и хорошая отладка Включение оптимизации в сочетании с отладкой ускоря- ет работу программы в отладочном режиме, но компиля - ция потребует больше времени, а отладчик может пере- прыгивать из одного места в другое, если Rust переупо- рядочил ваш код. Увы, бесплатных завтраков не бывает. Когда возникает необходимость в переполнении Иногда мы хотим, чтобы численное переполнение произошло. Во многих криптографических алгоритмах и генераторах случай- ных чисел предполагается, что должно произойти оборачивание целого числа (переход через максимальное значение к минималь- ному). Rust позволяет задать такое поведение с помощью струк- туры std: :num::Wrapping. Безопасная версия нашей программы вы- глядит так: byte_sized_wrap/src/nain.rs use std::num::Wrapping; fn main() { let mut counter = Wrapping(GiS); loop { printin!("/J , counter); counter += Wrapping(li8); } }
Обнаружение переполнения БЕЗ АВАРИЙНОГО ЗАВЕРШЕНИЯ Если оборачивание вашей программе не нужно, но вас беспокоит, что вы можете оказаться в ситуации, когда переполнится емкость переменной, то Rust готов подставить плечо. Для числовых типов в Rust реализован набор функций checked: checked_add, checked_div, checked_nul, checked_sub и еще несколько1. Функции checked возвращают значение типа Option, которое со- держит либо Sone(x), если операция выполнена успешно, либо Nene в случае переполнения, как показано в примере ниже. if let Some(n) = x.checked_aod(b) { // Все удачно, п содержит результат. } else { // Переполнение - обработать ошибку. ] Наконец, можно сочетать поведение Wrapping с его обнаружением с помощью функций overflowing-. Они возвращают кортеж, содержа- щий результат, включающий переполнение, и значение типа bool, по- казывающее, имело место оборачивание или нет1 2. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ std: :пит:: Wrapping https://doc.rust-lang.org/std/num/struct.Wrapping.html. Дополнительный код https://en.wikipedia.org/wiki/Two%27s_ complement. Профили Cargo https://doc.rust-long.org/cargo/refeience/profites.htmL Типы данных в Rust https://doc rust-lang.org/book/ch03-02-data-types.html. 1 https://doc.rust-lang.Org/std/primitive.u32.html#methad.checked_add. 2 https://doc.rust-lang.Org/std/primitive.u32.html#method.overflowing_add.
Задача 5 Какова длина строки? >____________________________________________________) string iength/src/main.rs const HELLO_WORLD : &'static str = "Hallo heinur"; fn main() { printin!('YJ is {} characters long.", HELLOjdORLD, HELLO_WORLD.len() ); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа печатает следующий результат: Hallo heimur is 13 characters long. Обсуждение Глаза вас не обманывают - строка «Hallo heimur» действительно со- держит 12 символов (включая пробел). Давайте поговорим о том, как работает тип String в Rust. Определение внутренней структуры struct String непритязательно: pub struct String { vec: Vec<u8>, } Строка - это просто вектор байтов (u8), представляющий символ Юникода в кодировке UTF-8. Rust автоматически преобразует вашу строку в UTF-8. Выглядит она так: Символы Юникода (10 символов) Кодировка UTF-8: скалярные значения (19 байт) Исходная строка «Hallo heimur» состоит из И символов ASCII (включая пробел) и одного символа из раздела «Дополнительная ла- тиница - 1»: 6. Для кодирования символов ASCII нужен 1 байт, для дополнительной латиницы 2 байта. Кодирование строк в Rust устроено так, чтобы не хранить лишних нулей для каждого символа Юникода. В противном случае тип String был бы вектором типов char. Тип Rust char занимает ровно 4 байта; это максимальный размер одного символа Юникода1. Переменные типа char представляют не один символ ASCII, а скалярное значение Юникода. Скалярное значение может представлять один глиф или модификацию другого глифа. Длина строки Функция String.len() подсчитывает количество байтов в векзоре, стоящем за строкой. Если бы в String все символы хранились в виде 1 https://doc.rust-lang.org/std/primitive.char.html^representation.
char, то строка Hallo heimur занимала бы 48 байт памяти. Но в String хранятся не символы, а массив байтов, содержащий только те байты, которые необходимы для вывода хранимого текста. Не каждый символ UTF-8 нуждается во всех четырех байтах для отображения. Например, для пробела нужен только 1 байт ()х2Э), а для большинства символов расширенной латиницы - 2 байта. Пер- вый байт (( хСЗ) указывает, что символ принадлежит набору «Расши- ренная латиница», а второй (0хВЗ - 6) идентифицирует сам символ. Строка Hallo heimur содержит 11 символов ASCII, каждый из кото- рых занимает 1 байт памяти, а все вместе - 11 байт. Добавьте еще 2 байта для 6, и окажется, что строка занимает 13 байт в памяти. Подсчет символов Правильно подсчитать количество символов в строке Hallo heimur позволяет следующий код: printin’("{} is {} characters long. , HELLO_WORLD, HELLO_WORLD .chars() // Преобразовать в итератор no последовательности символов .countO // Подсчитать число символов в последовательности ); Вызывая my_str.chars(), вы запрашиваете итератор, который бу- дет отдавать каждый элемент строки в виде char1. Rust правильно определяет, что всего имеется 12 глифов - или скалярных значений Юникода, - составляющих строку. Итератор передает каждый из них вашему потребителю в виде 4-байтового символа. Даже если глиф требует только 1 или 3 байта памяти, Rust выделяет все 4 байта для типа char. Обход итератора потребляет очень мало дополнительной памяти. Если вызвать метод итератора collect(), чтобы создать век- тор данных типа char, то этот вектор займет 4U байт памяти. Для доступа к отдельным символам строки используйте функ- цию my_str.chars(). Это итератор, поэтому вы можете вызывать nth, for_each и другие функции итератора, чтобы найти искомое. Например, для доступа к четвертому элементу строки нужно вы- звать my-Stг.chars().nth(4). 1 https://doc.rust-long.org/std/str/struct.Chars.html.
Последствия вычисления размера в UTF-8 Измерение размера строки Юникода иногда приводит к сюрпризам. Вы должны понимать разницу межу символами и байтами. О Проверяя длину строки, помните, что считается, а что - нет. Например, если вы принимаете только имена пользователей длиной не более 10 символов, то должны решить, что имеется в виду: глифы или байты. О При хранении строк в базах данных нужно выделять достаточ- но места для строк в кодировках, отличных от латиницы. □ При передаче или приеме информации от удаленного API не- обходимо договориться о стандарте длины для кодирования пересылаемых строк. О Если вы пишете программу для системы с ограниченной па- мятью, то помните, что посимвольный разбор строк Юникода может потребовать гораздо больше памяти, чем вы ожидаете. СтрокаТоуе: v иместдлину7символов,занимает12байтвтипе String и 32 байта, если обрабатывается как вектор отдельных символов. Может показаться, что это немного, но если ваша программа чтения попытается загрузить всю «Войну и мир» в поле ввода, то посимвольный разбор может потребовать куда больше ресурсов, чем вы ожидали. О При доступе к отдельным символам строки гораздо безопаснее использовать функцию chars, чем работать напрямую с массивом байтов. Символы знают о границах Юникода, байты - нет. Напе- чатав первые 6 байт сюва «Кбппеп», вы увидите только «Коппе». Напечатав первые 6 символов, вы увидите слово целиком. Для ДОПОЛНИТЕЛЬНОГО чтения Char https://doc.rust-lang.org/std/primitive.char.html. Длина строки https://doc.rust lang.org/std/string/struct.String.html#method.len. Справочное руководство по символам Юникода https://www.compart.com/en/unicode/. «Википедия» о UTF-8 https://en.wikipedia.org/wiki/UTF-8. Исходный код String https://doc.rust-lang.org/src/alloc/string.rs.html.
J Задача 6 Перезагрузите Вселенную >________________________________________________) rebootuniverse/src/main.rs fn main() { if 0.1 + 0.2 == 0.3 { printin! (''Arithmetic still works."'); } else { printin!('Please reboot the universe. ); } } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа печатает следующий результат: Please reboot the universe. Обсуждение Вы,конечно,ожидаете,что 0.1 + 0.2 равно 0.3. Так оно и есть,нотоль- ко не в случае арифметики чисел с плавающей точкой. Ответ очень близок, но приближение мешает точному сравнению, и складывает- ся впечатление, что арифметика перестала работать. Эта ситуация тесно связана с рассмотренной в задаче 1. Как вы уже, наверное, догадались, сравнение с плавающей точкой преисполнено ошибками, не очевидными с первого взгляда. Взять, к примеру, долго работающую программу, которая проверяет условие завершения, анализируя число с плавающей точкой, являюще- еся результатом некоего вычисления. Или последовательность авто- номных тестов, в которых результаты математических функций скру- пулезно сравниваются с заведомо верными ответами. В обоих случаях вычисленные значения могут оказаться «недостаточно близкими», чтобы все работало, как ожидается. Иными словами, программа может никогда не завершиться, а тесты не будут проходить - хотя и должны. Поскольку накопление крохотных ошибок может вырасти в боль- шую проблему, многие преподаватели информатики учат студентов «никогда не сравнивать чиста с плавающей точкой». Отлавливайте ошибки с помощью Clippy По счастью, мы можем использовать Clippy (линтер для Rust) для об- наружения ошибок еще до того, как они превратились в проблему. Чтобы посмотреть на него в действии, выполните команду cargo clippy в каталоге code/float_compare. Clippy напечатает следующее предупреждение: ।лллаалааллалалал help: consider comparing then within some margin of error: |'(0-1 + 0-2 - 0.3).abs() < error_margin' =note: '#[deny(clippy::float_cmp)] on by default =note f32::EPSILON' and f54::EPSILON' are available for the 'error_margin help, for further information visit https:11 rust-lang.github.io/rust-clippy/master/index.html#fioat_cmp Заодно Clippy предлагает безопасную альтернативу сравнению. Если производить сравнение с определенной точностью, то чиста
с плавающей точкой можно будет сравнивать безопасно, гарантируя, что они «достаточно близки» к правильному ответу. Clippy предлагает использовать EPSILON как верхнюю границу ошибки. Константа EPSILON встроена во все типы с плавающей точ- кой в Rust. Восстановить пошатнувшуюся веру в арифметические способности Rust поможет такой код: if (0.1Г32 + 0.2f32 - 0.3f32).abs() < std::f32::EPSILON { printin! ('Arithmetic works")', } Сравнение c EPSILON может показаться неуклюжим. Крейт float_cmp предлагает удобный интерфейс для вычисления приближенного ра- венства с помощью макроса approx._eq1 Берегитесь внешних библиотек Будьте особенно внимательны при работе с результа- тами с плавающей точкой, полученными от внешних библиотек. Если они были откомпилированы с расши- рением С fastmath, то результаты могут оказаться еще менее точными, чем вы ожидаете. Fastmath позволяет себе некоторые вольности при вычислениях во имя ско- рости, так что ответы оказываются очень близки к тре- буемым, но все же не совпадают с ними. Иногда fast- math применяет оптимизации, а иногда нет, так что если точность важна, то попробуйте найти версию внешней библиотеки, которая не пользуется оптимизацией. Моя команда недавно перенесла на Rust некоторые вы- числения радиочастот из старой библиотеки, написан- ной на C++. Несмотря на использование EPSILON, наши тесты упорно отказывались проходить. После долгих разбирательств мы выяснили, что библиотека, кото- рой мы пользовались почти десять лет, давала толь- ко четыре точных знака после запятой. Впрочем, это не имело значения, потому что и такая точность нам была не нужна. Воспользовавшись крейтом float_cmp с большей величиной погрешности, мы продемонстри- ровали, что перенесенный код был достаточно близок к правильным значениям, которые давала старая биб- лиотека, и смогли продолжить работу. 1 h tips ://cro tes. io/crates/float-cmp.
Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Крейт float-стр https://crates.io/crates/float-cmp. Предупреждение о сравнении чисел с плавающей точкой от Clippy https //rust lang.github.io/rust-clippy/master/index.html#float_cmp.
Задача 7 Туда и обратно >_________________________________________________) there_and_back/src/main.rs use std::f32::consts::PI; pub struct Degrees(pub f32); pub struct Radians(pub f32); impl Degrees { pub fn new(angle: f32) -> Self { Self(angle) } } inpl Fropi<Degrees> for Radians { fn from(itepi : Degrees) -> Self { Self(iten.0 * PI / 180.0) } } fn main() { let one_eighty_degrees = Degrees::new(180.G); let one_eighty_radians : Radians = cne_eighty_degrees.into(); printin!( ISO Degrees in Radians = {}", one_eighty_radians.0); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа печатает следующий результат: 180 Degrees in Radians = 3.1415927 Обсуждение Сюрприз заключается в том, что характеристика Into не реализова- на, и тем не менее программе удалось использовать функцию into() с типом Radians. Когда вы определяете характеристику From, Rust автоматически реализует обратную характеристику Into. Это очень удобно, но так- же и удивительно, поскольку вообще-то Rust настаивает, чтобы лю- бое поведение было определено явно. До выхода версии Rust 1.4.1 эта автоматическая реализация применялась только к типам, до- ступным из крейта, в котором определен тип; в последующих вер- сиях обратная характеристика включается всегда. С помощью кода из этой задачи можно преобразовать градусы в радианы любым из двух способов: let г : Radians = d.intoQ или let г = Radians::from(d). Это преобразование работает, потому что Rust добавил функцию in to () в тип Degrees, а сама программа уже опреде- лила fron(). Однако, поскольку ни Rust, ни программа не добавили обратное преобразование, для преобразования из радианов в граду- сы придется написать следующий код: impl From<Radians> for Degrees { fn from(item: Radians) -> Self { Self(item.0 / (PI / 180.0)) } Добавление этого кода позволяет преобразовывать градусы в ра- дианы и обратно. Rust не реализует автоматически преобразова- ние в обоих направлениях, потому что иногда смысл имеет только преобразование в одну сторону. Яйцо в омлет превратить можно, но вот сделать из омлета яйцо затруднительно. То же самое отно- сится к типам данных. Если при преобразовании какие-то данные теряются, то Rust не может знать, какие значения вставить в каждое поле конечного типа.
Тщетная попытка невозможного преобразования В задаче 3 «Преобразование типа» вы видели, что не всякое преоб- разование работает. Но не отчаивайтесь, вы можете сами реализо- вать характеристику TryFron для своих типов, чтобы попытаться вы- полнить преобразование и сообщить об ошибке в случае неудачи. Предположим, что нам нужно ограничить числовой тип, так что- бы он мог принимать только значения от 0 до 10. Сделать это можно, реализовав try_fron: use std::convert::TryFron; struct ZeroTo7en(i32); impl TryFrcm<i32> for ZeroloTen { type Error = &'static str; fn try_from(value: i32) -> Result<Self, Self::Error> { if value < 0 11 value > 10 { Err("Value must be between в and 10 ) } else { Ok(Self(value)) } } } Как и в случае характеристики From, определение TryFron автома- тически создает определение обратной характеристики Trylnto.
Используйте строгие типы во избежание ошибок Допустим, вы работаете с разными единицами измере- ния, например метрами, футами, дюймами и сантиме- трами. Чтобы уменьшить потенциальную погрешность при преобразовании дюймов в сантиметры, вы може- те определить единицы измерения и преобразования между ними. Затем, когда выполняется вычисление, требующее единицы измерения, вы определяете тип единицы в параметрах функции. Некоторые языки - и прежде всего Ada - позволяют задавать диапазон значений типа. Это полезно, когда значения не должны выходить за пределы определен- ного диапазона. Определив тип с ограниченным диа- пазоном, вы можете автоматически включить такую проверку при любом преобразовании типа. Строгие типы преобразуют логические ошибки - на- пример, использование градусов там, где нужны ради- аны, - в ошибки времени компиляции, что уменьшает общее число ошибок в коде. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ std::convert::From trait https://doc.rust-lang.org/std/convert/iraii.From.html. std: -.convert::TryFrom trait https://doc.rust-lang.org/std/convert/trait.TryFrom.html.
Л Задача 8 И ходит как утка, и крякает как утка >___________________________________________________/ quack/src/main.rs fn double_it(n: и64, 132) -> и64 { п * 2 } fn main() { let one: 132 = 1; let n = double_it(°ne as 3); printin!(n); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа печатает следующий результат, не выдавая ни пред- упреждений, ни ошибок: 2 Обсуждение Тут мы видим два сюрприза: О вы можете назвать параметр функции _, что потребует от ее пользователя передать переменную в этом параметре. Но эта переменная не используется и будет устранена при оп- тимизации в выпускных сборках; О конструкция one as _ компилируется и работает. Значение типа i32 было преобразовано в тип и54 без явного указателя типа. Rust не поддерживает утиную типизацию - автоматическое пре- образование типов, если имеется похожий тип, - и, вообще говоря, очень строго относится к преобразованиям типов. Знак подчеркивания (заполнитель) в Rust интерпретируется по- разному в зависимости от контекста: О при использовании в качестве префикса имени переменной (например, _ignore_me : 132) знак подчеркивания означает, что переменная сознательно не используется, и подавляет пред- упреждения о «неиспользуемой переменной»; О если он используется в качестве целого имени переменной, то Rust понимает, что вы вообще не собираетесь эту перемен- ную использовать. В предложении match (например, _ => { .. }) знак подчеркивания обозначает действие по умолчанию. Если не выбрана никакая другая ветвь match, то будет вычислено действие по умолчанию; О знаки подчеркивания можно использовать и в функциях, ко- торые возвращают значение, помеченное как #[must_use]. На- пример, let _ = riy_important_function() будет игнорировать результат функции, подавляя сообщения об ошибках и пред- упреждения о том, что результат не используется. В сочетании с ключевым словом as знак _ означает, что Rust дол- жен преобразовать значение в тип, ожидаемый в данном контексте. Например, function_that_requtres_a_u32(x as _) попытается преобра- зовать параметр х - вне зависимости от его типа - в тип и32. Если тип х в принципе не допускает преобразования в и32, то компилятор Rust выдаст ошибку. Если же этот тип может быть преобразован,
то Rust попытается это сделать - с той же потенциальной потерей точности, которую мы наблюдали в задаче 7 «Туда и обратно». Это называется выведенной типизацией. Rust пытается вывести тип переменной из контекста, в котором она используется. В нашем при- мере функция требует значения типа и64, поэтому' Rust пытается пре- образовать переменную в 64-разрядпое целое без знака. Выведенная типизация может оказаться полезной для сокращения кода, особенно когда имена типов сложны, но использовать ее везде не рекохмендуется. Почему не следует использовать выведенную типизацию ВСЮДУ? Очень соблазнительно использовать as _ повсеместно и больше не беспокоиться о преобразованиях типов. И в большинстве случаев это будет работать с оговоркой насчет потери точности. Так почему этого не стоит делать? Документация Rust ясно предупреждает, что язык против исполь- зования as _ всюду: «as можно использовать с заполнителем _, когда коночный тип можно вывести. Заметим, что это может привести к ошибкам вы- вода, и обычно следует использовать явный тип - так код будет более понятным и стабильным»1. Ниже перечислены недостатки as О использование явно поименованных типов - и придумыва- ние понятных имен для них - может значительно прояснить, что вы намеревались сделать в програмхме. Если вы работаете в одиночестве, то, вернувшись к проекту после нескольких не- дель отсутствия, вы с трудом вспомните, о чем думали, когда писали х as О иногда вывод типа в Rust дает сбой, если требуется вывести типы переменных в цепочке; О многие среды разработки всеми силами пытаются показать вам конкретные типы переменных после выведенного преоб- разования; О возможны тонкие ошибки, если механизм вывода типа в Rust остановится на каком-то типе, но это не тот тип, который вы ожидали. 1 https://doc.rust-lang.org/std/keyword.os.html.
Таким образом, хоть Rust и искушает вас чем-то вроде утиной ти- пизации, не стоит использовать ее постоянно. Сочетание проблем при выводе типа с потенциальной потерей точности при использо- вании ключевого слова as - прямая дорога к неприятностям. Во мно- гих случаях лучше использовать into() (или try_into()) вместо as. Когда стоит использовать выведенную типизацию? Самая распространенная ситуация, подходящая для использования as _, - низкоуровневый код с указателями. В следующем коде тип клонируется и возвращается как указатель: unsafe fn clone_ptr(&self) -> *mut () { E эх::into raw(Bэх::new(self.cLone())) as } Потребность в таком коде возникает нечасто, но если возникает, то as _ позволяет упростить трудный для чтения код. Нередко можно встретить as в библиотеках, находящихся на ранней стадии разра- ботки. Авторы библиотеки, возможно, еще не уверены, какой тип ста - нет частью интерфейса, поэтому оставили as _ до стабилизации API. Преобразование типа в реальном коде В больших программах неизбежно приходится при- бегать к преобразованиям типов. Использовать as можно, когда Rust уверен в желаемом типе, но не ре- комендуется в качестве первой же попытки решения. В общем случае рекомендуется применять различные меры в следующем порядке: 1) использование into() сохраняет точность и очень хорошо оптимизируется; 2) try_into() позволяет самостоятельно обработать ошибку преобразования; 3) используйте as type, когда уверены, что преобра- зование безопасно; 4) используйте as _, если больше не можете ничего придумать.
Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Знак подчеркивания https.//runrust.miraheze.ory/wiki/Underscore. As https://doc.rust-lany.org/sid/keyword.as.html.
Задача 9 Не по порядку out_of_order/src/main.rs fn main() { let nut floats = vec![3.1, 1.2, 4.5, 0.3]; floats. sort(); printin! ("{:#?}", floats); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа не компилируется со следующим сообщением об ошибке: the trait Ord' is not implemented for '{float} Обсуждение В Rust легко отсортировать векторы значений большинства типов. Функция вектора sort() умеет сортировать вектор строк в алфа- витном порядке или вектор целых в числовом порядке. Так поче- му же не работает сортировка вектора чисел с плавающей точкой? Поскольку числа с плавающей точкой не всегда числа (подробнее об этом чуть ниже), не всегда они допускают естественную сорти- ровку. В общем случае сортировка значений в Rust производится очень просто, но язык также тщательно избегает поведения, которое могло бы удивить программиста. Рассмотрим следующие «невозможные» математические вычис- ления: О тангенс угла 90е равен бесконечности, такой результат нельзя признать удовлетворительным; О деление на нуль дает плюс или минус бесконечность; О сравнение бесконечности с бесконечностью не имеет смысла; О сравнение нечисла с чем угодно также не имеет смысла. Во всех этих примерах результат не является чистом. Чтобы такого рода вычисления были возможны, тип с плавающей точкой позволяет хранить значения Not a Number (NaN - нечисто) и Infinity (бесконечность). В Rust имеются характеристики PartialOrd и PartialEq - и соответ- ствующая функция partial_cmp() - для представления числовых ти- пов, которые, вообще говоря, допускают сравнение, но в некоторых случаях два числа невозможно ни сравнить, ни упорядочить. tan(90°) Тангенс 90° равен NaN. Интересно, что неточность, присущая вычислениям с плавающей точкой, затруд- няет демонстрацию этого факта: printin!(“£}”, (90,0* (std: :f32: :consts: :PI/180.0)).tan()); Этот код печатает 22877334.G. Преобразование 90° в ра- дианы привело к потере точности, и в результате полу- чился угол, тангенс которого конечен.
Типы с плавающей точкой в Rust поддерживают PartialOrd и Раг- tialEq, поэтому сортировать их можно, но синтаксис не такой, как для других типов. Безопасная сортировка чисел с плавающей точкой Мы можем отсортировать вектор чисел с плавающей точкой следу- ющим образом: let nut floats = vec![3.1, 1.2, 4.5, 0.3]; floats.sort_by(|a, b| a.partial_cnp(b).unwrap()); Функция partial_cnp() (которую предоставляет характеристика PartialOrd) возвращает значение типа Option. Содержащуюся в нем информацию о порядке можно получить, вызвав функцию unwrap. Распаковка пустого значения приведет к аварийному завершению программы, если два значения невозможно упорядочить, потому что в ваши данные прокралось INFINITY или NaN. Если вы уверены, что в коде не встретятся недопустимые числа с плавающей точкой, то можете просто вызвать unwrap() для резуль- тата partial_cnp и таким образом отсортировать данные функцией sort_by(). Если же недопустимое значение возможно, то можете использо- вать функцию unwr ар_ог, которая устанавливает порядок сортировки по умолчанию для нечисел: use std::cnp::Ordering::Less; let nut floats = vec![ 3.1, 1.2, 4.5, 0.3, std::f32::INFINITY, std::f32::NAN ]; floats.sort_by(|a, b| a.partial_cnp(b).unwrap_or(Less)); Этот еще более длинный вызов sort_by работает и безопасен отно- сительно недопустимых чисел, но писать такой код всякий раз, как нужно отсортировать вектор чисел с плавающей точкой, утомитель- но. Чтобы решить эту проблему, мы предлагаем удобную функцию, которую стоит включить в свой арсенал: fn float_sort<T : PartialOrd>(data: 8nut [T]) { use std::cnp::Ordering::Less; data.sort_by(|a, b| a.partial_cnp(b).unwrap_or(Less)); }
Функцию float_s?rt безопасно использовать для сортировки лю- бого среза (коллекции чисел, обычно содержимого хмассива или век- тора) чисел с плавающей точкой: let net floats = vec! [ 3.1, 1.2, 4.5, 0.3, std::f32::INFINITY, std::f32::NAN ]; fbat_sort(&nut floats); Что еще за срезы? __ Срезом (slice) в Rust называется содержимое контейне- ра. Вектор или массив, содержащий [1,2,3]. может ссы- латься на содержимое как на срез, не зная, как работает контейнер, Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Характеристика Ord https://doc.rust-lang.org/std/cmp/trait.Ord.html. Характеристика PartialOrd https://doc rust-lang.org/srd/cmp/trait.PaitialOrd.himl. Примитив f32 https://aoc.rust-lang.org/std/primitive.n2.html.
Задача 10 Обманчивый X >___________________________________________J х marks spot/src/main.rs fn main() { if 'X' == 'X' { printin! ("It notches!")', } else { printin!( "It doesn't notch. ); } } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа печатает следующий результат: It doesn't natch. Обсуждение Юникод допускает омоглифы - очень похожие или даже идентич- ные символы, кодируемые по-разному. Первая X - символ латиницы в Юникоде, имеющий код 0x58. Вторая х - греческая заглавная буква Хи, имеющая в UTF-8 код 0хСЕ 0хА7 Внимательно приглядевшись, вы увидите, что они не одинаковы, но в некоторых шрифтах - пре- жде всего Consoles в Windows - неразличимы. Омоглифы популярны в фишинговых атаках. Доменное имя bert- bank.com сайта, который просит вас изменить пароль, выглядит совершенно невинно, но буква е на самом деле кириллическая (с кодом 0xDG 0хВ5). Вы, конечно, предпочли бы не сообщать детали своей учетной записи подложному банку, по очень трудно с первого взгляда распознать неправильно написанное имя. Помощь в деле с омоглифами - уже близко В настоящее время ведется работа по включению в компилятор Rust предупреждения о том, что в коде встречаются похожие символы, способные привести к недоразумениям1. Она не идеальна, но, когда работа будет завершена, компилятор будет выдавать пред- упреждение при попытке откомпилировать такой код. Можно также использовать крейт Nettfiske, который обнаруживает вероятные обфускации в тексте. Второй неожиданный эффект омоглифов связан с длиной строки. Латинская X занимает только один байт в String (но по-прежнему 4 байта как char). Кириллическая X занимает 2 байта. Внимательно проверяйте длину полученной от пользователя строки, сравнивая ее с ожидаемым значением, или будьте готовы обрабатывать дан- ные, содержащие не только английские буквы. 1 https://rust-lang.github.io/rfcs/2457-non-ascii-idents.html.
Многоглифовые ОМОГЛИФЫ Если я вас еще недостаточно запутал, скажу, что в UTF-8 есть еще один способ сгенерировать символы с одинаковым написанием. Некоторые символы Юникода служат специально для модифика- ции следующего или предыдущего символа в строке. Например, слово Manana можно написать, используя «малую латинскую букву N с тильдой» (0хСЗ 0хВ1), или в виде man\u{0303]ana, используя символ ЦТГ-8 с кодом 0x0303. Символ 0x0303 применяет тильду к предыду- щему символу, п. Это особенно проблематично, если вы модифицируете строку. Например, при обращении строки папапа, содержащей символ лати- ницы, получится апапаи - это правильно. Но, вызвав ту же функцию .chars().rev().coUect() для обращения строки с модификатором, мы получим апапап - тильда стоит не там, где нужно. Крейт unicode- reverse поможет в этом конкретнОхМ случае, но вообще из-за этой проблемы нужно быть очень вни.мательны.м при редактировании строк в кодировке UTF-8. Символы-модификаторы часто применяются для ввода безумных текстов на таких форумах, как Reddit. Следующие примеры набраны шрифтом sans-serif, но для корректировки текста в них используют- ся модификаторы UTF-8: ЯеПо $еПо Hefc Ы ‘о
Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Статья в «Википедии» об омоглифах https://en.wikipedia.org/wiki/Homogiyph. Генератор атак с применением омоглифов https://www.irongeek.com/homoglyph-attack generator.php. Nettfiske - крейт для обнаружения омоглифов https//crates uo/crates/netifiske Крейт для обращения Юникод-строк https://crates.io/crates/unicode-reverse. Генератор странных текстов https://lingojam.com/WeirdTextGenerator.
Задача 11 Стопка боксов >________________________________________________) boxes/src/main.rs fn main() { let с = Box::new([0u32; 10_000_000]); println!( {}", c.len()); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу'.
В отладочном режиме программа аварийно завершается с сообще- нием thread 'Plain' has overflowed its stack. В выпускном режиме программа печатает сообщение 10000000. Обсуждение Для хранения 10 000 000 32-разрядных целых нужно 40 Мб памяти; это больше, чем размер стека по умолчанию. В попытке обойти это огра- ничение код использует тип Вех дчя размещения структуры в куче. Однако в отладочном режиме Rust сначала выделяет память в стеке, а затем перемещает ее в кучу, что приводит к краху' программы. Чтобы лучше разобраться в примере, посмотрим, как организо- вана память программы. В работающей программе есть две разные области памяти: стек и куча. ЧТО ТАКОЕ СТЕК? У любой программы, работающей на вашем компьютере, есть свой собственный стек - небольшая область памяти по умолчанию раз- мером до 2 Мб на каждый поток1. Размер стеков специально ограни- чен - во имя производительности. Поскольку стек мал, у него есть все шансы целиком поместить- ся в процессорный кеш. Тогда программа будет работать быстрее, а стек останется «горячим», т. е. вероятность, что он будет вытеснен из кеша или выгружен в виртуальную память, снижается. В стеке хранятся локальные переменные, параметры функций и стек вызовов - список вызванных программой функций и адресов возврата после завершения каждой функции. Стек обслуживается в соответствии с дисциплиной LIFO (last in, first out - «последним пришел, первым ушел»). Чтобы лучше понять, как это, представьте себе стопку тарелок. Последняя тарелка, кото- рую вы положите в стопку, станет первой, которую вы потом доста- нете. Добавление элемента в стек называется заталкиванием, а из- влечение верхнего элемента из стека - выталкиванием. Во время работы программы стек используется постоянно. О Всякий раз, как создается локальная переменная (существую- щая только в текущей области видимости), она заталкивается
в стек. Локальные переменные могут быть ссылками, и в этом случае в стеке сохраняется ссылка, а данные, на которые она ссылается, могут быть где угодно. О При покидании области видимости определенные в ней пере- менные выталкиваются из стека. О При вызове функции ее параметры заталкиваются в стек вме- сте с адресом возврата. К стеку прилагается удобный инструмент отладки. Любой вызов функции помещается в стек; если программа аварийно завершает- ся, то Rust обходит стек и показывает состояние программы в мо- мент краха. ЧТО ТАКОЕ КУЧА? Куча - еще одна область памяти, имеющаяся в каждой программе. Ее размер ограничен объемом памяти компьютера, виртуальной па- мяти и параметрами операционной системы. Куча велика, она мо- жет быть непрерывной или распадаться на несколько кусков, в за- висимости от операционной системы. Для сохранения данных в куче нужно больше шагов, чем для со- хранения в стеке. Во-первых, стандартная библиотека Rust должна запросить выделение памяти, и в результате будет возвращен указа- тель на область памяти. Затем этот указатель нужно будет сохранить и только потом записывать в кучу данные. Для чтения данных из кучи тоже придется потрудиться чуть боль- ше. чтобы прочитать данные, ваша программа должна сначала про- читать указатель и определить, в каком месте кучи данные хранятся. Затем уже она может прочитать их оттуда. Из-за дополнительных шагов, необходимых для чтения и запи- си, - особенно когда выделение памяти производится часто, - доступ к данным в куче значительно медленнее, чем к стеку. Почему? По- тому что процессорный кеш делает все возможное, чтобы обеспечить быстрый доступ к куче, но данные, хранящиеся в куче, обычно велики и, скорее всего, будут чаще вытесняться из стека, чем данные в стеке. Кроме того, маловероятно, что куча - в отличие от стека - расположе- на в памяти непрерывно, поэтому шансы непопадания в кеш выше. В Rust большинство контейнеров пользуются кушей. Например, тип Vec определен следующим образом1:
pub struct Vec<T, A: Allocator = Global> { buf: RawVec<T, A>, len: usize, } Вектор - это просто указатель на область памяти (ее абстрагирует RawVec) плюс длина хранимых данных. Теперь, понимая, в чем разница между стеком и кучей, рассмо- трим некоторые стратегии хранения больших структур данных в куче. Хранение больших блоков данных в куче Тип Зох представляет собой интеллектуальный указатель на память в куче, который дает естественный способ хранить в куче большие мас- сивы. К сожалению, при конструировании бокса путем вызова Box: :new сначала создается массив стеке, а затем он перемещается в кучу. Этот процесс приводит к переполнению стека и краху программы. Есть несколько методов, гарантирующих сохранение данных в куше и тем самым решающих возникшую проблему: О заменить массив типом Vec, который автоматически исполь- зует кучу; О использовать Box для обертывания указателя на кучу; О использовать оптимизации компилятора, чтобы избежать проблемы, описанной в этом примере. Начнем с того, что откажемся от использования массивов. Решение проблемы переполнения стека с помощью ВЕКТОРОВ Избежать исчерпания памяти стека можно путем использования типа Vec вместо массива. Векторы всегда выделяют память для сво- его содержимого в куче, что позволяет безопасно использовать го- раздо большие наборы данных. Следующий код выделяет память для вектора непосредственно из кучи. #[feature(box_syntax)] tn piain() { let x = vec![0u32; 10_000_000]; ]
Стек и куча в других языках Читатели, знакомые с С, C++ или другими системны- ми языками, сталкивались с подобными проблемами. В большинстве языков стек - конечный ресурс, и нуж- но тщательно взвешивать, стоит ли использовать его для хранения больших структур данных. В С проблему можно решить путем использования функции naUoc, чтобы выделить память для структуры данных. В C++ то же самое делается с помощью оператора new или ин- теллектуального указателя. Как и в случае Box, память выделяется в куче, а не в стеке. О Использование Vec сопряжено с небольшими наклад- ными расходами - хранится еще и размер вектора. Если вы не будете увеличивать однажды созданный вектор, то он будет работать в точности как массив, выделен- ный в куче, в других языках. Rust также включает сред- ства обнаружения переполнения в отладочных сборках (но исключает их в оптимизированных), что повышает безопасность на этапе разработки, но ничуть не сни- жает скорость в выпускной сборке. Использование Vec вместо массива - очень небольшая цена за дополни- тельную безопасность работы с памятью, предлагае- мую Rust. При использовании вектора расходуется несколько лишних байтов (для хранения размера), но сами операции эквивалентны. В большинстве случаев это «достаточно хорошо». Но если все-таки вы больше расположены к использованию массива, то компилятор готов прийти на помощь. Решение проблемы переполнения стека С ПОМОЩЬЮ ОПТИМИЗАЦИЙ КОМПИЛЯТОРА При выполнении примера в выпускном режиме программа работа- ет нормально. Дело в том, что LLVM (движок, на котором основан компилятор Rust) понимает, что выделенная память сразу же пере- мещается в кучу, и вообще пропускает выделение памяти в стеке. Однако есть две причины не полагаться на эту оптимизацию.
1. Не может быть стопроцентной уверенности в том, что опти- мизация будет применена; обычно она применяется, но ком- пиляторы несовершенны. 2. Вы больше не сможете выполнять программу в отладочном режиме. Вручение своей судьбы компилятору может сработать, но это не оптимально. КОхМанда разработки ядра Rust знает о проблеме и работает над ее разрешением. Новый синтаксис Box, пока еще не стабилизированный, может решить проблему раз и навсегда. Решение проблемы переполнения стека С ПОМОЩЬЮ НЕСТАБИЛЬНОЙ ВЕРСИИ RUST Вы также можете воспользоваться предлагаемой возхможностью box_syntax, отдавая себе отчет в том, что она еще не стабильна и не вошла в ядро Rust. Все согласны, что делать нужно, но мало что сделано в этом направлении, поэтому неясно, когда новая воз- можность станет частью Rust, и произойдет ли это вообще. Ниже показан предлагаемый синтаксис Box, который позволит выделять память прямо в куче: #[feature(brx_syntax)] fn main() { let x = box [0; 10 000 000]; ] Новый синтаксис пока доступен только в ветви Rust unstable. Для использования нестабильных возможностей необходимо активи- ровать комплект инструментов nightly. Если вы готовы сделать его комплектом по умолчанию (а он не всегда работает стабильно), то иЗхМените режИхМ по умолчанию командой rustup default nightly Можете также откомпилировать свою программу с помощью Car- go, добавив в командную строку флаг: cargo +nightly run
Нестабильный значит НЕ стабильный Режим nightly предлагает много будущих возможно- стей Rust, включая и такие, которые никогда не по- падут в стабильную версию. Эти возможности могут изменяться и даже исчезать. Пользуйтесь ими на свой страх и риск. Кроме того, есть канал бета-версий, который дает до- ступ к почти стабильным возможностям, включение которых в основной компилятор уже запланировано. Когда-нибудь новый синтаксис Box стабилизируется. А до тех пор выбор у вас невелик: заменить массив типом Vec, положиться на компилятор или использовать еще не стабильный синтаксис. Из этих вариантов Vec, пожалуй, лучший, потому что векторы рабо- тают очень стабильно, похожи на массивы и демонстрируют отлич- ную производительность. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Стек и куча httpsy/doc.rust-lanq.org/1.22 O/book/first-edition/the-stack-and-the-heap.html. Тип Вок https://doc.rust-lang.org/std/boxed/struct.Box.htmL hox_syntax https//doc.rust-long.org/beta/unstabLe-book/languoge-features/box-syntax.html.
Задача 12 Амнезия >_____________________________________________/ amnesia/src/main.rs fn main() { loop { let buffer = (0. .1000).collect: :<Vec<u32»(); std::mem::forget(buffer); print!("."); } } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа будет вечно печатать Обсуждение То, что программа работает бесконечно, неудивительно, потому что в цикле нет предложения break, которое бы его остановило. Странно другое - если ее запустить, то она в конечном итоге «сожрет» всю память компьютера или будет снята операционной системой. Когда вы выделяете память, забываете ссылку на нее и не освобождаете впоследствии, имеет место утечка памяти. Не менее странно то, что обычно Rust заставляет вас обертывать нарушения гарантий безопасности памяти блоком unsafe; но в дан- ном примере ничего подобного нет. Rust дает сильные - гарантированные компилятором - обеща- ния касательно безопасности работы с памятью. Но, как ни странно, утечки па мята к нарушениям таких гаран тай не относятся. На самом деле Rust даже предоставляет функции std: :mem::forget и Bex: :leak, которые явным образом вызывают утечку памяти. Утечки памяти безопасны? Функция forget когда-то носила отметку unsafe, но потом команда, за- нимающаяся ядром Rust, решила, что утечки памяти не покрываются гарантиями безопасности Rust. Хуон Уилсон (бывший член Rust Core Team) так резюмирует философию Rust в вопросе утечек памяти: «Проще говоря, небезопасность памяти - это какие-то дей- ствия с некорректными данными, а утечка памяти - это от- сутствие действий с корректными данными1». Документация по Rust добавляет: «f erget не помечена как unsafe, потому что Rust не гаранти- рует, что деструктор обязательно будет выполнен. Например, программа может создать циклические ссылки с помощью Rc или вызвать process::exit и выйти, не вызвав деструкто- ры. Таким образом, разрешение вызывать mem::forget из без- 1 http://huonw.github.io/blog/2016/04/memory-leaks-are-memory-safe/.
опасною кода фундаментально не меняет гарантий безопас- ности Rust1». Rust предлагает два способа прекратить использование перемен- ной. Первый - использовать функцию std: :nen::drop, которая гаран- тирует, что все деструкторы, реализованные с помощью характе- ристики Drop, будут выполнены. При этом память обычно очищает- ся, и этот подход является предпочтительным. Когда используется std::men::drop или переменная покидает область видимости, все Drop-деструкторы будут выполнены. Другой подход - использовать функцию std: :nem::forget, которая обходит деструктор и просто «забывает» ссылку на область памяти, на которую указывает переменная. В этом случае забытая перемен- ная все еще использует выделенную память. Хотя утечки памяти безопасны, Rust все же помогает их избежать. Rust помогает предотвращать утечки При нормальном использовании Rust по умолчанию помогает из- бежать утечек памяти. Когда переменная покидает область види- мости, автоматически вызывается ее деструктор drop. В коде ниже вектор уничтожается при выходе из области видимости: { let ny_vec = vec![100; 100]; } Этот код функционально эквивалентен следующему: let ny_vec = vec![100; 100]; std::nen::drop(ny_vec); В обоих случаях вызывается деструктор вектора. В случае вектора он безопасно освобождает выделенный буфер и не забывает вызвать droo для каждого сохраненного элемента. Автоматическая очист- ка реализована для всех встроенных типов коллекций. Аналогично встроенные в Rust типы подчищают за собой. В большинстве случаев созданные вами типы делают то же самое, но если вы хотите осво- бождать ресурсы явно, то можете реализовать характеристику Drop. 1 https://doc.rust-long.org/std/mem/fn.forgethtmL
Многим программам на Rust нет нужды думать об управлении памятью - они просто позволяют Rust уничтожать переменные при покидании области видимости. Если необходим более точ- ный контроль, то Rust предоставляет типы интеллектуальных указателей. Интеллектуальные указатели Чаще всего для создания указателя на кучу погружают переменную в обьект типа Box. Боксы - это интеллектуальные указатели, анало- гичные типу cnique_ptr в C++. Объект Box содержит указатель на свое содержимое и реализует характеристику Lrop, гарантируя, что ука- затель будет удален при выходе бокса из области видимости. Если вы явно не вызовете leak или forget для бокса, то память будет гаран- тированно освобождена. В Rust также имеется аналог типа C++ shared_ptr - указателя с под- счетом ссылок. Тип Rc обертывает данные указателем и увеличивает счетчик ссылок при его клонировании. Счетчик ссылок уменьшает- ся при вызове droD, а когда он обращается в нуль, уничтожается вся структура и ее содержимое. Rust также предоставляет тип Аге - атомарный интеллектуальный указатель с подсчетом ссылок. Атомарные переменные автомати- чески обеспечивают потокобезопасный доступ. Аге работает почти так же, как обычные указатели с подсчетом ссылок, но его безопас- но использовать в многопоточной программе. Задачи могут конку- рентно увеличивать или уменьшать счетчик ссылок, и это не приво- дит ни к гонке за данные, ни к краху программы. Rust предлагает сродства для недопущения утечек памяти, но по- чему они не сделаны обязательными? Иногда необходимо, чтобы Rust перестал управлять областью памяти. Иногда необходимо забывать Опытные разработчики наверняка скажут вам, что преднамеренная утечка памяти - идея не из лучших. Но бывает так, что переменную необходимо забыть. Например, если вы передаете управление об- ластью памяти или описателем другой программе и хотите, чтобы Rust не предпринимал никаких действий по освобождению. Допустим, вы открыли описатель файла (функцией File::cpen) и сразу же передали этот описатель другому приложению. Тогда
вы должны забыть описатель, чтобы ваша программа его не закры- ла, прекратив тем самым доступ со стороны второй программы. Другой пример - интероперабельность с другой программой. Бы выделили область памяти, - возможно, в специальном месте, к которому может обращаться несколько процессов, - а затем хотите передать управление этой областью другой программе. Вам не нуж- но, чтобы Rust очищал память, потому что теперь за это отвечает программа, перенявшая управление. Эта программа может быть даже написана не на Rust - по этой причине forget часто встречается в коде, интероперабельном с С1. Функция std::men::forget - мощный инструмент, но пользовать- ся им нужно очень осторожно. Если нет каких-то особых причин, то лучше не забывать про выделенную вами память. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ std: :тет:’.Forget https://doc.rust-lang.org/std/mem/fn.forget.html. std:’.boxed: :Вох https.//doc.rust-lang org/std/boxed/struct.Box.html. 1 https://www.ralphminderhoud.com/blog/rust-fp-wrong-woy/.
Задача 13 Измените полярность потока нейтронов reverse_poLarity/src/main.rs fn display_neutron_flow( polarity: isize) { print In!( "Neutron FIom is {}", if polarity < 0 { "reversed"} else { "normal" } ); } fn niain () { let polarity = 1; { let polarity = polarity - 2; display_neutron flow(polarity); } display neutron flow( polarity); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа напечатает следующий результат: Neutron Flow is reversed Neutron Flow us pcrnal Обсуждение Программа создает переменную polarity, а затем еще одну перемен- ную с таким же именем, но другим значением. Повторное исполь- зование имен переменных называется затенением, в сообществе разработчиков на Rust это противоречивая тема. Rust явно разре- шает создание затененных переменных, но, в отличие от многих других языков, вы не увидите при этом никакого предупреждения компилятора. Давайте сделаем шаг назад и посмотрим, что в действительности делает предложение let Dularity = 1;. Компилятор: 1) выделяет область памяти - обычно в стеке - для разхмещения ваших данных; ее размер определяется типом данных; 2) записывает значение 1 в выделенную область; 3) обновляет свой список «привязок переменных» для текущей области видимости, чтобы отразить тот факт, что pularity ссы- лается па эту1 область памяти. Компилятор не хранит имя polarity, именование переменных - это всего лишь удобство для программиста, которому гораздо про- ще запомнить имя, чем адрес в памяти. (В отладочной информации имя и ассоциация хранятся, именно благодаря этому отладчик мо- жет показать вам сведения о переменной.) Компилятор выполняет эти действия при каждом присваивании значения переменной. Если написать let polarity = 2; let polarity = 2; в одной и той же области видимости, то первая переменная продолжит существовать, но ее привязка замещается второй, так что обратиться к ней будет невозможно. На рисунке ниже показано, что происходит.
Привязка переменной polarity 1 let polarity = 1; { let polarity = polarity • 2; display nuetron_flow(pclarity); } display jiuetronjlowf polarity); Привязка переменной polarity -1 В этой программе используются правила областей видимости. Переменная polar ity привязана к новому имени во внутренней об- ласти видимости. При выходе из этой области восстанавливается привязка исходной переменной, которая связана с той же самой об- ластью памяти. Перепривязка имен переменных может привести к путанице, поэтому многие разработчики языков просят програм- мистов не пользоваться затенением переменных. Имейте в виду следующие последствия затенения: Э затенение не замещает привязку имени до своего завершения, т. е. вы можете обращаться к предыдущей версии переменной в предложении присваивания; Э создание теневой переменной никак не влияет на исходную. Вы не совершили волшебства и не сделали ее изменяемой; но- вой переменной отведено другое место в памяти. Изменилась только привязка имени, которое теперь указывает на новое значение; О затенив переменную, вы не сможете обратиться к исходной переменной, пока новая привязка не выйдет из области ви- димости; О теневая переменная не обязана быть того же типа и соблюдать те же ограничения изменяемости, что и затененная ей. Они вообще никак не связаны, это две разные переменные; О исходная (но недоступная) переменная по-прежнему зани- мает память. Если вас волнует потребление памяти, то за- тенение ничем не поможет. Компилятор Rust часто удаляет неиспользуемые переменные, но лучше точно знать, что пе- ременная удалена. Иногда затенение делает код понятнее.
Когда затенение полезно? Затенение разрешено, потому что может упростить чтение кода. Самый распространенный аргумент в пользу затенения - одно- буквенные имена переменных, которые повторно используются в функции. Нередко можно встретить такой код: let х = a+b; // Сделать что-то с х let х = c/d; // Сделать что-то с х Для одноразовых программ повторное использование Ихмен пере- менных, быть может, и оправданно, но если вам предстоит сопрово- ждать программу в течение длительного времени, то короткие име- на не лучшее проектное решение. Давайте рассмотрим следующие сценарии. О Если в будущем вы вернетесь к своему коду или если работаете вместе с другими программистами, то имя х или другое одно- буквенное мало что скажет о предназначении переменной. Согласен, более длинные имена переменных дольше наби- рать, но зато работать с таким кодом будет не в пример проще. О Если ваша функция выполняет два разных действия, то не луч- ше ли оформить каждое действие в виде отдельной функции? У каждой функции будет своя область видимости, так что вы сможете использовать одноименные переменные, не при- бегая к затенению. О Если вам ни в какую не хочется заводить разные функции, то подумайте об ограничении области видимости переменных с коротким временем жизни. Это делает очевидным любому читателю вашего кода, что х не должна существовать за преде- лами области видимости, в которой определена. Еще одно применение затенения - преобразование общих вход- ных данных в конкретный тип и повторное использование имени для ясности. Затенение преобразования типа может сделать код по- нятнее. Взгляните на следующий пример: fn exami.ne_a_string<S: T9String>(ny_string; S) -> String { let ny_string = my_string.to_string(); // Здесь какая-то сложная обработка ny_stri.ng ]
Функция oxanine_a_string() принимает любой тип, который мож- но преобразовать в строку функцией to_string(). Поместив преоб- разование в начало функции, мы сможем использовать понятное имя ny_striig на протяжении всего последующего кода и тем самым избежать потенциальных недоразумений. У нас больше нет досту- па к исходной переменной my^string, и не нужно думать о том, ка- кой конкретный тип удовлетворил общему требованию ToString, - гарантируется, что ny_string преобразовано в String. Еще один случай пользы от затенения - обеспечение внутренней изменчивости без требования изменчивости в сигнатуре функции. Рассмотрим пример: fn ny„conplex_function(base: f32, data: &[f32]) -> f32 { let nut base = if (base < 0.0) { base + 100.0 } else { base }; // Итеративно вычислить base. Здесь может быть и что-то более // полезное. data.iter().for_each(|n| base += data); base } Функция ny_ronplex_function создает изменяемую теневую копию base и пользуется ей для внутреннего вычисления. Многие про- граммисты считают, что такой код проще читать, чем включать в объявление nut base: f32 (итог будет тем же), а затененная пере- менная не выходит за пределы области видимости функции, так что возможность стать жертвой недоразумений ограничена. Этот паттерн особенно часто применяется при работе с локальным кло- ном переменной; let n = n.cl-;пе() многим кажется понятнее, чем let n_clone = n.clone(). Когда не следует использовать затенение ПЕРЕМЕННЫХ? Правила затенения переменных в Rust противоречивы и дали пред- лог для оживленной дискуссии в сети. Многие компании, посвятив- шие себя разработке на Rust, и опытные разработчики призывают вообще не использовать затенение. Если ваш код начинает услож- няться из-за затенения, то и не пользуйтесь им. Набрать новое имя переменной - всего лишь секунда, а вернувшись к отладке кода че- рез несколько месяцев и не сумев вспомнить, за каким чертом вам понадобилось это затенение, вы скажете себе спасибо.
Обнаружение затенения с помощью Clippy Если вы привыкли к С или C++, то, наверное, ожидаете, что компилятор выдаст предупреждение о затененном имени переменной. Rust сознательно разрешает зате- нение, поэтому по умолчанию никаких предупрежде- ний не выдается. Помочь может Clippy. О Факультативное правило shadow_sane может опре- делить, когда производится перепривязка пере- менной к себе самой, например: let nut х = &nut х. О Факультативное правило shadow,reuse предупре- дит о большинстве упомянутых выше паттер- нов затенения. Оно находит случаи повторного использования затененной переменной. Если вы предпочитаете в основном запретить затене- ние, то это правило - то, что вам нужно. Приве- денный в примере код генерирует именно такое предупреждение. О Факультативное правило shadow_unrelated пред- упредит о затенении переменной вообще без ис- пользования одноименной исходной переменной. Если вы хотите активировать факультативные пра- вила Clippy, то можете либо вызвать Clippy командой cargo clippy -- -W clippy: rule_nane, либо добавить ма- кросы [warn(clippy: :rule_nane)] в свой код. У макросов есть область видимости: макрос, добавленный в на- чало файла в модуле, распространяется только на этот модуль. Чтобы активировать предупреждение для все- го крейта, предпошлите ему восклицательный знак: #![warnfclippy:rule.name)]. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Области видимости и затенение (Rust на примерах) https.//doc rust-lang.org/rust-by-example/variable_bindings/scope.htmL
Задача 14 Измерение структур >_____________________________________________> structure_sizing/src/main.rs use std::mem::size_of; struct VerylnportantMessage { _message_type: u8, -destination: ul6 } ’ fn niain () { println!( "VerylnportantMessage occupies {} bytes. ", size„of::<verylnportantMesbage>() ); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа напечатает следующий результат: VerylnportantMessage occupies 4 bytes. Обсуждение Размеры _nessage_type и -destination ожидаемы: 1 и 2 байта соответ- ственно. Но почему тогда VerylnportantMessage занимает 4 байта? По умолчанию Rust дает два обещания о представлении структур в памяти: О размер структуры может отличаться от размера ее содержи- мого по причинам, связанным с производительностью; О структура может хранить данные не в том порядке, в каком они перечислены в объявлении, если оптимизатор сочтет, что так лучше. В большинстве современных процессоров данные выравнивают- ся на границу, кратную 32 битам, как в памяти, так и в кеше. До- ступ к 8-битовым (один байт) и 16-битовым (два байта) элементам осуществляется быстро, потому что процессор предоставляет для этого примитивы, а структуры можно упаковать с выравниванием на 32-битовую границу. 24-битовую (3 байта) структуру невозможно естественным об- разом выровнять па 32-битовую границу, поэтому по умолчанию Rust добавляет к ней лишние (неиспользуемые) 8 бит, чтобы обе- спечить быстрый доступ к занимаемой ей памяти. Это поведе- ние особенно полезно при работе с массивами или другими не- прерывными блоками 3-байтовых структур, потому что иначе каждая вторая структура начиналась бы с 24-го бита 32-битового блока, что снизило бы эффективность чтения памяти и исполь- зования кеша. Но иногда такое поведение может приводить к проблемам. На- пример: О если вы сохраняете очень большое количество 24 битовых структур, то потеря одного байта на каждую структуру может заметно увеличить потребление памяти, особенно во встраи- ваемых системах; О если в программе имеются куски, написанные на другом язы- ке, то этот язык может ожидать, что размер структуры будет в точности равен 24 битам. Наоборот, сервер, написанный
на Rust, может дополнять структуры, удивляя клиентов нали- чием лишних байтов; О в программе, написанной на смеси языков, переупорядочение данных в памяти компилятором Rust может вызвать странные проблемы при передаче данных между функциями на разных языках. Обуздание оптимизатора Rust Отключить обе оптимизации структур в Rust позволяет декоратор #[герг()], который дает вам контроль над представлением struct в памяти: Э #[герг(С)] говорит, что необходима интероперабельность с языком С. Rust не будет переупорядочивать поля структуры; О #[герг(packed)] требует, чтобы Rust не увеличивал размер структуры. Это может немного снизить производительность, но гарантирует, что структура будет иметь в точности размер, определяемый содержимым. Оба декоратора можно объединить. Например, структура с деко- ратором #[герг(С, packed)] не будет подвергаться ни переупорядоче- нию, ни дополнению: #[герг(С, packed)] struct ReaUyThreeBytes { a: u8, b: ul6 } fn main() { println!( ‘'ReaUyThreeBytes occupies {} bytes. , size_of: :<ReaUyThreeBytes>() ); } Эта программа напечатает: ReaUyThreeBytes occupies 3 bytes.
Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ repr(Rust) https//doc.rust-lang.org/nomicon/repr-rust.html. Размещение типа в памяти https://doc.rust-lang.org/reference/type-layout.html. Размещение структур и кортежей в памяти https://rust-lang.github.io/unsafe-code-guidelines/layout/structs-and-tuples.html.
Задача 15 И так до бесконечности >_______________________________________________/ linked_List/src/main.rs use std::cell::RefCelI; use std::rc::Rc; type Link = OptiGn<Rc<RefCell<Node»>; #[derive(Debug)] struct Node { elen: i32, next: Link, } fn main() { let mut head = Some(Rc::new( RefCell::new(Node{ elen: 1, next: None }) )); head .as_mut() .unwrapО .borrowjnut() .next = Some(Rc::new(RefCell::new( Node{ elem: 2, next: head.clone() }) )); printin!("{:?}", head); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа напечатает node 1, node 2, а затем снова node 1. Эта по- следовательность будет повторяться, пока программа не завершит- ся с сообщением thread 'main' has overflowed its stack. Обсуждение Связанные списки - одна из первых динамических структур данных, с которыми студент сталкивается при изучении информатики; кон- цептуально они очень просты: каждый элемент списка содержит ука- затель на следующий элемент1. Связанный список допускает простую вставку в середину списка - нужно лишь найти точку вставки и скопи- ровать хранящийся в ней указатель next в новый элемент, а указатель next в предыдущем элементе заменить указателем на новый элемент. Перемещать элементы списка, как видим, не пришлось. Чтобы обой- ти связанный список, нужно следовать по указателям next. Наглядно связанный список можно представить следующим образом: Поле next в каждом узле имеет тип Option и может содержать либо указатель на следующий узел, либо None, если элемент последний. Реализация связанных списков в Rust Хотя модель памяти в Rust затрудняет создание связанных списков1 2, эту трудность можно обойти, если использовать типы Rc и Ref Cell - две низкоуровневые структуры, предназначенные для повышения гибкости механизма проверки заимствования Вот что они делают' О Rc реализует подсчет ссылок. Когда вы вызываете get, чтобы получить доступ к Rc, счетчик ссылок увеличивается на 1. На- против, когда ссылка уничтожается, счетчик уменьшается на 1. 1 https://en.wikipedia.org/wiki/Linked_list. 2 https://rust-unofficial.github.io/too-many-lists/.
Если на Rc не осталось ссылок, то содержимое удаляется. Бла- годаря Rc мы можем безопасно ссылаться на содержащуюся структуру из других структур и гарантировать, что содержимое будет удал ено, когда не останется никого, кому оно нужно1; О RefCeU предоставляет изменяемую область памяти и дела- ет проверку заимствования динамической, а не статической. Когда мы заимствуем содержимое RefCeU, Rust замечает, что заимствование произошло во время выполнения, а не на эта- пе компиляции. Хотя второе изменяемое заимствование мо- жет завершиться неудачей, при этом будет иметь место ошиб- ка времени выполнения, а не времени компиляции1 2. Что такое подсчет ссылок? Если вы писали на C# или Java, то наверняка знакомы с механизмом подсчета ссылок. Это форма «сборки му- сора» - неиспользуемая память очищается а втоматиче- ски, освобождая вас от обязанности управлять памятью самостоятельно. Для работы этого механизма нужно, чтобы объект хранил счетчик переменных, «указы- вающих» или «ссылающихся на него». Когда счетчик ссылок обращается в нуль, объект никто не использует, и, следовательно, его можно безопасно удалить. Совместно Rc и RefCeU создают структуру для динамической сбор- ки мусора, как в Java или С#. Эта «дружная парочка» позволяет обра- щаться к отдельным узлам, будучи уверенной в том, что узлы будут удалены, когда в них отпадет необходимость (не остане тся ссылок). Кроме того, реализовав собственный сборщик мусора с похмощью Rc и RefCeU, вы сможете обойти проблемы, возникающие у механизма проверки заимствования Rust при работе со связанными списками. Rc и RefCeU удобны также, когда требуется создать потенциально циклические структуры данных, способные смутить гарантирован- ные Rust механизмы проверки безопасности. В этом случае вместо того, чтобы заимствовать весь список при доступе к узлу, мы можем динамически заимствовать каждый элемент по мере необходимо- сти. А благодаря подсчету ссылок хможно быть уверенным, что неис- пользуемые узлы в конечном итоге будут удалены. 1 https://doc.rust-lang.org/std/rc/struct.Rc.html. 2 https //doc.rust- lang.org/std/ccll/struct.RefCelLhtml.
Циклические ссылки Наш пример не работает, потому что в нем создается циклическая ссылка, когда поле next первого узла ссылается на второй узел, а поле next второго узла - на первый узел. Такая циклическая цепочка ссы- лок представлена на рисунке ниже. Rust не может удалить эту структуру из памяти, потому что узлы хранят ссылки друг на друга, так что у списка нет конца. Циклические ссылки являются пробле^мой и в языках со сборкой мусора. После того как циклическая цепочка ссылок образована, не- возможно сказать, когда удалить узел из памяти безопасно. В прихмере подразумеваемая по умолчанию отладочная реализация пытается перейти к следующему узлу по указателю next и напечатать его содержимое, которое ссылается на тот же узел, что приводит к его печати, и программа зацикливается. В конечном итоге у программы заканчивается хМесто в стеке, и она аварийно завершается. Эту конкретную проблему .можно решить, определив собствен- ную реализацию Debug, которая не печатает следующий узел. Не за- будьте удалить макрос #[deri ve(Debug)] из определения Node: //#irderi ve (Debug)] struct Node { elem: 132, next: Link, } use std::fmt; .npl fmt::Debig for Node { fn fmt(&self, f: &nut fmt::Formatter<'_>) -> frit::Result { write!(f, "elem: {}", self.elem) } }
Но этот модифицированный код не решает общую проблему ци- клических ссылок, а лишь предотвращает крах программы. Вообще говоря, следует избегать создания циклических струк- тур, потому что их трудно безопасно обходить и удалять. Вме- сто этого пользуйтесь стандартной библиотечной структурой std::collections::LipkedLtst1. Связанные списки Rust позаботятся о создании узлов и их связывании. Стоит ЛИ ИСПОЛЬЗОВАТЬ СВЯЗАННЫЕ списки? Связанные списки плохо перенесли старение. В современных архи- тектурах процессоров и памяти предпочтение отдается непрерыв- ным данным, а скачки по памяти туда, куда ведут указатели next, могут стать причиной задержек, вынуждая процессор загружать сле- дующий участок памяти. Связанные списки вызывают к себе проти- воречивое отношение, но исследования показывают, что Vec превос- ходит по производительности LinkeaList почти во всех тестах. Даже операция, составляющая основное преимущество списка - вставка элемента в середину списку, - оказывается медленнее вставки эле- мента в вектор с последующей сортировкой1 2. Возможно, у вас есть какая-то особая необходимость в связанном списке, но если производительность важна, то обязательно сравните его с вектором. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Rust и слишком много связанных списков https://rust-unofficial.github.io/ioo-many-lists/index.htmL Подсчет ссылок https//en.wikipedia.org/wiki/Reference_counting. 1 https //doc.rust- lang.org/std/collections/struct.LinkedList.html. 2 https //github.com/matktad/vec-vs-tist.
Задача 16 Удвой или отстань >_________________________________________________1 doublc_or_nothing/src/main.rs fn double_it(n: 132) -> 132 { n * 2 } fn double_it(n: f32) -> f32 { n * 2.0 } fn nain() { printin!("2 *4 = {}", double lt(2)); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа не откомпилируется, и будет выдано сообщение: err or[Е0428]: the папе "dodble_it" is defined multiple tines Обсуждение В C + + и подобных ему языках повторное определение функции с другими типами параметров называется перегрузкой функции. Это распространенная идиома, которая позволяет предоставлять похо- жую функциональность для разных типов, не изобретая каждый раз новое имя функции. Например, следующий код в C++ допустим: float double_it(float n) { return n * 2.0; } int <fouble_it(int n) { return n * 2; Перегрузка функций работает в C++, но не в Rust из-за декориро- вания имен1. При компиляции функции создается зависящее от ком- пилятора имя, которое компоновщик использует для связывания вызовов функции с конкретными адресами в памяти. В C++ декори- рованные имена включают имя функции и типы параметров. double_ it(float) и docble_lt(int) - разные функции. Rust декорирует только имя функции, поэтому функции, имеющие разные списки параме- тров, не могут существовать в одном и том же пространстве имен. Но не печальтесь, Rust предлагает средства для достижения той же цели. Вместо того чтобы определять функции с несколькими набо- рами параметров, Rust позволяет создать универсальную (generic) функцию, которая принимает парахметры разных типов. Универсальные функции в Rust В Rust все же можно создать функцию double_it, которая будет ра- ботать с разными типами. Но, вместо того чтобы перегружать ее, мы должны использовать механизм универсализации1 2. Это очень по- лезное средство для создания повторно используемого кода, рабо- 1 https://en.wikipedia.org/wiki/Name_mangling. 2 https://doc.rust-lang.ofg/rust-by-example/generics.html.
тающею с разными типами, но при неумеренном использовании оно становится чрезвычайно запутанным. Ниже приведена функ- ция double_it, которая работает с любым типом, поддерживающим умножение и копирование по значению: doubleornothingworking/src/main.rs ®- fn double_it<T>(n: Т) -> Т 0 where Т: std::ips::Ми1<0итрит = Т> + Fron<i32> { 0 n * 2.into() } fn main() { printin!("2^2 = {}", double it(2)); } О T добавляется как универсальный тип в сигнатуру функции, а удваиваемое число определяется как принадлежащее типу Т. 0 Механизм универсализации в Rust ограничивает множество типов, с которыми может работать универсальная функция, с помощью ключевого слова where. Требования разделяются знаком плюс. Первое ограничение требует, чтобы Т реализовывал операцию std: :ops: :Mul, т. е. он должен поддерживать оператор умноже- ния, а результат умножения должен иметь тип Т. Второе ограничение добавляется с помощью конструкции + Fron<i32>, означающей, что входной тип можно сконструи- ровать из целого числа. Если бы мы опустили это ограниче- ние, то часть * 2 не откомпилировала^ бы, потому что Rust не смог бы гарантировать, что число 2 можно преобразовать в тип, совместимый с типом параметра функции. 0 В теле функции мы воспользовались тем, что параметр п дол- жен поддерживать умножение и быть совместим с целым числом 2. Выполнить умножение можно с помощью обычных операторов Rust. Механизм универсализации в Rust очень мощный, но при первом знакомстве может отпугнуть своей сложностью. Универсальные функции и типы Rust работают путем комбинирования характери- стик, устанавливающих минимальные стандарты, которым должен удовлетворять тип, чтобы функция была к нему применима. Время, потраченное на их изучение, окупится, особенно при написании библиотечного кода. Код, в котором применяются универсальные
типы, проще использовать. В пользовательском коде больше не нуж- ны преобразования as ny_type или into() - он «просто работает». И самое главное: ваш универсальный код сохраняет все гарантии безопасности Rust, а типы, выбранные конечным пользователем, явно проверяются. Правда, за это приходится заплатить увеличе- нием времени компиляции. Зачастую результат стоит того, но если время компиляции оказывается слишком велико, подумайте о том, чтобы сократить использование универсальных функций. Интероперабельность Rust и C++ При использовании в одной программе Rust и C++ вам мо- жет не хватать перегрузки имен функций. Если функции объявляются на стороне Rust, то заводите новую функ- цию для каждой комбинации параметров, например: fn new(nane: &str)nfn new_with_age(nane &str, age: U16)1. Воспользоваться написанной на C++ библиотекой, в ко- торой требуется перегрузка имен функций, сложнее. Rust не позволит определить несколько экземпляров функции с одним и тем же именем, поэтому взаимно однозначное отображение на библиотеку, опирающую- ся на перегрузку, затруднено. Возможно, придется на- писать библиотеку-обертку, которая отображает пере- груженные функции на функции с другими именами, чтобы Rust мог их вызвать. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Универсализация (Rust на примерах) https://doc rust-lang.org/rust-by-example/generics.html. Универсализация в Rust https://doc.rust-lang.org/book/chl0-01 -syntax.html. Изучаем Rust - универсализация https://learning-rustgithub.io/docs/b4.generics.html. Язык программирования Rust - универсализация http,//web. mit.edu/iust-lang_vl.25/arch/amd64_ubuntul404/share/doc/rust/html/ book/frstedition/generics.html. 1 https://locka99.gitbooks.io/a-guide-to-porting-c-to-rust/content/features_of_rust/ polymorphism.html.
Задача 17 Какой длины вектор? vec_size/src/main.rs fn main() { let nut my_vcc = Vec::with_capacity(l); ny_vec.push("Heilo"); printin!( {} , ny_vec.capacity()); ny_vec.push( "World"'); printing {} , my_vec.capacity()); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа напечатает следующий результат: 1 4 Обсуждение В состав вектора входят две вещи: длина, показывающая, сколько элементов хранится в векторе, и буфер непрерывной памяти в куче, содержащий данные всех элементов, расположенные последова- тельно. Этот буфер часто оказывается больше, чем нужно для раз- мещения хранящихся в векторе элементов. Векторы очень похожи на массивы, но имеют переменный размер. Можно создать вектор емкости 0, вызвав nevv, или вектор с заданной пользователем емкостью, вызвав with_capacity. Емкость представля- ет полный размер вектора. При добавлении нового элемента вектор сравнивает свою длину (текущее число элементов) с емкостью. Если длина меньше, то до- бавление производится без проблем: длина вектора увеличивается на 1, а элемент помещается в первую свободную позицию. Если же места в буфере не осталось, то вектор: 1) выделяет память для нового буфера, рассчитанного на число элементов, вдвое большее текущей длины; 2) перемещает существующий буфер в начало нового; 3) освобождает старый буфер; 4) увеличивает длину на 1 и добавляет элемент в конец нового увеличенного буфера. На рисунке ниже показан рост вектора. Рост вектора кажется трудоемкой операцией, но ее результатом является весьма высокая производительность. Гарантируется, что вектор целиком расположен в непрерывной области памяти, по- этому он прекрасно «дружит» с кешем - действия с векторами очень быстрые. Но, как всегда, платой за скорость является компромисс: когда емкость вектора все-таки оказывается исчерпанной, его рас- ширение производится медленно. Тип Vec предоставляет конструктор with_capacity, который по- зволяет задать точный размер (число элементов) нового вектора. Если этот размер не будет превышен, то и перераспределять память не понадобится.
В нашем примере вызов Vec: :with_capacity(l) создает новый век- тор емкости, достаточной для размещения одного элемента. За- тем в вектор добавляется новый элемент («Hello»), так что вектор оказывается заполнен. В этот момент вектор еще не знает, придет- ся ли в будущем увеличить емкость, поэтому она остается равной 1. Затем программа помещает в вектор еще один элемент («World»), но на этот раз для него нет места, поэтому вектор добавляет еще ячейки, предполагая возможный рост в будущем, так что емкость становится равна 4. Rust не дает никаких гарантий относительно стратегии роста вектора, так что она может измениться. На момент написания кни- ги «коэффициент роста» равен 2. В большинстве случаев размер вектора удваивается, если необходима дополнительная емкость. Если вы будете последовательно добавлять в вектор много элемен- тов, то количество операций перераспределения памяти будет ве- лико, что может привести к падению производительности. Советы по работе с векторами При работе с векторами помните о следующем: О если вы примерно знаете, сколько данных нужно будет хра- нить, то используйте конструктор Vec: :with_capacity, чтобы за- резервировать нужный объем заранее, - тогда впоследствии перераспределения памяти, возможно, удастся избежать;
О если вы добавляете много данных, то попробуйте вызвать Vec:: extend, чтобы Rust знал размер добавляемых данных и пе- рераспределил память только один раз. Копирование из од- ного вектора в другой позволяет Rust выделить ровно столько памяти, сколько нужно новому вектору, потому что его длина известна. От такой оптимизации выиграет также любой ите- ратор, реализующий характеристику ExactSizelterator1. Итера- тор с произвольным раЗхМером может многократно выделять память, потому что точный размер копируемых данных зара- нее неизвестен; О добавляйте элементы в конец вектора функцией push, а не в конкретное место функцией insert. Хотя операция insert позволяет точнее контролировать расположение элементов в векторе, она гораздо медленнее push, потому что Rust должен реорганизовать вектор, освободив место для нового элемента. Если вам необходимо вставить элемент в начало, то лучше все- го воспользоваться структурой VecDeque1 2. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ std::vec::Vec https://doc.rust-lang.org/std/vec/struct.Vec.html. 1 https://doc.rust-lang.org/std/iter/trait.ExactSizelterator.html. 2 https://doc.rust-long.org/std/collections/struct.VecDeque.html.
Задача 18 Изменить неизменяемое >__________________________________________J mutablejmmutable/src/main.rs fn main() { let life_the_universe = &mut 41; *life_the_universe += 1; printin!("Life, the Universe and Everything: {}", life the_universe); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа напечатает следующий результат: Life, the Universe end Everything: 42 Обсуждение Сюрприз этой задачи в том, что переменная life_the_unxverse неизме - няема, и тем не менее нам удается изхменить ее содержимое. Чтобы понять, как такое возможно, рассмотрим следующую иллюстрацию: let lifeJihejjniverse = Smut 41; Создать неизменяемую переменную life the universe Область расположена во временной (с точки зрения времени жизни области видимости) памяти и содержит число 41 Life_the_universe содержит изменяемую ссылку на область памяти Обратите внимание на несколько примененных здесь трюков: О мы можем объявить ссылку на литерал1. В этом случае Rust создает временную область, содержащую требуемое значение, а поскольку литерал изменяемый, его хможно изменить1 2; О сама ссылка Ufe_the_universe остается неизменяемой - после того как ссылка создана, она вечно будет указывать на одну и ту же область памяти, и изменить это невозможно; О мы можем разыменовать изменяемую ссылку, воспользовав- шись операторохм *, который предоставляет изменяемый до- ступ к ее содержимому. Следующий код также работает с переменными и несколько яснее предыдущего: let nut life = 40; let the_iiniverse = &nut life; *the_universe += 2; printin!('{} the_universe); 1 https://doc.rust-lang.org/reference/expressions/literal-expr.html. 2 https://doc.rust-lang.Org/reference/expressions.html#temporary-lifetimes.
Делая life изменяемой переменной, мы ясно показываем, что ее значение в будущем может измениться. Поэтому, создавая изме- няемую ссылку на life (the_universe), мы ожидаем, что ее содержи- мое будет изменяться. Правила изменчивости и проверки заимство- вания, гарантируемые компилятором Rust, соблюдены: О life изменяема и может измениться; О the_universe неизменяема, поэтому после создания всегда бу- дет указывать на life; О разыменование the_universe позволяет изменить содержимое life; О правила проверки заимствования применимы и к изменяе- мым ссылкам. Нельзя более одного раза изменяемо заимство- вать область памяти, на которую указывает life, нельзя ссы- латься на нее, после того как она покинула активную область видимости, и нельзя изменяемо разделять ее между потоками без синхронизации. Организовать код можно любым из двух способов, но, когда изме- няемость life сделана очевидной, читать код проще. Правилам из- меняемости Rust безразлично, как организован код; он будет рабо- тать одинаково в любом случае. Паттерн изменяемой переменной - метки области памяти, - к которой обращаются другие переменные, применяется весьма часто. Заимствованная изменяемость Редко возникает ситуация, когда литерал нужно заимствовать опи- санным способом. Гораздо проще создать переменную естественно- го типа как изменяемую и работать с ней напрямую. Изменяемость вызывает меньше путаницы, а код потенциально работает быстрее, когда компилятору не нужно оптимизировать (путем устранения) разыменования. Обычно вам нет нужды напрямую объявлять содер- жимое переменной как изменяемую ссылку7. Изменяемо заимствованные переменные весьма полезны. Пере- менную х: &riut ny_type можно передавать функциям, и функция сможет изменить исходное (заимствованное) значение, а не возвра- щать новую структуру. При этом по-прежнему действует гарантия неизменяемости переменной х, потому что х - указатель на область памяти, а это значит, что мы не сможем случайно изменить сам ука- затель или адрес в памяти, на который он указывает. Однако можно изменить данные в этой области, потому что заимствование остает- ся изменяемым.
Во встраиваемых системах и в коде драйверов часто встречаются указатели и ссылки на изменяемую память. Написанный на Rust код может вести себя в этом отношении как написанный на С, но с допол- нительной защитой от случайного изменения состояния по ошибке1. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Ссылки и заимствование https://doc.rust-lang.otg/book/ch04-02-references-and-bori-owing.litmL Изменяемость (Rust на примерах) https://doc.rust-lang.org/rust-by-example/scope/borrow/mut.htmt. 1 https://docs.rust-embedded.org/book/peripherals/a-first-attempthtml.
Задача 19 Бессонница в Токио sleepless/Cargo.toml [package] папе = "sleepless" version = "в.1.6" edition = "2018" [dependencies] tokio = { version = "1.7", features = ["full"] } sleepless/src/main.rs use tokio::join; use std::tine::Duration; async fn count_and_wait(n: u64) -> u64 { pfintln! ("Starting {}", n); std: :thread: :sleep(Duration: :froni_millis(n * 100)); printin!("Returning {}", n); n } #[tokio::main] async fn main() -> Result<(), Box<dyn std::еггог::Еггог» { // join исполняет несколько задан конкурентно и возвращает управление, // когда все они завершатся. join!(count and wait(l), count and wait(2), count and wait(3)); 0k(()) } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа напечатает следующий результат: Starting 1 Returning 1 Starting 2 Returning 2 Starting 3 Returning 3 Обсуждение Результат удивляет, потому что макрос join обещает выполнять все три экземпляра count_and_wait конкурентно, но вывод показывает, что задачи выполняются последовательно - для начинающих изу- чать систему async в Rust это неожиданно. Понимание разницы меж- ду асинхронным и многопоточным программированием поможет избежать таких подводных камней и выбрать модель, подходящую для вашей программы. Асинхронные и многопоточные программы работают по-разному, и у каждого механизма ость свои плюсы и минусы. Асинхронные (основанные на Future) задачи не то же самое, что многопоточные, и для того чтобы заставить их работать конкурентно, нужно прило- жить усилия. Однако нет ничего невозможного в том, чтобы запу- стить асинхронную программу в одном потоке. На рисунке на стр. 103 показаны основные различия между мно- гопоточным и асинхронным выполнением. В многопоточной модели каждая задача работает в отдельном по- токе, пользуясь полной поддержкой со стороны операционной си- стемы. Потоки планируются независимо от других потоков и про- цессов. В асинхронной модели задачи хранятся в очереди задач и работают до тех пор, пока не уступят управление исполняемой программе. Рассмотрим несколько подходов к организации конкурентного выполнения в этом примере.
Многопоточное выполнение: Асинхронное выполнение: Запустить задачи ▼ Очередь задач Очередь задач Очередь задач Задача 1 Задача 2 Задача 3 Работает, пока не уступит Работает, пока не уступит Работает, пока не уступит Задача 2 Задача 3 > Задача 3
Платформенные потоки Потоки планируются операционной системой с вытеснением. Пока один поток приостановлен, другие продолжают работать. Чисто многопоточная версия этого примера выглядит так: asyncjhreaded/src/main.rs use std::thread; use std::time::Duration; fn count_and_wait(n: u64) -> u64 { printin!("Starting {}", n); std::thread::sleep(Duration::fron_millis(n * 100)); printin!("Returning {}", n); n } fn main() -> Result<(), Boxcdyn std::error::Error» { let a = thread::spawn(|| count_and_wait(l)); let b = thread: :spawn( 11 count_and_«iait(2)); let c = thread::spawn(|| count_and_wait(3)); a.join(),unwrap(); b.join().unwrap(); c.join().unwrap(); Ok(()) } Программа запускает три потока, и все они работают конкурент- но. Поскольку программа вызывает sleep и задерживает выполнение каждого потока, вы почти наверняка - при условии, что компьютер плотно занят работой, - увидите следующий вывод: Starting 1 Starting 2 Starting 3 Returning 1 Returning 2 Returning 3
Потоки предлагают отличную конкурентность, но не бесплат- но. Их контекст управляется операционной системой. Для запу- ска потока нужен системный вызов, а это может быть хмедленно, если требуется запускать много потоков. В разных операционных системах действуют разные ограничения, но имеется жесткий ли- мит на максимальное количество потоков - и ваша ОС, скорее все- го, не предназначена для планирования тысяч потоков одновре- менно. Кроме того, платформенный синтаксис работы с потоками может быть более громоздким, чем эквивалентный асинхронный вызов join или await. Потоки начинают работать сразу после вызова Thread: :spawn. За- тем поток работает (как планирует операционная система), пока не закончит свои дела или не получит сигнал завершения. Асинхронные задачи Асинхронные задачи планируются кооперативно. Операционная система не вмешивается, чтобы гарантировать, что каждая зада- ча получит справедливую долю процессорного времени. Задача работает, пока не уступит (yield) управление. Это действие воз- вращает управление исполнителю - коду, который отвечает за ор- ганизацию всего асинхронного механизма. Уступка управления происходит, когда: О задача возвращает результат (сообщение об ошибке или зна- чение); О задача завершает выполнение; О задача ждет одной или более задач с помощью await; О задача явно вызывает функцию yield_now(), приостанавливая себя до тех пор, пока исполнитель ее не возобновит. При правильном использовании код, основанный на асинхрон- ных задачах, может демонстрировать фантастическую производи- тельность. Особенно это относится к программам, ограниченным вводом-выводом, которые должны ждать ответа от баз данных, за- вершения операций с файлами или других процессов. Облегченные задачи отправляют запросы другим системам и ждут результата с помощью await. Очередь задач возобновляет выполнение задачи, когда запрошенные данные поступят.
Что такое исполнитель? Механизм асинхронности в Rust предоставляет все не- обходимое для организации асинхронной среды, но сам исполнитель не реализован, есть лишь вся требуемая для этого функциональность. Исполнитель отвечает за отсле- живание запущенных задач, их выполнение и предостав- ление таких сервисов, как yield. Tokio - один из самых популярных исполнителей, он пре- доставляет готовую систему, функциональности которой достаточно для наиболее типичных задач. Также популяр- ны крейты std-async и futures. Если вам нужна конкретная функциональность,то можете написать свой исполнитель. Многие исполнители распределяют задачи по оче- редям в группах потоков, но поступать именно так не обязаны. Большинство планирует несколько задач в каждом потоке (это называется организацией зе- леных потоков M:N), но асинхронная система может быть и однопоточной. На других платформах также используется эта парадиг- ма. NodeJS, ErlangTlixir и различные системы в .NET предлагают похожую функциональность. Но асинхронные задачи обеспечивают выдающуюся производи- тельность, только если вы играете по правилам и избегаете блокиру- ющих вызовов. Блокирующий вызов приостанавливает выполнение процесса и возобновляет его, когда вызов завершится. Кроме того, блокирующий вызов не уступает управление исполнителю - вызов Thread::sleep приостанавливает весь поток, включая исполнителя. Именно поэтому программа в нашем примере работает последова- тельно, хотя макрос join обещает конкурентность. Для типичной задачи засыпания Tokio предоставляет безопасный неблокирующий вызов, который позволяет приостановить задачу на заданное время. Замените вызов Thread::sleep в функции count_ and_wait таким кодом: tokio::time::sleep(Duration::from_nillis(n*100)).await; Запустив эту программу, вы увидите такой же вывод, как в много- поточной версии, - это значит, что программа работает конкурентно.
Блокирующие асинхронные задачи Иногда необходимо блокировать выполнение; например, когда долго работающая задача должна взаимодействовать с оборудованием, не предоставляющим специальной обертки для поддержки асин- хронности, или когда нужно обратиться к другой библиотеке, tokio предлагает для подобных ситуаций функцию, которая не тормозит конвейер выполнения: let blocking_task = tokio::spawn_blocking(|| { // Здесь происходил чпо-по медленное и блокирующее }); // Запуспиль задачу blocking_task.await.unwrap(); Функция spawn_blocking сообщает tokio, что ваша задача будет блокировать выполнение, и tokio запускает ее в отдельном потоке, приостанавливая текущую задачу, пока этот поток не вернет управ- ление. Ваша задача работает в фоне, а исполнитель продолжает об- рабатывать другие задачи. Заметим, что блокирующая задача по- прежнему ждет (await) возврата; Tokio разбудит родительскую зада- чу, когда блокирующая завершится. Долго РАБОТАЮЩИЕ АСИНХРОННЫЕ ЗАДАЧИ Бывает, что необходимо выполнить какое-то долгое вычисление внутри асинхронной задачи. Задача может в любой момент вызвать функцию yietd_now, чтобы приостановить свою работу и позволить поработать другим задачам. Когда планировщик вернется к этой задаче, она про- должит работать с прерванного места. Взгляните на следующий код: async fn my_big_task() { for i in 0..1_000_000 { // Выполнили какое~ло длилельное вычисление c i tokio::task::yield_now(); } } Эта задача уступает управление исполнителю после каждого шага вычисления, что сглаживает замедляющее воздействие вычисления в целом без создания отдельного потока.
Выбор между многопоточным И АСИНХРОННЫМ РЕЖИМОМ РАБОТЫ tokio и другие системы предлагают асинхронные версии большин- ства типичных операций, нуждающихся во вводе-выводе. Для чте- ния и записи файлов, подключения к базам данных и даже записи в журнал имеются версии, способные работать с исполнителем. Основанный на задачах асинхронный код может оказаться на удив- ление быстрым в программах, которые часто ждут ответа от другой системы. Веб-серверы и другие серверы могут существенно вы- играть от наличия асинхронной структуры и продемонстрировать очень высокую пропускную способноегь. Потоки больше подходят для счетных задач и задач, которые должны блокировать выполнение. С потоками сопряжены свойственные только им накладные расходы, но если многопоточная программа достаточно «тяжелая» в смысле нагрузки на процессор, то она может показать луч- шую производительность, чем система на основе асинхронных задач. Во встраиваемых системах или при написании кода, для которого про- изводительность стоит на первом месте, часто отдают предпочтение потокам, потому что они позволяют управлять параметрами плани- ровщика (и закреплять за потоком конкретный процессор) и благодаря этому давать более строгие гарантии времени выполнения. Rayon - многопоточная многозадачность Rayon - популярный крейт Rust, который реализует мно- гопоточное выполнение задач. Rayon создает пул пото- ков, которые ждут работы, а до тех пор неактивны. Когда в Rayon создастся задача, ее начинает выполнять следую- щий доступный поток. Задача выполняется независимо и не тормозит конвейер, когда встречается блокирующий вызов. Rayon может предложить лучшее из обоих миров для счетных задач: основанный на задачах синтаксис, простоту управления и низкие накладные расходы. Rayon демонстрирует очень хорошую производитель- ность, но для ограниченных вводом-выводом серверов все же уступает традиционным асинхронным систе- мам. Конечно, можно организовать смесь того и друго- го, но бы должны будете обратить пристальное внима- ние на размер пула рабочих потоков, чтобы не лишить исполнителя доступа к процессорному времени.
Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Асинхронное программирование в Rust https://rust-lQnggithub.io/async-booK/01_getting_Marted/01_chapter.htmL Будущие объекты в Rust https://github.com/rust-lang/futures-rs. Tokio https://github.com/rayon-rs/rayon. Async-Std https .//github.com/async-r s/async-std. Rayon https://github.com/rayon-rs/rayon.
Хэлло, бонжур hello_bonjodr/src/main.rs fn main() { let hello = || printin!("Hello World")', let hellc = || printin!(‘‘Bonjour le nonde"); helloQ; } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа напечатает следующий результат: Bonjour le nonde Обсуждение В задаче 16 «Удвой или отстань» мы видели, что нельзя иметь функ- ции с одинаковыми именами, даже если списки их параметров раз- личаются. Правила перегрузки функций не относятся к замыканиям (иногда их называют лямбда-функциями). У замыканий нет своих имен. Переменная, в которой хранится замыкание, является указателем на область памяти, содержащую функцию. Такие переменные подчиняются тем же правилам затене- ния, что и любые другие (см. задачу 13 «Измените полярность пото- ка нейтронов»). Декорирование имен здесь не применяется, потому что нет имени, которое можно было бы декорировать; имя замыка- ния идентифицируется указывающей на нее переменной. Мы можем создать сколько угодно затененных лямбда-функций при условии соблюдения тех же правил видимости, что и для пере- менных. После повторного объявления идентификатора, указыва- ющего на другое замыкание, первоначальная переменная оказы- вается недоступной до тех пор, пока новое объявление не покинет активную область видимости (если это когда-нибудь произойдет). Такое затенение определения замыкания не особенно полезно - оно лишь иллюстрирует тот факт, что замыкания подчиняются пра- вилам затенения переменных, а не функций. Вместо того чтобы создавать замыкание и сразу же заменять его, вы, скорее, захотите выбрать то, которое будет выполняться. Такой выбор можно осуществить на этапе компиляции с помощью стати- ческой диспетчеризации или на этапе выполнения с помощью дина- мической диспетчеризации. Статическая диспетчеризация Статическая диспетчеризация позволяет программе принимать ре- шения о поведении на этапе компиляции. В Rust есть два основных подхода к реализации статической диспетчеризации: условная ком- пиляция и константные функции. Начнем с условной компиляции.
Флаги возможностей и условная компиляция Rust поддерживает широкий спектр систем условной компиляции1. Вы можете обусловить порядок компиляции программы плат- формой, компилятором или даже переменной окружения оболоч- ки. Флаги возможностей могут быть очень полезны для настройки взаихмодействия программы с различными системами. Rust также предоставляет флаги, позволяющие настроить код под требования конечною пользователя1 2. Давайте протестируем условную компиляцию, добавив флаги возможностей в файл Cargo.toml: hello_bonjour static/Cargo.tomL [package] name = "hello_bonjour_static" version = "0.1.0" edition = "2018" [features] english = [] french = [] [dependencies] Теперь модифицируем код, так чтобы определения hello зависели от флага возможностей: hello_bonjour_static/src/main.rs fn main() { #[cfg(feature = "english")] let hellc = || printin!( 'Hello hlorld ); #[cfg(feature = "french")] let hello = || printin!('Bonjour le monde")] hello(); } При попытке выполнить эту програмхму командой cargo run она не откомпилируется, потому что функция hellc не определена. 1 https://doc.rust-lang.org/reference/conditional-compilation.html. 2 https //doc.rust- lang.org/curgo/reference/features.html.
Но можно запустить ее командой cargo run --features english, чтобы она поздоровалась по-английски, или командой cargo run - features french - чтобы по-французски. Константные функции Если вы имеете дело с постоянно расширяющимся подмножеством кода, который можно выполнить в константных функциях (const fn), то можете реализовать статическую диспетчеризацию с помощью этих функций. Например: hello_bonjour_const/src/main.rs enum Language { English, French } const fn hello(language: Language) -> Astatic str { match language { Language::English => "Hello Horld", Language::French => "Bonjour le nonde", } ] fn main() { printin!(hellolLanguage::English)); } Здесь hello() - константная функция. Она целиком вычисляется на этапе компиляции, поэтому параметр language должен поступать из константного источника. На другом конце спектра мы находим динамическую диспетче- ризацию, когда решения принимаются на этапе выполнения про- граммы. Динамическая диспетчеризация Динамическая диспетчеризация означает просто «использовать предложение match для решения о том, что делать во время выпол- нения». Приведем пример: enum Language = { English, French }; let language = Language::Engltsh;
let hello = match language { Language::English => || printing Hello Horld'), Language::French => || printin!( 'Bonjour le nonde"), }; Динамическая диспетчеризация работает медленнее, чем при- нятие решений на этапе компиляции, но ненамного. Простое пред- ложение match на современных компьютерах выполняется очень бы- стро; более сложные требуют больше времени, особенно если встре- чаются внутри цикла. Отметим, что замыкания по-прежнему ведут себя как переменные: из предложения match можно вернуть полное замыкание. Это и не удивительно, потому что замыкания и есть переменные типа Fn (для неизменяемых функций) или FnMut (для функций, которые могут изменять захваченные переменные). Ди- намический выбор замыкания - чрезвычайно удобная вешь, когда нужно настроить поток выполнения. Когда использовать динамическую ДИСПЕТЧЕРИЗАЦИЮ, А КОГДА СТАТИЧЕСКУЮ? Динамическая диспетчеризация - хорошая отправная точка, а ста- тическая очень полезна в следующих случаях: О когда нужно до минимума уменьшить размер программы, исключив возможности, которые не будут использоваться на данной целевой платформе; О когда нужно факультативно поддержать дополнительные возможности. Например, графической библиотеке может по- требоваться флаг возможностей wasm, чтобы включить под- держку Web Assembly. Использование условной компиляции необходимо, когда для определенной целевой среды нужно откомпилировать только часть кода - относящиеся к WASM функции отсутствуют при компиляции для других целей. Кроме того, условная компиляция позволяет изменить пове- дение программы при компиляции для конкретной операци- онной системы; О когда вы хотите реализовать несколько алгоритмов и прове- рить, какой из них лучше. Статическая диспетчеризация по- зволяет выбрать один на этапе компиляции без дополнитель- ных издержек в тестах производительности.
Локализация Большие приложения редко выводят текст на разных языках с помощью разных функций. Гораздо чаще все текстовые сообщения приложения определяют- ся в «языковых файлах», а нужный язык определяется в конфигурационном файле программы и подгружает- ся во время выполнения. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Возможности https://doc.rust-lang.org/cargo/reference/features.html. Константные функции https,//dOC.rUSt-lang.Org/referenCe/COnSt_eVGt.html#COnSt-funCtiOnS. Замыкания https://doc.rust-lang.org/book/chl3-01-closures.html.
Задача 21 Завязать гордиев узел gordian_knot/src/main.rs #[derive(Debug)} struct Parser<’a> { body: String, subtext : &'a str, } fn main() { let nut document = Parser { body: "Hello".to_string(), subtext: "" }; document, subtext = iidocument.body; let b = document; print In b); } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа не компилируется и печатает следующее сообщение: еггэг[ЕЭ505]: car.pot move out )f document' because it is borrowed --> gordian_knot\src\matn.rs:14:13 Обсуждение To, что этот пример не компилируется, не удивительно; зада- ние в структуре ссылок на другие части той же структуры выгля- дит подозрительно. Удивительно, что компилятор дошел почти до конца программы и только там указал на ошибку в предпо- следней строке. Структурные ссылки Хранение ссылки на структуру абсолютно законно, но необходимо добавить аннотацию времени жизни как для структуры, так и для ссылки. В данном примере время жизни структуры определено: struct Parser<'a>. Так что ее врехМЯ жизни связано с хранимой ссылкой: subtext : & а str. Синтаксис задания времени жизни таков: Вводится время жизни а, применяемое к структуре struct Parser<'a> { body: String, subtext : &'a str, } f Указывается, что ссылка, хранящаяся в subtext, пользуется временем жизни а Связывание времени жизни структуры со временем жизни ссылки помогает Rust предоставить гарантии времени жизни. Невозможно создать переменную типа Parser, если нет уверенности, что содержа- щаяся в ней ссылка проживет дольше, чем сама структура. Аннота- ции времени жизни дают возможность встроенному в Rust средству проверки времени жизни прийти вам на помощь. Вы не сможете непреднамеренно создать ссылку, уничтожить оригинальную пере- менную, а затем по ошибке использовать переменную Parser; по- пытка использовать ссылку с истекшим временем жизни приведет к ошибке компиляции.
Но не заблуждайтесь - не время жизни и не ссылки в структуре являются причиной проблемы. Как насчет автореферентного при- сваивания? Быть может, проблема в этом? Разберемся. Автореферентное присваивание В примере одной части структуры Parser присваивается ссылка на другую часть: a.subtext = 8а.body; Такие автореферентные присваивания выглядят подозрительно, но, как ни странно, код корректный и компилируется. Присваивание не нарушает правил безопасности памяти, действующих в Rust: О хранение ссылки на себя не нарушает спецификаций време- ни жизни. Ссылка на часть структуры должна жить столько же, сколько сама структура. Тут проблем нет; О body заимствуется неизменяемо и только один раз. Правила за- имствования не нарушены. Если ни правила заимствования, ни правила времени жизни не яв- ляются причиной проблемы, то почему же код не компилируется? Действительные, но неизменяемые структуры После того как переменной document присвоена автореферентная ссылка, она оказалась в необычном состоянии: по-прежнему дей- ствительна, но не может бы гь модифицирована. В результате всту- пают в игру следующие ограничения безопасности Rust: О время жизни требует, чтобы член структуры body оставался действительным. Уничтожение Ьоау обнулило бы гарантию времени жизни для subtext; О механизм проверки заимствования помнит, что subtext неиз- меняемо заимствует body. Любая попытка модифицировать body, включая изменение и уничтожение, обречена на неудачу. Как ни странно, Rust позволяет создать гордиев узел - сложную проблему, которую, кажется, невозможно разрешить. Переменная document действительна, но использовать ее почти никак нельзя. Когда полю subtext присваивается ссылка (указатель) на body - другое поле в той же структуре, - создаются две гарантии без- опасности:
Присвоить subtext ссылку на body Время жизни а subtext и Parser разделяют одь о и то же требование к Бремени жизни a Parser { body: "Hello world" a subtext: pointer-— subtext заимствует ooay Такое невинное на первый взгляд предложение let b = а вызыва- ет нарушение правила заимствования. Присваивание document пере- менной b приводит к перемещению. Содержимое памяти, в котором хранится document, перемещается в Ь. Это рассматривается как изме- нение, и механизм проверки заимствования не позволяет совершить такое перемещение, потому что document.body заимствует неизменяе- мо. В перемещении будет отказано даже в выпускном режиме сборки, когда компилятор почти наверняка исключит фактическое копиро- вание и перемещение памяти в процессе оптимизации. Правило за- имствования нарушено, потому что Parser заимствует сам у себя: Присваивание document переменной Ь: s Parser { = body: "Hello World" subtext: pointer»-----------' о } •o j Ошибка компиляции subtext заимствует body body нельзя переместить Редактировать document тоже очень трудно. Если вы попробуете из- менить document.body так: document.body = "World '.to_string(), то меха- низм проверки заимствования пометит редактирование как ошибку и не даст вам это сделать. Вы создали изменяемую переменную, до- ступную только для чтения. Также очень трудно избавиться от document. Вызов std: :mem: :drop(document) тоже считается ошибкой из-за нарушений правил заимствования. Попытки Rust уничтожить структуру блоки- руются, потому что структура уже заимствована. Так как же нам раз- вязать этот кажущийся намертво завязанным узел? Развязать гордиев узел Один из способов развязать гордиев узел - поместить его в отдель- ную область видимости. Следующий код компилируется:
fn main() { let nut э = Parser { body: ''WeZZo".to_string(), subtext: }; a.subtext = &a.body; } } По этой причине мы можем безопасно использовать авторефе- рентные структуры внутри функций. Rust знает, что вся структура исчезнет при выходе из активной области видимости, и избавляется от нее одним махом. Зачем могут понадобиться автореферентные структуры? Если в структуре хранится много данных, то хранение ссылок на ча- сти данных может быть весьма полезно. Например, парсеру, воз- можно, было бы удобно хранить ссылки на части читаемого им ис- ходного кода. Ниже приведено несколько рекомендаций, как это лучше сделать. О Попробуйте выделить автореферентные системы в отдель- ные структуры, ссылающиеся на родительскую переменную. При таком решении удаление объектов производится явно; вы можете безопасно удалить дочерний объект в любой мо- мент, но механизм защиты времени жизни в Rust по-прежнему гарантирует, что родительский объект переживет потомков. О Ограничьте использование автореферентных объектов обла- стями видимости с коротким временем жизни, которые мож- но безопасно удалить. О Если вы ссылаетесь на индексированные данные, то можете со- хранить индекс вместо прямого указателя (ссылки) на данные. О Если больше ничего не помогает, используйте подсчет ссылок (тип Rc) и слабые указатели, чтобы распутать данные1. Проверка заимствования и времени жизни иногда немного ус- ложняет код. Но это компромисс: с одной стороны, очень трудно 1 https://doc.rust-long.org/std/rc/structWeak.html.
написать опасный код. С другой стороны, иногда вы бы и рады вы- полнить операцию безопасно, но приходится изрядно потрудиться, чтобы доказать Rust, что это действительно безопасно. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Проверка ссылок с заданным временем жизни https://doc.rust-lang.org/book/chl0-03-Lifetime-syniax.himl. Ссылки и заимствование http://web.mit.edu/rust-lang_vl.25/arch/amd64_ubuntul404/share/doc/rust/html/ oook/firstedition/references-and-aorrowing.html.
Задача 22 В ожидании Годо >_____________________________________________J waitingforgodot/Cargo.toml [package] папе = "waiting_for_godot" version = "0.1.0" edition = "2018" [dependencies] tokio = { version = "1", features = ["full"] } waiting_for_godot/src/main.rs async fn heUo() { printin'("Hello, Horld!") } #[tokio::main] pub async fn main() -> Result<(), Box<dyn std::error::Error» { hello(); 0k(()) } Угадайте результат Попробуйте угадать результат, не переворачивая страницу.
Программа не печатает ничего. Обсуждение В задаче 19 «Бессонница в Токио» мы изучали планирование асин- хронных задач и узнали, что задачи исполняются исполнителем и работают до тех пор, пока добровольно не вернутся в очередь, уступив ему управление. Задача уступает управление, когда возвра- щает значение или ошибку, вызывает другую асинхронную задачу, завершает выполнение или вызывает yield_now. Задача не начинает работу, пока вы ее не вызовете. При вызове функции, декорирован- ной ключевым словом async, возвращается объект типа Future1. Вы- зов асинхронной функции не начинает ее исполнение, а обертывает для выполнения в будущем. Рассмотрим жизненный цикл асинхронной функции. 1. Создать асинхронную функцию с помощью ключевого слова async. async fn my_function() { ... } 2. При выполнении функции создается объект Future, который обертывает вашу функцию обещанием будущего выполнения. let promise = my_function() -> Возвращает Future<my_function> my-function еще не выполнена! 3. Добавить Future<my_function> в очередь задач исполнителя: Можете использовать любой из my_f unction(). await этихметодов spawn( promise) join!(promise, -other futures-) select!(promise, -other futures) Выполнение асинхронной функции - двухшаговый процесс. На нервом шаге создается обещание будущего выполнения. Разде- ление шагов позволяет гибко принимать решение о том, как именно мы хотим выполнить функцию. 1 https://doc.rust-lang.org/std/future/index.html.
Варианты асинхронного выполнения При вызове функции с ключевым словом async она не начинает вы- полняться автоматически. Вместо этого возвращается переменная, реализующая характеристику Future1. Будущие объекты - основной строительный блок асинхронного программирования. Создание Future означает, что ваша задача упакована и готова к выполнению. Имея объект Future, вы можете: О вызвать spawn, т. е. добавить задачу в очередь задач исполнителя и не дожидаться ответа; О ждать результата задачи с помощью await, уступив на это вре- мя управление другим задачам; □ использовать макросы join, чтобы выполнить н есколько задач сразу и дождаться завершения всех; О вызвать select для выполнения нескольких задач и продол- жить, когда одна из них вернет результат. Воспользуемся await для выполнения функции hello(). Выполнение Hello Мы можем заставить про грамму напечатать «Hello, World!», потре- бовав, чтобы функция main дождалась результата hello() с помощью await. Для этого нужно изменить код следующим образом: #[tokio::main] pub async fn main() -> Result<(), Box<dyn std: :error:: Error» { hello().await; 0k(()) } await требует от исполнителя tokio приостановить выполнение за- дачи и начать выполнение hello. Когда функция hello() завершится, она уступит управление функции nain - и вот тогда-то программа закончит работу. После такой модификации программа будет рабо- тать, как вы, наверное, и ожидали, и напечатает: Hello, World!
Добавления в Clippy, связанные с Future Создание Future без выполнения (путем вызова spawn или использования ключевого слова await) - типичная ошибка разработчиков, приступающих к изучению асинхронного программирования в Rust. Компилятор Rust выдает предупреждение о том, что функция не бу- дет выполнена, если не использовать await или spawn. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Асинхронное программирование в Rust https://rust-long.gnhub.io/async-book/. Объекты Future в Rust https.//doc.rust-lang.org/std/future/trait.Future.html. Демистификация замыканий, будущих объектов и async-await в Rust - часть 3: Async & Await https://medium.com/@alistairisrael/demystifying-closures-futures-and-osync-await- in-rust-part-3-async-await-9ed20eede7a4.
Константные циклы const_loop/src/mainrs const fn fib(n: ul28) -> ul28 { let nut a = 1; let nut b = 1; for _ in 2..n { let tmp = a + b; a = b; b = tmp; } b } fn main() { for i in 0..5 { printin' ("Fib {} = {}", i, fib(i)); } } Угадайте результат
Программа не компилируется со следующим сообщением об ошибке: 'for' is not allowed in a 'const fn' Обсуждение Пометка функции ключевым словом const означает, что функция должна выполняться на этапе компиляции, а не на этапе выполне- ния. В этом случае компилятор вычисляет результат заранее на ос- нове константных входов, что дает возможность ускорить сложные вычисления, которые могут понадобиться позже. Предположим, что вашей программе нужно .много чисел Фибонач- чи. Если бы функция не была константной, то пришлось бы вычис- лять их по мере необходимости и, быть может, не один раз. А кон- стантная функция позволяет сохранить эти числа в виде постоянных значений в программе, что способно резко увеличить производи- тельность последней. Константные функции - относительно новая возможность Rust, которая постепенно становится все более развитой. Однако на мо- мент публикации этой книги внутри константных функций были за- прещены следующие средства Rust1: О операции с плавающей точкой (их можно перехмещать, но нельзя производить какие-то арифметические действия); О типы динамических характеристик; О обобщенные границы параметров, за исключением Sized; О операции с простыми указателя.ми; О доступ к полям объединения (перечисления); О transmute и другие подобные операции с памятью. Циклы for тоже не разрешены, потому что требуют объекта Range, а он запрещен из-за ограничений на обобщенные границы. Потому- то приведенный в задаче код не кОхМпилируется. Впрочем, другие циклы работают без проблем. Так, наш пример можно переписать с помощью цикла while: 1 https://doc.rust-lang.org/reference/const_eval,html^const-functions.
Константные циклы 129 cunst_lcjp_works/src/main.rs const fn fib(n: ul28) -> ul28 { let nut a = 1; let mut b = 1; let nut counter = 2; while counter < n { let tmp = a + b; a = b; b = tmp; counter += 1; } b } fn main() { for i in 0..5 { printin’ ("Fib {} = {}", i, fib(i)); } } Постепенно Rust расширяет поддержку константных функций по мере совершенствования среды времени компиляции. Гарантии константности В языке C++ имеется возможность пометить функцию как constexpr. Но это лишь пожелание, а не гарантия того, что функция будет выполнена на этапе компи- ляции. Rust в этом отношении строже: константные функции должны выполняться именно во время ком- пиляции, а не обычного выполнения программы. Та- ким образом, вы точно знаете, какие ресурсы потре- бляются при выполнении функции: никаких во время выполнения, но ценой более длительной компиляции1. Механизм constexpr в C++ изначально поддерживал очень ограниченный набор операций, который посте- пенно расширяется. Rust идет по тому же пути.
Использование константных функций Как вы только что узнали, константные функции могут перенести часть вычислений на этап компиляции, что ускоряет программу. Приведем несколько ситуаций, в которых константные функции могут быть полезны. О Программы часто полагаются на результаты сложных вычис- лений с ограниченными наборами входных данных. Благода- ря константным переменным и функциям мы можем заранее построить таблицы результатов и не производить эти вычис- ления во время выполнения. О Иногда результат математических вычислений заранее изве- стен, но они все же включаются в код программы ради объяс- нения того, что она делает. Перенос этой работы на этап ком- пиляции позволяет не тратить ресурсы во время выполнения, но при этом вы сохраняете возможность подправить матема- тические действия в исходном коде. Заметим, однако, что константные функции налагают серьезные ограничения на возможные типы данных. Альтернатива - написать отдельную программу для вычисления таблицы результатов, а за- тем вставить ее в код в виде константной переменной. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Константные функции https://doc.rust-lang.Org/reference/const_eval.html#const-functions.
Дом на ранчо1 morse_code/src/main.rs fn main() { • • • • • • > } Угадайте результат Попробуйте угадать результат, не переворачивая страницу. «Дом на ранчо» - мультфильм студии Walt Disney, который в российском прокате выходил под названием «Не бей копытом». В названии главы игра слов: range означает как ранчо, так и диапазон. - Прим, перев.
Программа компилируется и выполняется, но ничего не печатает. Обсуждение Символ . в Rust употребляется в следующих случаях: О без чисел означает диапазон, содержащий все имеющиеся значения; О 1. .3 создает диапазон, который начинается числом 1 и закан- чивается числом 2; О 1. создает диапазон, начинающийся с 1 и продолжающийся до бесконечности; О .. 10 создает диапазон, включающий все числа, меньшие 10; О применительно к индексированному типу [.. ] создает значе- ние RangcFuU; О в предложениях natch и if let конструкция MyOption(field, ..) игнорирует все прочие параметры, которые не были поиме- нованы явно. В примере мы видим несколько диапазонных выражений. Сами диапазоны не указаны, но программа синтаксически корректна. Код компилируется, но очень сомнительно, что он вам когда-нибудь по- надобится. Rust разрабатывался открыто и вобрал в себя на удивление боль- шое количество выражений, которые компилируются, но при этом бесполезны. В репозитории языка Rust имеется большое собрание таких странностей1. Есть даже пример, который доводит рассматри- ваемую задачу до абсурда: fn punch_card() -> impl std::frit::Debug { ] 1 https://github.com/rust-lang/rust/blob/master/src/test/ui/weird-exprs.rs.
В каждом языке со временем накапливается подобный список не особенно полезных, но забавных синтаксических курьезов, компиляция которых преподносит сюрпризы. Искать их - тоже развлечение. Маркеры Когда я обнаружил, что может быть телом функции, у меня возникло искушение использовать эту кон- струкцию как маркер кода, который еще только пред- стоит написать. Но в Rust есть встроенное решение: макрос todo!1. Для ДОПОЛНИТЕЛЬНОГО ЧТЕНИЯ Странные выражения в Rust https://github.com/rust-long/rust/blob/master/src/test/ui/weird-exprs.rs.
Литература [KN19] Steve Klabnik and Carol Nichols. The Rust Programming Language (Covers Rust 2018). No Starch Press, San Francisco, CA, 2019. [Wol21] Herbert Wolverson. Hands-on Rust. The Pragmatic Bookshelf, Raleigh, NC, 2021.
Предметный указатель Symbols #[derive(Debug)] макрос 86 &mut конструкция 99 .NET системы 106 #[repr(C)] декоратор 81 А автореферентные структуры 119 асинхронное программирование блокирующие вызовы 106 будущие объекты 109 выполнение асинхронных функций 125 долго работающие задачи 107 исполнитель 106, 124 когда использовать 107 общие сведения 105 сравнение с многопоточным 102 асинхронные функции, выполнение 124 атомарные интеллектуальные указатели с подсчетом ссылок 70 Б базы данных внедрение SQL-кода 21 параметризованные запросы 21 безопасность внедрение SQL-кода 21 фишинговые атаки 56 блокирующие задачи 106 В векторы выделение в куче 61 преимущества 62 производительность 87 размер 94 рекомендации по использованию 95 рост 94 срез 54 внешние библиотеки, вычисления с плавающей точкой 40 время жизни 118,121 выведенная типизация 47 выпускной режим запуск программы 30 предотвращение переполнения стека 63 Г генератор странных текстов 58 ..(две точки) 132 д декорирование имен 90
динамическая диспетчеризация 114 дополнительный код 30, 32 3 заимствование 119 замыкания 112 затененные переменные когда не следует использовать 77 когда полезно использовать 76 обнаружение с помощью Clippy 78 общие сведения 74 зеленые потоки M:N 106 _ (знак подчеркивания) в или в качестве идентификатор 46 И изменяемость переменных 98 интероперабельность и размер структуры 81 исполнитель 105,124 К константные переменны 130 константные функции 114 когда использовать 130 ограничения 128 циклы 128 Л литералы, ссылки на 98 литеральные значения, проверка типа компилятором 24 локализация 116 лямбда-функции см. замыкания М массивы 54, 62, 80 многопоточная многозадачность 108 многопоточное программирование когда использовать 107 многозадачное 108 общие сведения 102 сравнение с асинхронным программированием 102 Н неиспользуемые переменные 46 О область видимости и затенение переменных 75, 78 омоглифы 56 генератор атак 58 многоглифовые 57 * оператор разыменования 98 оптимизации компилятора библиотека fastmath 39 в выпускном режиме 63 в отладочном режиме 31,44,48 для структур 81 заимствованная изменяемость 99 отладочный режим запуск программы в 31,44,48 и циклические ссылки 86 переполнение стека 60 проверки в 31,44,48 п память куча 61 стек 60 параметризованные запросы 21 перегрузка функций 90 переменные заимствованная изменяемость 99 затененные 74 неизменяемость по умолчанию 98 неиспользуемые 46 определение как изменяемой 99 разыменование 98 подсчет ссылок 85, 87
преобразование типа as ключевое слово 46 Into характеристика 25,42,48 Try!nto характеристика 25,48 нахождение ошибок 26 проверки компилятора 24 строгие типы 44,48 преобразование типа с потерей информации 24 производительность асинхронных задач 106 динамической диспетчеризации 115 и измерение структур 80 и рост вектора 94 и точность, компромисс 18 компиляции универсальных функций 92 константных функций 128 массивов и векторов 62 отладочного режима 31 потоков 103,108 связанных списков и векторов 87 стека 60 произвольная точность, библиотека 18 Р разыменования оператор (*) 98 регулярные выражения 21 С сборка мусора 85 связанные списки когда использовать 87 реализация 84 циклические ссылки 86 символы непечатаемые 20 омоглифы 56 подсчет в строках 35 представление 34 срез 54 ссылки изменяемость 98 структурные 118 статическая диспетчеризация 112 стек 60 строгие типы 44,48 строки длина 34, 56 извлечение частей 21 изменение регистра 21 полученные из stdin 20 представление 34 удаление пробельных символов 21 структуры, размер 80 тангенс 52 типы данных выведенная типизация 47 символы 34, 56 с плавающей точкой 16,24,38,52 строгие 44,48 строки 20 целые 24, 30 указатели выведенная типизация 47 интеллектуальные 62, 70 с подсчетом ссылок 70 универсальные функции 90 управляющие последовательности в строках 20 условная компиляция 112 Ф фишинговые атаки 56 флаги возможностей 113 ц целые переполнение 31
представление 30 преобразование чисел с плавающей точкой в 24 циклические ссылки 86 циклы бесконечные 68 в константных функциях 128 числа с плавающей точкой вычисления и сравнение 58 представление 16 преобразование в целые 24 сортировка 52 точность 16,38 числовое переполнение 31 А Async-Std 109 async ключевое слово 102,124 await ключевое слово 65, 70,105,125 В Box::leak функция 68 box syntax возможность 64 Box тип 60 С cargo +nightly команда 64 cargo run --features команда 114 cargo run --release команда 30 Cargo.tonil файл 11 ceil() функция 24 chars() функция 35 checked функции 32 Clippy обнаружение затененных переменных 78 предупреждения, связанные с Future 126 проверка преобразований типов 26 сравнение чисел с плавающей точкой 38 const ключевое слово 128 count() функция 35 С и C++ constexpr функции 129 интеллектуальные указатели 70 перегрузка функций 90 предупреждение о затененных переменных 78 стек и куча 62 точность вычислений с плавающей точкой в библиотеках 39 C# подсчет ссылок 85 Е EPSILON сравнение с 59 Erlang/Elixir 106 F f64 тип 17 f 128 крейт 17 fastmath библиотека 39 fixed крейт 18 float стр крейт 39 floorO функция 24 From характеристика 42 Future 102,124 futures крейт 1 Об I IEEE-754 стандарт представления чисел с плавающей точкой 16,18 Infinity ключевое слово 52 Into характеристика 25,42,48 J Java подсчет ссылок 85 join макрос 102,125 L 1еп() функция 34 LIFO (последним пришел, первым ушел) 60
М match предложение 115 N NaN (Not a Number) ключевое слово 52 Nettfiske крейт 56 NodeJS 106 О Ord характеристика 52 overflowing- функции 52 Р partial cmp() функция 53 PartialEq характеристика 53 PartialOrd характеристика 53 R Rc тип 70, 84 RefCellTnn 85 round() функция 24 rug крейт 17 s select макрос 125 sort by() функция 53 sort() функция 52 spawn() функция 125 SOL-код, внедрение 21 std-async крейт 106 std':mem::drop функция 69,120 std::mem::forget функция 68 Thread::sleep функция 106 Thread::spawn функция 105 todo! макрос 133 tokio spawnblockmg функция 107 task yield now функция 105,107 Tokio 109 исполнитель 105 tokio::time::sleep функция 106 to lowercaseO функция 21 to uppercaseO функция 21 trim() функция 20 TryFrom характеристика 43 Trylnto характеристика 25,43,48 u unicode-reverse крейт 57 unsafe блок 68 unwrap or() функция 53 unwrap() функция 53 UTF-8 кодировка символы-модификаторы 57 строк 34 V VecDeque структура данных 96 Vec:: ext end функция 96 Vec::with capacity функция 95 w Wrapping структура 31
Книги издательства «ДМК Пресс» можно купить оптом и в розницу на складе издательства по адресу: Москва, ул. Электродная, д. 2, стр. 12, офис 7, тел.+7 (499) 322-19-38, а также заказать на сайте www.dmkpress.com с доставкой в любой регион РФ. Герберт Волверсон Хорошо ли вы знаете Rust? Главный редактор Мовчан Д. А Зам. главного редактора Яценков В. С. editor@dmkpress.com Перевод Слинкин А. А. Корректор Абросимова Л. А. Верстка Луценко С. В. Дизайн обложки Мовчан А. Г. Формат 60*90 1/16. Гарнитура «РТ Serif». Печать цифровая. Усл. печ. л. 8,75. Тираж 200 экз. Геб-сайт издательства: www.dmkpress.com
Rust - последовательный язык, который старается убе- речь программиста от сюрпризов. Но, как и во всех язы- ках, в нем есть свои странности. Подборка нестандарт- ных задач в этой книге поможет вам узнать, насколько хорошо вы знаете Rust. Многие задачи взяты из собст- венного опыта автора. В основе других лежат вопросы, часто задаваемые на форумах программистов. Скорее приступайте к решению задач, охватывающих числа и текст, затенение, работу с памятью и многое другое. Попробуйте догадаться, почему программа в задаче не компилируется, дает неожиданный резуль- тат или завершается с сообщением об ошибке. Выпол- ните код и сравните свои предположения с ответом в книге. Подробные объяснения раскроют вам суть проблемы и помогут избежать ошибок в будущем. Вы узнаете, как аналогичные проблемы могут повлиять на работу ва- ших собственных программ, даже находящихся в экс- плуатации. Готовы получить уникальный опыт? Тогда прочистите мозги и приступайте к делу! Предпола1ается, что вы хоть немного знакомы с языком программирования Rust. Для проработки задач понадобится среда разработки и выполнения Rust на любой платформе. Чтобы установить Rust, зайдите на сайт https.//rustup.rs/ Pragmatic Bookshelf /Lf i .it £1 i, WWW-ДМК.рф ISBN 978-5-93700-361-4 9 785937 0Э3614 >