Язык программирования rust - github · Полезные ссылки Чаты...

354

Upload: others

Post on 26-Sep-2020

44 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи
Page 2: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Полезныессылки

Чаты Ссылки

дляобсужденияязыка,полученияпомощи gittergitter joinchatjoinchat

дляобсуждениясамойкнигиивопросовперевода gittergitter joinchatjoinchat

pullrequestsclosedinpullrequestsclosedin about6hoursabout6hours issuesclosedinissuesclosedin 3days3days

МынаХабре

ВведениекрусскоязычномупереводуЭта книга представляет собой перевод «The Rust Programming Language». Оригинал

книгирасположенздесь.

ВНИМАНИЕ!Переводокончени соответствует stable версиикнигинамоментвыходаRust1.2stable.Есливывидитенесоответствиепримеровилитекстареальномуповедениюилиоригиналукниги,пожалуйста,создайтезадачуилисразуделайтеPullRequestсисправлениями.Мынекусаемсяирадыисправлениям!:wink:

ЧитатькнигуСкачатьвPDFСкачатьвEPUBСкачатьвMOBI

Соавторам

СчегоначатьЕстьнекотороеколичествооченьпростыхпроблем.Этоопечатки,и,взяводнуизтаких

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

Небойтесьcodereview,унаснепринятонаезжатьнановичков.:smile:

ГдеполучитьпомощьУэтогорепозиторияестьчат-комнатанаGitter.Еслиувасвозниквопроспозадачеили

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

ДляопытныхПравилаперевода.

ЯзыкпрограммированияRust

2Полезныессылки

Page 3: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

От@kgv: «Хочу поблагодаритьмоих родителей:Таню иВолодю. Безнихнебылобыэтойкниги».

ОшибкиЕсливывстретилиошибкуилинеточность,пожалуйста,напишитеоней.

Ресурсы

rustbookрасположенздесьgitbookрасположенздесьgithubрепозиторийрасположенздесь

Ревизияисходногокодаданнойверсиикнигиbbcf304

ЯзыкпрограммированияRust

3Полезныессылки

Page 4: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

программированияRust. Rust — это системный язык программирования, внимание которогососредоточенонатрёхзадачах:безопасность, скоростьипараллелизм.Онрешаетэти задачибез сборщика мусора, что делает его полезным в ряде случаев, когда использование другихязыковбылобынецелесообразно:привстраиваниивдругиеязыки,принаписаниипрограммсособыми пространственными и временными требованиями, при написании низкоуровневогокода, такого как драйверы устройств и операционные системы. Во время компиляции Rustделает ряд проверок безопасности. За счёт этого не возникает накладных расходов во времявыполнения приложения и устраняются все гонки данных.Это даёт Rust преимущество наддругими языками программирования, имеющими аналогичную направленность. Rust такженаправлен на достижение «абстракции с нулевой стоимостью». Хотя некоторые из этихабстракций и ведут себя как в языках высокого уровня, но даже тогда Rust по-прежнемуобеспечиваетточныйконтроль,какделалбыязыкнизкогоуровня.

Книга «Язык программирования Rust» делится на восемь разделов. Это введениеявляетсяпервымизних.Затемидут:

Cчегоначать —НастройкакомпьютерадляразработкинаRust.Изучение Rust  — Обучение программированию на Rust на примере небольшихпроектов.ЭффективноеиспользованиеRust —ПонятияболеевысокогоуровнядлянаписаниякачественногокоданаRust.Синтаксисисемантика —КаждоепонятиеRustразбиваетсянанебольшиекусочки.Нестабильные возможности Rust  — Передовые возможности, которые пока недобавленывстабильнуюсборку.Глоссарий —Ссылкинатермины,используемыевкниге.Академическиеисследования —Литература,котораяоказалавлияниенаRust.

После прочтения этого введения, в зависимости от ваших предпочтений, вы можетепродолжитьдальнейшееизучениелибовнаправлении«ИзучениеRust», либовнаправлении«Синтаксисисемантика».Есливыпредпочитаетеизучитьязыкнапримеререальногопроекта,лучшим выбором будет раздел «Изучение Rust». Раздел «Синтаксис и семантика» подойдёттем, кто предпочитает тщательно изучить каждое понятие языка отдельно, перед тем какдвигатьсядальше.Большоеколичествоперекрёстныхссылоксоединяетэтичастивоедино.

СодействиеИсходныефайлы,изкоторыхгенерируетсяоригиналэтойкниги,могутбытьнайденына

Github:github.com/rust-lang/rust/tree/master/src/doc/trpl

ЯзыкпрограммированияRust

4Введение

Page 5: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Исходные файлы перевода этой книги на русский язык также находятся на GitHub:github.com/kgv/rust_book_ru

КраткоевведениевRustЧем же Rust может заинтересовать вас? Давайте рассмотрим несколько небольших

примеровкода,чтобыпродемонстрироватьнекоторыеизегосильныхсторон.

Основноепонятие,котороеделаетRustуникальным,называется«владение».Рассмотримследующийнебольшойпример:

Эта программа создаётсвязанное имя x . Его значением являетсяVec<T> , «вектор»,который мы создаём с помощьюмакроса, определённого в стандартной библиотеке. Этотмакрос называетсяvec , и при его вызове используется символ! . Это следует из общегопринципаRust:делатьвещиявными.Макросможетделатьзначительноболеесложныевещи,чем вызовы функций, и поэтому они визуально отличаются. Символ! также помогает приразборе,чтооблегчаетнаписаниеинструментов,аэтотожеважно.

Мы использовалиmut , чтобы сделатьx изменяемым: связанные имена в Rust поумолчаниюнеизменяемы.Дальшевпримеремыбудемизменятьэтотвектор.

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

Rustпредпочитаетвыделятьпамятьвстеке,аневкуче:x находитсянепосредственновстеке.ОднакотипVec<T> выделяетпространстводляэлементоввекторавкуче.Есливынезнакомы с различиями этих двух видов выделения памяти, можете пока простопроигнорировать эту информацию или же ознакомиться с разделом «Стек и Куча». Каксистемный язык программирования, Rust даёт вам возможность контролировать выделениепамяти.Нонебудемзабегатьвперёд,мытольконачинаемизучениеязыка.

Ранее мы упоминали, что «владение»  — это то, что делает Rust уникальным. ВтерминологииRust,x «владеет»вектором.Этоозначает,чтокактолькоx выходитизобластивидимости,выделеннаядлявекторапамятьбудетосвобождена.Когдаэтобудетпроисходить,определяетсясредствамикомпилятораRust,анечерезмеханизмынаподобиесборщикамусора.Другимисловами,вRustвыневызываетефункциивродеmalloc иfree собственноручно:компиляторстатическиопределяет,когданужновыделитьилиосвободитьпамять,ивставляетэти вызовы самостоятельно. Человек может совершить ошибку при использовании этихвызовов,акомпилятор —никогда.

Давайтедобавимещёоднустрокувнашпример:

fnmain(){letmutx=vec!["Hello","world"];}

ЯзыкпрограммированияRust

5Введение

Page 6: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мысоздаёмещёодноимя,y .Вэтомслучае,y является«ссылкой»напервыйэлементвектора. Ссылки в Rust похожи на указатели в других языках, но с дополнительнымипроверками безопасности на этапе компиляции. Ссылки взаимодействуют с системой праввладения при помощи «заимствования». Ссылки заимствуют то, на что они указывают, а неполучаютправа владенияим. Разница в том, что при заимствовании ссылка не освобождаетосновнуюпамять,когдавыходитзапределыобластивидимости.Еслибыэтобылонетак,топамятьосвобождаласьбыдвараза —плохо!

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

push   — это метод, который добавляет ещё один элемент в конец вектора. Когда мыпытаемсяскомпилироватьэтупрограмму,тополучаемошибку:

error:cannotborrow`x`asmutablebecauseitisalsoborrowedasimmutablex.push("foo");^note:previousborrowof`x`occurshere;theimmutableborrowpreventssubsequentmovesormutableborrowsof`x`untiltheborrowendslety=&x[0];^note:previousborrowendsherefnmain(){

}^

Воттак!КомпиляторRustвнекоторыхслучаяхвыдаётдостаточноподробныеошибки,иэтокакразодинизтакихслучаев.Какобъясняетсявошибке,несмотрянато,чтомыисделалинашеимяизменяемым,мывсёещёнеможемвызватьметодpush .Этопотому,чтоунасужеестьссылканаэлементвектора,y .Изменятьвектор,покасуществуетдругаяссылкананего,опасно, потому что можно сделать ссылку недействительной. В данном конкретном случае,когда мы создаём вектор, у нас есть выделенное пространство памяти только для двухэлементов.Добавлениетретьегоэлементабудетозначатьвыделениеновойобластипамятидлявсехэтихэлементов,копированиестарыхзначенийиобновлениевнутреннегоуказателянаэту

fnmain(){letmutx=vec!["Hello","world"];

lety=&x[0];}

fnmain(){letmutx=vec!["Hello","world"];

lety=&x[0];

x.push("foo");}

ЯзыкпрограммированияRust

6Введение

Page 7: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

память.Всё это работает просто отлично. Проблема заключается в том, чтоy не будетобновлена,из-зачегомыполучим«зависшийуказатель».И этоплохо.В этомслучаелюбоеиспользованиеy будетозначатьошибку.Компиляторобнаружилданнуюпроблему.

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

По умолчанию, Rust используетсемантику перемещения, поэтому, если мы хотимсделатькопиюнекоторыхданных,мыдолжнывызыватьметодclone() .В этомпримереyбольше не является ссылкой на вектор, хранящийся вx , но является копией его первогоэлемента,"Hello" . Теперь, когда у нас больше нет ссылки, методpush() прекрасноработает.

Еслинамвсёженужнассылка,тоследуетиспользоватьдругойвариант:убедиться,чтонашассылкавыходитизобластивидимости,преждечеммыпопытаемсясделатьизменения.Этовыглядитпримернотак:

Мы создали внутреннюю область видимости с помощью дополнительных фигурныхскобок.y выйдет запределыэтойобластивидимостидовызоваметодаpush() ,ипоэтомувсебудетхорошо.

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

fnmain(){letmutx=vec!["Hello","world"];

lety=x[0].clone();

x.push("foo");}

fnmain(){letmutx=vec!["Hello","world"];

{lety=&x[0];}

x.push("foo");}

ЯзыкпрограммированияRust

7Введение

Page 8: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

CчегоначатьПервый раздел книги рассказывает о том, как начать работать с Rust и его

инструментами. Сначала мы установим Rust, затем напишем классическую программу«Привет,мир!»,и,наконец,поговоримоCargo,которыйпредставляетизсебясистемусборкиименеджерпакетоввRust.

УстановкаRustПервым шагом к использованию Rust является его установка. В этой главе нам

понадобитсяинтернетсоединениедлявыполнениякоманд,спомощьюкоторыхмызагрузимRustизинтернета.

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

ПоддерживаемыеплатформыПеречень платформ, на которых работает и для которых компилирует компиляторRust

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

Платформы идентифицируются по их "целевой тройке", которая является строкой,сообщающейкомпилятору,какиевыходныеданныедолжныбытьпроизведены.Столбцынижеуказывают,работаетлисоответствующийкомпонентнауказаннойплатформе.

ПервыйуровеньПервый уровень платформ может восприниматься как "гарантировано собирается и

работает".Вчастности,каждыйизнихудовлетворяетследующимтребованиям:

Автоматическиетестыобеспечиваюттестированиеэтихплатформ.Изменения, принятые в ветку master репозиторияrust-lang/rust , прошлитестирование.Дляэтихплатформпредоставляютсяофициальныепакеты.Доступнадокументацияотомкаксобратьииспользоватьплатформу.

Target std rustc cargo notes

x86_64-pc-windows-msvc ✓ ✓ ✓ 64-bitMSVC(Windows7+)

i686-pc-windows-gnu ✓ ✓ ✓ 32-bitMinGW(Windows7+)

ЯзыкпрограммированияRust

8Cчегоначать

Page 9: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

x86_64-pc-windows-gnu ✓ ✓ ✓ 64-bitMinGW(Windows7+)

i686-apple-darwin ✓ ✓ ✓ 32-bitOSX(10.7+,Lion+)

x86_64-apple-darwin ✓ ✓ ✓ 64-bitOSX(10.7+,Lion+)

i686-unknown-linux-gnu ✓ ✓ ✓ 32-bitLinux(2.6.18+)

x86_64-unknown-linux-gnu ✓ ✓ ✓ 64-bitLinux(2.6.18+)

ВторойуровеньВторой уровень платформ может восприниматься как "гарантировано собирается".

Автоматические тесты не поддерживаются и в связи с этим работоспособность сборки негарантируется. Но эти платформы обычно работают довольно хорошо, и предложения поулучшению всегда приветствуются! В частности эти платформы удовлетворяют следующимтребованиям:

Настроенаавтоматическаясборка,нотестированиянепроисходит.Изменения, принятые в ветку master репозиторияrust-lang/rust , собираютсядля этих платформ.Имейте ввиду, что для некоторых платформ собирается толькостандартнаябиблиотека,нодляостальныхнастроенаполнаяраскруткакомпилятора(bootstraping).Дляэтихплатформпредоставляютсяофициальныепакеты.

Target std rustc cargo notes

i686-pc-windows-msvc ✓ ✓ ✓ 32-bitMSVC(Windows7+)

x86_64-unknown-linux-musl ✓ 64-bitLinuxwithMUSL

arm-linux-androideabi ✓ ARMAndroid

arm-unknown-linux-gnueabi ✓ ✓ ARMLinux(2.6.18+)

arm-unknown-linux-gnueabihf ✓ ✓ ARMLinux(2.6.18+)

aarch64-unknown-linux-gnu ✓ ARM64Linux(2.6.18+)

mips-unknown-linux-gnu ✓ MIPSLinux(2.6.18+)

mipsel-unknown-linux-gnu ✓ MIPS(LE)Linux(2.6.18+)

ТретийуровеньТретийуровеньплатформ—этоте,которыеRustподдерживает,нопринятыеизменения

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

Target std rustc cargo notes

i686-linux-android ✓ 32-bitx86Android

ЯзыкпрограммированияRust

9Cчегоначать

Page 10: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

aarch64-linux-android ✓ ARM64Android

powerpc-unknown-linux-gnu ✓ PowerPCLinux(2.6.18+)

i386-apple-ios ✓ 32-bitx86iOS

x86_64-apple-ios ✓ 64-bitx86iOS

armv7-apple-ios ✓ ARMiOS

armv7s-apple-ios ✓ ARMiOS

aarch64-apple-ios ✓ ARM64iOS

i686-unknown-freebsd ✓ ✓ 32-bitFreeBSD

x86_64-unknown-freebsd ✓ ✓ 64-bitFreeBSD

x86_64-unknown-openbsd ✓ ✓ 64-bitOpenBSD

x86_64-unknown-netbsd ✓ ✓ 64-bitNetBSD

x86_64-unknown-bitrig ✓ ✓ 64-bitBitrig

x86_64-unknown-dragonfly ✓ ✓ 64-bitDragonFlyBSD

x86_64-rumprun-netbsd ✓ 64-bitNetBSDRumpKernel

i686-pc-windows-msvc (XP) ✓ WindowsXPsupport

x86_64-pc-windows-msvc (XP) ✓ WindowsXPsupport

Имейте ввиду, что эта таблица со временем может быть дополнена, это неисчерпывающийнабортретьегоуровня!

УстановканаLinuxилиMacЕсли вы используете Linux или Mac, то всё что вам нужно сделать — это ввести

следующуюкомандувконсоль:

$curl-sSfhttps://static.rust-lang.org/rustup.sh|sh

Эта команда загрузит скрипт и начнет установку. Если все пройдет успешно, то выувидитеследующийтекст:

WelcometoRust.

ThisscriptwilldownloadtheRustcompileranditspackagemanager,Cargo,andinstallthemto/usr/local.Youmayinstallelsewherebyrunningthisscriptwiththe--prefix=<path>option.

Theinstallerwillrununder‘sudo’andmayaskyouforyourpassword.Ifyoudonotwantthescripttorun‘sudo’thenpassitthe--disable-sudoflag.

Youmayuninstalllaterbyrunning/usr/local/lib/rustlib/uninstall.sh,orbyrunningthisscriptagainwiththe--uninstallflag.

Continue?(y/N)

Нажмитеy дляподтвержденияиследуйтедальнейшимподсказкам.

ЯзыкпрограммированияRust

10Cчегоначать

Page 11: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

УстановканаWindowsЕсливыиспользуетеWindows,тоскачайтеподходящийустановщик.

УдалениеУдалить Rust так же просто, как и установить его. На Linux или Mac нужно просто

запуститьскриптудаления:

$sudo/usr/local/lib/rustlib/uninstall.sh

Если вы использовали установщик Windows, то просто повторно запустите.msi ,которыйпредложитвамвозможностьудаления.

РешениепроблемЕслиувасустановленRust,томожнооткрытьтерминаливвести:

$rustc--version

Выдолжныувидетьверсию,хешкоммитаидатукоммита.

Еслиэтотак,тотеперьувасестьустановленныйRust!Поздравляем!

Если нет и вы пользовательWindows, то убедитесь в том, что Rust прописан в вашейсистемнойпеременной%PATH%.Еслиэтонетак,тозапуститеустановочникснова,выберете"Change" на старнице "Change, repair, or remove installation" и убедитесь, что "Add to PATH"указываетналокальныйжесткийдиск.

Существуютнесколькомест, гдевыможетеполучитьпомощь.Самыйпростойвариант—Канал#rustнаirc.mozilla.org,ккоторомувыможетеподключитьсячерезMibbit.Нажмитенаэту ссылку, и вы будете общаться в чате с другими Rustaceans (это дурашливое прозвище,которыммысебяназываем),имыпоможемвам.Другиеполезныересурсы,посвящённыеRust:форумпользователейиStackOverflow.Русскоязычныересурсы:сайтсообщества,форум,StackOverflow.

Установщик также устанавливает документацию, которая доступна без подключения ксети.НаUNIXсистемахонарасполагаетсявдиректории/usr/local/share/doc/rust .В Windows используется директорияshare/doc , относительно того куда вы установилиRust.

Привет,мир!Теперь, когда вы установили Rust, давайте напишем первую программу на Rust.

Традиционно при изучении нового языка программирования, первая написанная программапростовыводитнаэкран«Привет,мир!»,имыследуемэтойтрадиции.

ЯзыкпрограммированияRust

11Cчегоначать

Page 12: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

На самом деле это приводит к ещё одной проблеме, о которой мы должныпредупредить:данноеруководствопредполагает,чтоувасестьбазовыенавыкиработыскомандной строкой. Rust не выдвигает специфических требований к вашей средеразработкиилитому,каквыхранитесвойкод.ЕсливыпредпочитаетеиспользоватьIDE,посмотритенапроектSolidOak,илинаплагиныквашейлюбимойIDE.Естьмножестворасширений, разрабатываемых сообществом, а такжеплагинов для разных редакторов,поддерживаемых командой Rust. Настройка вашего редактора или IDE выходит запределы данного руководства. Посмотрите руководство по использованию выбранноговамиплагина.

СозданиепроектаПервое,счегомыдолжныначать–созданиефайладлянашегокода.ДляRustнеимеет

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

$mkdir~/projects$cd~/projects$mkdirhello_world$cdhello_world

ЕсливыиспользуетеWindowsинеиспользуетеPowerShell, ~можетнеработать.Обратитеськдокументациивашейоболочкидляуточнениядеталей.

НаписаниеизапускпрограммынаRustТеперьсоздадимновыйфайлдлякодапрограммы.Назовёмнашфайлmain.rs.Файлыс

исходнымкодомнаRust всегдаимеютрасширение.rs.Есливыхотитеиспользоватьвименивашегофайлабольшеодногослова,разделяйтеихподчёркиванием;напримерhello_world.rs,анеhelloworld.rs.

Теперьоткройтетолькочтосозданныйфайлmain.rsидобавьтевнегоследующийкод:

Сохраните файл и вернитесь к вашему окну терминала. На Linux или OSX введитеследующиекоманды:

fnmain(){println!("Привет,мир!");}

ЯзыкпрограммированияRust

12Cчегоначать

Page 13: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$rustcmain.rs$./mainПривет,мир!

НаWindows просто заменитеmain наmain.exe .В независимости от вашейОС выдолжны увидеть строкуПривет, мир! в терминале. Поздравляем! Вы написали первуюпрограммунаRust.ТеперьвыRust-разработчик!Добропожаловать!

АнатомияпрограммнаRustТеперь давайте детально разберемся, что происходит в программе "Привет, мир!". Вот

первыйкусочекголоволомки:

Этистрокиобъявляют«функцию»вRust.Функцияmain особенна:этоначалокаждойпрограммы на Rust. Первая строка говорит: «Мы объявляем функцию, именуемуюmain ,которая не получает параметров и ничего не возвращает». Если бы мы хотели передать вфункцию параметры, то указали бы их в скобках (( и) ). Поскольку нам не надо ничеговозвращатьизэтойфункции,мыможемопуститьуказаниетипавозвращаемогозначения.Мывернёмсякэтомупозже.

Вы должны были заметить, что функция обёрнута в фигурные скобки ({ и} ). Rustтребует оборачивать ими тело любой функции. Также хорошим стилем считается ставитьоткрывающуюфигурнуюскобкунатойжестроке,чтоиобъявлениефункции,отделённуюотнегооднимпробелом.

Теперьэтастрока:

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

Теперь разберёмся сprintln!() . Это вызов одного измакросов, которымипредставлено метапрограммирование в Rust. Если бы вместо макроса была функция, этовыглядело бы следующим образом:println() (без! ). Позже мы обсудим макросы Rustподробнее, а на данныймомент все что вам нужно знать: если вы видите! , то вызываетсямакросвместообычнойфункции.

Идёмдальше."Привет,мир!"  —это«строка».Строки—этоудивительносложнаятемадлясистемногоязыкапрограммирования.Этостатическирасположеннаявпамятистрока.Мы передаём строку в качестве аргумента вprintln! , который выводит строки на экран.Достаточнопросто!

fnmain(){

}

println!("Привет,мир!");

ЯзыкпрограммированияRust

13Cчегоначать

Page 14: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Cтроказаканчиваетсяточкойсзапятой(; ).Rust—языксориентациейнавыражения,аэто означает, что в нём большая часть вещей является выражением.; используется дляуказания конца выражения и начала следующего. Большинство строк кода на Rustзаканчиваетсясимволом; .

КомпиляцияизапускэтоотдельныешагиВразделе"НаписаниеизапускпрограммынаRust"мырассмотреликакзапуститьтолько

чтосозданнуюпрограмму.Теперьмыразберемкаждыйшагпоотдельности.

Перед запуском программы ее нужно скомпилировать. Вы можете воспользоватьсякомпиляторомRustспомощьюкомандыrustc ипередатьвашфайл,какпоказаноздесь:

$rustcmain.rs

ЕслираньшевыпрограммировалинаСилиС++,тозаметите,чтоэтонапоминаетgccилиclang .ПослеуспешнойкомпиляцииRustсоздастдвоичныйисполняемыйфайл.НаLinuxилиOSXвыможетеубедитьсявэтомспомощьюкомандыls :

$lsmainmain.rs

ИливWindows:

$dirmain.exemain.rs

У нас есть два файла: файл с нашим исходным кодом, с расширением.rs , иисполняемыйфайл (main.exe в Windows,main в остальных случаях). Все что осталосьсделать—этозапуститьmain илиmain.exe :

$./main#илиmain.exeнаWindows

Мывывелинаштекст"Привет,мир!" вокнетерминала.

Если раньше вы использовали динамические языки программирования вроде Ruby,PythonилиJavaScript,товозможноразделениекомпиляцииизапускапокажетсявамстранным.Rust—этоязык,накоторомпрограммыкомпилируютсяпередисполнением.Этоозначает,чтовыможете собрать программу, дать её кому-то ещё, и емуне нужно устанавливатьRust длязапуска этой программы. Если вы передадите кому-нибудь.rb , .py или.js файл, импонадобится интерпретатор Ruby, Python или JavaScript чтобы скомпилировать и запуститьвашу программу (это делается одной командой). В мире языков программирования многокомпромиссов,иRustсделалсвойвыбор.

Использоватьrustc удобнолишьдлянебольшихпрограмм,нопомереростапроекта,потребуетсяинструмент,которыйпоможетуправлятьнастройкамипроекта,атакжепозволяетпроще делиться кодом с другими людьми и проектами. Далее мы познакомимся с новыминструментомCargo ,которыйиспользуетсядлянаписаниянастоящихпрограммнаRust.

ЯзыкпрограммированияRust

14Cчегоначать

Page 15: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Привет,Cargo!Cargo—этосистемасборкиипакетныйменеджердляRust,иRustaceansиспользуютего

дляуправлениясвоимипроектаминаRust.Cargoзаботитсяотрехвещах:сборкакода,загрузкабиблиотек,откоторыхзависитвашкод,исборкаэтихбиблиотек.Библиотеки,которыенужнывашему коду, мы называем "зависимостями" ("dependencies"), поскольку ваш код зависит отних.

Поначалу вашей программе не понадобится никаких зависимостей, поэтому будемиспользоватьтолькопервуючастьеговозможностей.Современемнампонадобитсядобавитьнесколькозависимостей,инамнесоставиттрудасделатьэто,используяCargo.

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

$cargo--version

Если вы увидели номер версии, то все в порядке. Если же вы увидели сообщение обошибкенаподобии"команданенайдена ", то вамнужноознакомится сдокументациейдлясистемы,вкоторойвыустановилиRust.

ПереходнаCargoДавайтепереведемнашпроект"Привет,мир"наиспользованиеCargo.Дляпереходана

Cargoнужносделатьтривещи:

1. Расположитьфайлсисходнымкодомвправильнойдиректории.2. Избавитьсяотстарогоисполняемогофайла(main.exe илиmain )исделатьновый.3. СоздатьконфигурационныйфайлдляCargo.

Давайтесделаемэто!

Создание нового исполняемого файла и директории сисходнымкодом

Дляначалавернитеськвашемутерминалу,перейдитеввашудиректориюhello_worldивведитеследующиекоманды:

$mkdirsrc$mvmain.rssrc/main.rs$rmmain#или'delmain.exe'дляWindows

ЯзыкпрограммированияRust

15Cчегоначать

Page 16: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Cargo ожидает что ваши файлы с исходным кодом находятся в директорииsrc. Такойподход оставляет верхний уровнь вашего проекта для вещей, вроде README, файлов стекстомлицензииидругихнеотносящихсяквашемукоду.Cargoпомогаетнамсохранятьнашипроектыкрасивымииаккуратными.Всемуестьсвоеместоивсенаходитсянасвоихместах.

Теперьскопируйтеmain.rsвдиректориюsrcиудалитескомпилированныйфайл,которыйвысоздалиспомощьюrustc .

Отметим, что поскольку мы создаём исполняемыйфайл, то мы используемmain.rs .Если бымы хотели создать библиотеку, то мы использовали бы lib.rs. Cargo использует этосоглашение для успешной компиляции вашего проекта, но вы можете это изменить, еслизахотите.

СозданиеконфигурационногофайлаТеперь создайте новый файл внутри директорииhello_world и назовите его

Cargo.toml .

Убедитесьвтом,чтоимяправильное:вамнужназаглавнаяC !ВпротивномслучаеCargoненайдетконфигурационныйфайл.

ЭтофайлвформатеTOML.TOMLэтоаналогINI,носнекоторымидополнениями,иониспользуетсявконфигурационныхфайлахдляCargo.

Вставьтеследующуюинформациювнутрьэтогофайла:

[package]

name="hello_world"version="0.0.1"authors=["Yourname<[email protected]>"]

Первая строка,[package] , говорит о том, что следующие параметры отвечают занастройку пакета. Когда нам понадобится добавить больше информации в этот файл, мысоздадимдругиеразделы,носейчаснамдостаточнонастроекпакета.

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

ПослетогокаквыдобавилиэтуинформациювCargo.toml,сохранитеизменения.Наэтомсозданиеконфигурационногофайлазавершено.

СборкаизапускCargoпроектаТеперь, после создания файлаCarto.toml в корневой директории, мы готовы

приступить к сборке и запуску нашего проекта. Чтобы сделать это, введите следующиекоманды:

ЯзыкпрограммированияRust

16Cчегоначать

Page 17: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$cargobuildCompilinghello_worldv0.0.1(file:///home/yourname/projects/hello_world)$./target/debug/hello_worldПривет,мир!

Та-да! Мы собрали наш проект вызвавcargo build и запустили его с помощью./target/debug/hello_world . Мы можем сделать это в один шаг используяcargorun :

$cargorunRunning`target/debug/hello_world`Привет,мир!

Заметьте,чтосейчасмынепересобралинашпроект.Cargoпонял,чтомынеизменилифайлсисходнымкодомисразузапустилисполняемыйфайл.Еслибымыизменилифайл,мыбыувиделиобашага:

$cargorunCompilinghello_worldv0.0.1(file:///home/yourname/projects/hello_world)Running`target/debug/hello_world`Привет,мир!

Напервыйвзглядэтокажетсясложнее,посравнениюсболеепростымиспользованиемrustc ,нодавайтеподумаемобудущем:есливнашемпроектебудетбольшеодногофайла,мы должны будем вызывать rustc для каждого из них и передавать кучу параметров, чтобысобрать их вместе. С Cargo, когда наш проект вырастет, нам понадобится вызвать толькокомандуcargobuild ионавсёсделаетзанас.

СборкарелизнойверсииКогда вы закончите работать над проектом, и он окончательно будет готов к релизу,

используйте командуcargo build --release для компиляции вашего проекта соптимизацией.ЭтиоптимизацииделаютвашкоднаRustбыстрее,нотребуютбольшевременина компиляцию. Именно из-за этого существует два разных способа: один для разработки,другойдлясборкифинальнойверсии,которуювыотдалитепользователям.

Такжевыдолжныбылизаметить,чтоCargoсоздалновыйфайл:Cargo.lock .

[root]name="hello_world"version="0.0.1"

Этот файл используется Cargo для отслеживания зависимостей в вашем приложении.Прямо сейчас у нас нет ни одной, поэтому этот файл немного пустоват. Вам не нужноредактироватьэтотфайлсамостоятельно,Cargoсамснимразберётся.

Вотивсе!Мыуспешнособралиhello_world спомощьюCargo.

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

ЯзыкпрограммированияRust

17Cчегоначать

Page 18: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

спомощьювариацииэтихкоманд:

$gitclonesomeurl.com/foo$cdfoo$cargobuild

ПростойспособсоздатьновыйCargoпроектВамне нужно повторять вышеприведённыешаги каждый раз, когда вы хотите создать

новый проект! Cargo может создать директорию проекта, в которой вы сразу сможетеприступитькразработке.

ЧтобысоздатьновыйпроектспомощьюCargo,нужноввестикомандуcargonew :

$cargonewhello_world--bin

Мы указываем аргумент--bin , так как хотим создать исполняемуюпрограмму.Еслимы не укажем этот аргумент, тоCargo создаст проект для библиотеки.Исполняемыефайлычастно называютбинарниками (поскольку обычно они находятся в/usr/bin , если выиспользуетеUnixсистему).

Cargo сгенерировал два файла и одну директорию:Cargo.toml и директориюsrc сфайломmain.rs.Онидолжнывыглядетьтакжекакте,чтомысоздалидоэтого.

Этого достаточно для того чтобы начать. ОткрывCargo.toml , вы должны увидетьследующее:

[package]

name="hello_world"version="0.1.0"authors=["YourName<[email protected]>"]

Cargo наполнил этот файл значениями по умолчанию на основании переданныхаргументов и глобальной конфигурацииgit . Также он инициализировал директориюhello_world какgit репозитоий.

Вотчтодолжнобытьвнутриsrc/main.rs :

Cargoсоздал«HelloWorld!»длянасивыужеможетеприступитькпрограммированию!

У Cargo есть собственноеруководство, в котором про него рассказано болеедетально.

Заключение

fnmain(){println!("Hello,world!");}

ЯзыкпрограммированияRust

18Cчегоначать

Page 19: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Увасестьдвапути:погрузитьсявизучениереальногопроекта,открывраздел«ИзучениеRust»,илиначатьссамогонизаипостепеннопродвигатьсянаверх,начавсраздела«Синтаксиси семантика». Программисты, имеющие опыт работы с системными языками, вероятно,предпочтут «Изучение Rust», в то время как программисты, имеющие опыт работы сдинамическимиязыками,скореевсегозахотятпойтиповторомупути.Разныелюдиучатсяпо-разному!Выберитето,чтоподходитименновам.

ЯзыкпрограммированияRust

19Cчегоначать

Page 20: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ИзучениеRustДобро пожаловать! Этот раздел книги содержит несколько глав, которые научат вас

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

Если вы хотите более основательно изучить язык, читайте раздел «Синтаксис исемантика».

ЯзыкпрограммированияRust

20ИзучениеRust

Page 21: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

УгадайкаВ качестве нашего первого проекта, мы решим классическую для начинающих

программистов задачу: игра-угадайка. Немного о том, как игра должна работать: нашапрограмма генерирует случайное целое число из промежутка от 1 до 100. Затем она проситввести число, которое она «загадала». Для каждого введённого нами числа, она говорит,большелионо,чем«загаданное»,илименьше.Игразаканчиваетсякогдамыотгадываемчисло.Звучитнеплохо,нетакли?

СозданиеновогопроектаДавайтесоздадимновыйпроект.Перейдитеввашудиректориюспроектами.Помните,

как мы создавали структуру директорий иCargo.toml дляhello_world? Cargo можетсделатьэтозанас.Давайтевоспользуемсяэтим:

$cd~/projects$cargonewguessing_game--bin$cdguessing_game

Мы сказали Cargo, что хотим создать новый проект с именемguessing_game .Припомощифлага--bin ,мыуказаличтохотимсоздатьисполняемыйфайл,анебиблиотеку.

ДавайтепосмотримсгенерированныйCargo.toml :

[package]

name="guessing_game"version="0.1.0"authors=["YourName<[email protected]>"]

Cargo взял эту информацию из вашего рабочего окружения. Если информация некорректна,исправьтееё.

Наконец,CargoсоздалпрограммуПривет,мир! .Посмотритефайлsrc/main.rs :

ДавайтепопробуемскомпилироватьсозданныйCargoпроект:

$cargobuildCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)

Замечательно! Снова откройтеsrc/main.rs .Мы будем писать весь наш код в этомфайле.

Прежде, чем мы начнём работу, давайте рассмотрим ещё одну команду Cargo:run .cargo run похожа наcargo build , но после завершения компиляции, она запускаетполучившийсяисполняемыйфайл:

fnmain(){println!("Привет,мир!")}

ЯзыкпрограммированияRust

21Угадайка

Page 22: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$cargorunCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)Running`target/debug/guessing_game`Привет,мир!

Великолепно! Командаrun помогает, когда надо быстро пересобирать проект. Нашаигракакразиестьтакойпроект:намнадобыстротестироватькаждоеизменение,преждечеммыприступимкследующейчастипрограммы.

ОбработкапредположенияДавайте начнём! Первая вещь, которую мы должны сделать для нашей игры  — это

позволитьигрокувводитьпредположения.Поместитеследующийкодввашsrc/main.rs :

Здесьмногочего!Давайтеразберёмэтотучастокпочастям.

Намнадополучитьто,чтоввёлпользователь,азатемвывестирезультатнаэкран.Значитнам понадобится библиотекаio из стандартной библиотеки. Изначально, вовступлении(prelude),Rustимпортируетвнашупрограммулишьсамыенеобходимыевещи.Есличего-тонетповступлении,мыдолжныуказатьприпомощиuse ,чтохотимэтоиспользовать.

Каквыужевиделидоэтого,функцияmain()  —этоточкавходавнашупрограмму.fnобъявляет новую функцию. Пустые круглые скобки() показывают, что она не принимаетаргументов.Открывающаяфигурная скобка{ начинаеттелонашейфункции.Из-затого,чтомы не указали тип возвращаемого значения, предполагается, что будет возвращаться()  —пустойкортеж.

Мыужеизучили,чтоprintln!()  —этомакрос,которыйвыводитстрокинаэкран.

usestd::io;

fnmain(){println!("Угадайтечисло!");

println!("Пожалуйста,введитепредположение.");

letmutguess=String::new();

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

println!("Вашапопытка:{}",guess);}

usestd::io;

fnmain(){

println!("Угадайтечисло!");

println!("Пожалуйста,введитепредположение.");

ЯзыкпрограммированияRust

22Угадайка

Page 23: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Теперь интереснее! Как же много всего происходит в этой строке! Первая вещь, накоторую следует обратить внимание —выражение let, которое используется длясозданиясвязи .Оновыглядиттак:

Это создаёт новую связь с именемfoo и привязывает ей значениеbar . Во многихязыках это называетсяпеременная , но в Rust связывание переменных имеет несколькотрюковврукаве.

Например,поумолчанию,связинеизменяемы.Поэтойпричиненашпримериспользуетmut :этотмодификаторразрешаетменятьсвязь.Слевойстороныуlet можетбытьнепростоимясвязи,аобразец.Мыбудемиспользоватьихдальше.Ихдостаточнопростоиспользовать:

Ах да,// начинает комментарий, который заканчивается в конце строки. Rustигнорируетвсё,чтонаходитсявкомментариях.

Теперьмызнаем,чтоletmutguess объявляетизменяемуюсвязьсименемguess ,аподругуюсторонуот= находитсято,чтобудетпривязано:String::new() .

String   — это строковый тип, предоставляемый нам стандартной библиотекой.String  —этотекствкодировкеUTF-8переменнойдлины.

Синтаксис::new() использует:: , так как это привязанная к определённому типуфункция. То есть, она привязана к самому типуString , а не к определённой переменнойтипаString .Некоторыеязыкиназываютэто«статическимметодом».

Имяэтойфункции —new() , так как она создаёт новый, пустойString .Выможетенайтиэтуфункциюумногихтипов,потомучтоэтообщееимядлясозданияновогозначенияопределённоготипа.

Давайтепосмотримдальше:

Это уже побольше! Давайте это всё разберём. В первой строке есть две части. Этопервая:

Помните, какмыимпортировали (use ) std::io в самом начале нашей программы?Сейчасмывызвалиассоциированнуюснимфункцию.Еслибымынесделалиusestd::io ,намбыпришлосьздесьнаписатьstd::io::stdin() .

letmutguess=String::new();

letfoo=bar;

letfoo=5;//неизменяемаясвязьletmutbar=5;//изменяемаясвязь

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

io::stdin()

ЯзыкпрограммированияRust

23Угадайка

Page 24: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Эта функция возвращает обработчик стандартного ввода нашего терминала. Болееподробнообэтоможнопочитатьвstd::io::Stdin.

Следующая часть использует этот обработчик для получения всего, что введётпользователь:

Здесьмы вызвалиметодread_line() обработчика.Методыпохожинапривязанныефункции,нодоступнытолькоуопределённогоэкземпляратипа,анесамоготипа.Мыуказалиодинаргументфункцииread_line() :&mutguess .

Помните, как мы выше привязалиguess? Мы сказали, что она изменяема. Однако,read_line неполучаетвкачествеаргументаString :онаполучает&mutString .ВRustестьтакаяособенность,называемая«ссылки»,котораяпозволяетнамиметьнесколькоссылокна одни и те же данные, что позволяет избежать излишнего их копирования. Ссылки  —достаточно сложная особенность, и одним из основных подкупающих достоинств Rustявляетсято,каконрешаетвопросбезопасностиипростотыихиспользования.Покачтомынедолжны знать об этих деталях, чтобы завершить нашу программу. Сейчас, всё, что намнужно  — это знать, что ссылки, как и связывание при помощиlet , неизменяемы поумолчанию.Следовательно,мыдолжнынаписать&mutguess ,ане&guess .

Почемуread_line() получаетизменяемуюссылкунастроку?Егоработа —этовзятьто, что пользователь написал в стандартный ввод, и положить это в строку. Итак, функцияполучает строку в качестве аргумента, и для того, чтобы добавить в эту строку что-то, онадолжнабытьизменяемой.

Номыпокачтоещёнезакончилисэтойстрокойкода.Покаэтооднастрокатекста,этотолькоперваячастьоднойлогическойстрокикода:

Когдамывызываемметод,используясинтаксис.foo() ,мыможемперенестивызоввновую строку и сделать для него отступ. Это помогает работать с длинными строками.Мымоглибысделатьитак:

Но этодостаточно трудночитать.Поэтомумыразделили строку:по строкенакаждыйвызовметода.Мыужепоговорилиоread_line() ,ноещёничегонесказалипроok() иexpect() . Мы узнали, чтоread_line() передаёт всё, что пользователь ввёл в&mutString , которуюмыемупередали.Но этотметод такжеи возвращает значение: в данномслучае  —io::Result . В стандартной библиотеке Rust есть несколько типов с именемResult : общаяверсияResult инесколькоотдельныхверсийвподбиблиотеках,напримерio::Result .

.read_line(&mutguess)

.ok().expect("Неудалосьпрочитатьстроку");

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

ЯзыкпрограммированияRust

24Угадайка

Page 25: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЦельютиповResult являетсяпреобразованиеинформацииобошибках,полученныхотобработчика. У значений типаResult , как и любого другого типа, есть определённые длянего методы. В данном случае, уio::Result имеется методok() , который говорит, что«мы хотим получить это значение, если всё прошло хорошо. Если это не так, выбросьсообщениеобошибке».Нозачемвыбрасывать?Длянебольшихпрограмм,мыможемзахотетьтолько вывести сообщение об ошибке и прекратить выполнение программы.Методok()возвращаетзначение,укоторогообъявлендругойметод:expect() .Методexpect() берётзначение, для которого он вызван, и если оно не удачное, выполняетpanic! со строкой,заданной методу в качестве аргумента.panic! остановит нашу программу и выведетсообщениеобошибке.

Eсли мы уберем вызовы этих двух методов, наша программа скомпилируется, но мыполучимследующеепредупреждение:

$cargobuildCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)src/main.rs:10:5:10:39warning:unusedresultwhichmustbeused,#[warn(unused_must_use)]onbydefaultsrc/main.rs:10io::stdin().read_line(&mutguess);^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Rust предупреждает, что мы не используем значениеResult . Это предупреждениепришло из специальной аннотации, которая указана вio::Result . Rust пытается сказатьнам, что мы не обрабатываем ошибки, которые могут возникнуть. Наиболее правильнымрешением предотвращения ошибки будет её обработка. К счастью, если мы только хотимобрушитьприложение,еслиестьпроблема,мыможемиспользоватьэтидванебольшихметода.Еслимыможемвосстановитьчто-либоизошибки,мыдолжнысделатьчто-либодругое,номысохранимэтодлябудущегопроекта.

Тамвсегооднастрокаизпервогопримера:

Здесь выводится на экран строка, которая была получена с нашего ввода.{} - этоуказатель места заполнения. В качестве второго аргумента макросаprintln! мы указалиguess . Если нам надо вывести несколько привязок, в самом простом случае, мы должныпоставитьнесколькоуказателей,поодномунакаждуюпривязку:

Просто.

Мыможемзапуститьто,чтоунасестьприпомощиcargorun :

println!("Вашапопытка:{}",guess);}

letx=5;lety=10;

println!("xиy:{}и{}",x,y);

ЯзыкпрограммированияRust

25Угадайка

Page 26: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$cargorunCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)Running`target/debug/guessing_game`Угадайтечисло!Пожалуйста,введитепредположение.6Вашапопытка:6

Всёправильно!Нашаперваячастьзавершена:мыможемполучатьданныесклавиатурыипотомпечататьихнаэкран.

ГенерациясекретногочислаДалее, нам надо сгенерировать секретное число. В стандартной библиотеке Rust нет

ничего,чтомоглобынампредоставитьфункционалдлягенерациислучайныхчисел.Однако,разработчикиRustдляэтогопредоставиликонтейнер(crate)rand .«Контейнер» —этопакетскодомRust.Нашпроект —«бинарныйконтейнер»,изкотороговитогеполучитсяисполняемыйфайл.rand   — «библиотечный контейнер», который содержит код, предназначенный дляиспользованиясдругимипрограммами.

Прежде, чем мы начнём писать код с использованиемrand , мы должнымодифицироватьнашCargo.toml .Откроемегоидобавимвконецследующиестрочки:

[dependencies]

rand="0.3.0"

Секция[dependencies] похожанасекцию[package] :всё,чторасположенопослеобъявления секции и до начала следующей, является частью этой секции. Cargo используетсекциюсзависимостямичтобызнатьотом,какиесторонниеконтейнерыпотребуются,атакже какие их версии необходимы. В данном случае, мы используем версию0.3.0 . Cargoпонимаетсемантическое версионирование, которое является стандартом нумерации версий.Еслимыхотимиспользоватьпоследнююверсиюконтейнера,мыможемиспользовать* .Такжемыможем указать необходимый промежуток версий. ВдокументацииCargo есть большеинформации.

Теперь,безкаких-либоизмененийвнашемкоде,давайтесоберёмнашпроект:

$cargobuildUpdatingregistry`https://github.com/rust-lang/crates.io-index`Downloadingrandv0.3.8Downloadinglibcv0.1.6Compilinglibcv0.1.6Compilingrandv0.3.8Compilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)

(Конечноже,выможетевидетьдругиеверсии.)

ЯзыкпрограммированияRust

26Угадайка

Page 27: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Многонового!Теперь, когда унас есть внешние зависимости,Cargo скачалпоследниеверсиикаждойизнихизсвоегореестра,являющегосякопиейреестрасCrates.io.Crates.io —этоместо,гдепрограммистынаRustмогутпубликоватьсвоипроектысоткрытымисходнымкодом,чтобыихиспользоваливдругихпроектах.

Послеобновленияреестра,Cargoпроверяетраздел[dependencies] искачиваетвсё,чтонамнеобходимо.Внашемслучае,мысказали,чтонашпроектзависитотrand .Самомуконтейнеруrand дляработынуженконтейнерlibc .ПоэтойпричинеCargoскачалиlibc .Послезагрузкивсегонеобходимого,онокомпилируется,азатемкомпилируетсяинашпроект.

Еслимызапустимcargobuild снова,текствыводабудетдругим:

$cargobuild

Всёправильно,ничегонебудетвыведено!Cargoзнает,чтоужесобраныинашпроект,ивсе его зависимости, а значит незачем делать это снова. Раз делать ничего не надо, Cargoпростозавершилработу.Еслимысноваоткроемфайлsrc/main.rs ,сделаемкакие-нибудьизмененияизатемсохранимих,мыувидимтолькооднустроку:

$cargobuildCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)

Итак, мы сказали Cargo, что нам нужна библиотекаrand с любой версией ветки0.3.x ,ионвзялпоследнююверсию,натотмомент,когдаегозапустили-v0.3.8 .Ночтоделать, когдана следующейнеделе выйдет версияv0.3.9 ,содержащаяважныеизменения?Чтоеслиисправлениянастолькомасштабны,чтоверсия0.3.9 становитсянесовместимойснашимкодом?

РешениемэтойпроблемыявляетсяфайлCargo.lock ,которыйнаходитсявдиректориис нашим проектом. Когда мы в первый раз собирали наш проект, Cargo подобрал версии,подходящиеподнашиусловия,изаписалихвфайлCargo.lock .Когдамывбудущембудемсобирать наш проект, Cargo будет проверять, существует лиCargo.lock , и затемиспользовать указанные в нём версии контейнеров. Благодаря этому мы автоматическиполучаем повторяемые сборки. Другими словами, мы будем использовать контейнер версии0.3.8 дотехпор,покаявнонеобновиминформациюоеговерсиивCargo.lock .

А что, если мы захотим использовать версиюv0.3.9? У Cargo есть другая команда,update , которая скажет «игнорируй Cargo.lock, найди последние версии библиотек из тойветки,которуюмыуказаливCargo.toml.Когдавсёсделаешь,запишиинформациюоверсияхвCargo.lock».Нопоумолчанию,Cargoсмотриттольковерсиюбольше,чем0.3.0 , именьше0.4.0 .Еслимыхотимперейтинаверсии0.4.x ,мыдолжныуказатьэтовCargo.toml .Потом, когда мы запустимcargo build , Cargo обновит индекс и пересмотрит нашитребованиякrand .

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

ЯзыкпрограммированияRust

27Угадайка

Page 28: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

входятвсоставдругихболеекрупныхпроектов.

Давайтеиспользоватьrand .Вотнашследующийшаг:

Первое,чтомысделали —изменилипервуюстроку.Теперьонавыглядиттак:externcrate rand . Так как мы указалиrand в разделе[dependencies] , мы можемиспользоватьexterncrate длятого,чтобыRustзнал,чтомысобираемсяиспользоватьэтузависимость.extern crate также выполняет эквивалент оператораuse rand; , т.е.теперьмыможемиспользоватьвсё,чтоестьвконтейнереrand ,используяпрефиксrand:: .

Далее, мы добавили новую строкуuse : use rand::Rng . Мы собираемсяиспользоватьметод,аемунужно,чтобыRngбылвобластивидимости.Основнаяидеятакова:методы,объявленныегде-товдругомместе,называются«типажами»(traits),идлятого,чтобыэтот метод можно было использовать, необходимо чтобы типаж был в области видимости.Чтобыузнатьобэтомболееподробно,можнопрочитатьсекциюотипажах.

Мыдобавилидвеновыестрокивсерединукода:

Мы используем функциюrand::thread_rng() для получения копии генератораслучайных чисел, который будет локальным для текущегопотока выполнения. Выше мыдобавилиuserand::Rng итеперьможемиспользоватьметодgen_range() .Этотметодполучаетдвааргументаигенерируетчисло,котороеможетбытьбольшелиборавнопервомуаргументуименьше,чемвторойаргумент.Такимобразом,еслимыукажемчисла1и101,тоотгенератораможнополучитьчислаот1до100включительно.

externcraterand;

usestd::io;userand::Rng;

fnmain(){println!("Угадайтечисло!");

letsecret_number=rand::thread_rng().gen_range(1,101);

println!("Загаданноечисло:{}",secret_number);

println!("Пожалуйста,введитепредположение.");

letmutguess=String::new();

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

println!("Вашапопытка:{}",guess);}

letsecret_number=rand::thread_rng().gen_range(1,101);

println!("Загаданноечисло:{}",secret_number);

ЯзыкпрограммированияRust

28Угадайка

Page 29: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Давайтезапустимизменённуюпрограмму:

$cargorunCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)Running`target/debug/guessing_game`Угадайтечисло!Загаданноечисло:7Пожалуйста,введитепредположение.4Вашапопытка:4$cargorunRunning`target/debug/guessing_game`Угадайтечисло!Загаданноечисло:83Пожалуйста,введитепредположение.5Вашапопытка:5

Замечательно! Следующий шаг: сравнение нашего предположения с «загаданным»числом.

СравнениеТеперь, когдамы знаем, что ввёл пользователь, давайте сравним «загаданное» число с

предполагаемым ответом. Здесь приведён наш следующий шаг, который, к сожалению, небудетработать:

ЯзыкпрограммированияRust

29Угадайка

Page 30: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Здесь мы видим что-то новое. Первое  — это ещё одинuse . Мы ввели в областьвидимости типstd::cmp::Ordering . Далее, ещё пять новых строк в конце, которыеиспользуютего:

Методcmp() может быть вызван у чего-либо, что может сравниваться, и получаетссылку на то, с чем мы хотим его сравнить. Результатом сравнения будет типOrdering ,которыймыдобавиливыше.Мыиспользуемоператорmatch дляопределенияOrdering  —результатасравнения.Ordering  —перечисление .Ониобозначаютсяenum ,сокращённоотenumeration (перечисление).Перечислениявыглядятследующимобразом:

Стакимопределением,всё,чтоимееттипFoo можетиметьзначениелибоFoo::Bar ,либоFoo::Baz . Мы используем:: для обозначения пространства имён для вариантовперечисления.

externcraterand;

usestd::io;usestd::cmp::Ordering;userand::Rng;

fnmain(){println!("Угадайтечисло!");

letsecret_number=rand::thread_rng().gen_range(1,101);

println!("Загаданноечисло:{}",secret_number);

println!("Пожалуйста,введитепредположение.");

letmutguess=String::new();

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

println!("Вашапопытка:{}",guess);

matchguess.cmp(&secret_number){Ordering::Less=>println!("Слишкоммаленькое!"),Ordering::Greater=>println!("Слишкомбольшое!"),Ordering::Equal=>println!("Вывыиграли!"),}}

matchguess.cmp(&secret_number){Ordering::Less=>println!("Слишкоммаленькое!"),Ordering::Greater=>println!("Слишкомбольшое!"),Ordering::Equal=>println!("Вывыиграли!"),}

enumFoo{Bar,Baz,}

ЯзыкпрограммированияRust

30Угадайка

Page 31: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

УперечисленияOrdering естьтривозможныхварианта:Less , Equal иGreater .Выражениеmatch получаетпеременнуюкакого-либотипаипредлагаетвамсоздать«ветви»длякаждоговозможногозначения.ТаккакунасестьтривозможныхзначенияOrdering ,унасбудеттриветви:

Если результатом сравнения будет значениеLess , мы выведем на экранСлишкоммаленькое! ; если будетGreater , тоСлишком большое! ; и еслиEqual , тоВывыиграли! .match оченьудобениончастоиспользуетсявRust.

Мыупоминали,чтоэтонесовсемкорректныйкод,новсёжедавайтепопробуем:

$cargobuildCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)src/main.rs:28:21:28:35error:mismatchedtypes:expected`&collections::string::String`,found`&_`(expectedstruct`collections::string::String`,foundintegralvariable)[E0308]src/main.rs:28matchguess.cmp(&secret_number){^~~~~~~~~~~~~~error:abortingduetopreviouserrorCouldnotcompile`guessing_game`.

У-у-у! Это большая ошибка. Суть этой ошибки в «несоответствии типов» (mismatchedtypes).ВRustстрогаястатическаясистематипов.Однако,унастакжеестьвыводтипов.Когдамыпишемletguess=String::new() ,Rustпонимает,чтоguess должнабытьтипаString , благодаря чему мы можем не указывать тип явно.secret_number  —число,котороеможетиметьзначениеотодногодоста.Ономожетиметьтипi32  —32-битноецелое,илиu32  —32-битноецелоебеззнака,илиi64  —64-битноецелое,иликакой-нибудьдругой.По умолчанию, Rust сделает его 32-битным целым,i32 . Однако, здесь Rust не знает каксравнитьguess иsecret_number .Онидолжныбытьодноготипа.Витоге,чтобыможнобыло сравнитьguess иsecret_number , мы должныпреобразовать переменнуюguess ,которую мы прочитали с ввода, из типаString в настоящий числовой тип. Мы можемсделатьэто,добавивнесколькострочек.Воткакбудетвыглядетьнашапрограмма:

matchguess.cmp(&secret_number){Ordering::Less=>println!("Слишкоммаленькое!"),Ordering::Greater=>println!("Слишкомбольшое!"),Ordering::Equal=>println!("Вывыиграли!"),}

ЯзыкпрограммированияRust

31Угадайка

Page 32: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вотстроки,которыемыдобавили:

Подождитеминутку,унасведьужеестьguess?Rustпозволилнам«затенить»(скрыть)предыдущееguess новым. Это часто используется в подобных случаях, когдаguessизначально бывает типаString , но нам требуется преобразовать её вu32 . Затенениепозволяет нам переиспользовать имяguess , а не создавать для каждого типа новоеуникальноеимя,такоекакguess_str иguess иликакое-нибудьдругое.

Мысвязалиguess свыражением,котороепохоженато,чтомыписалиранее:

За которым следует вызовok().expect() . Здесьguess ссылается на старыйguess ,которыйещёявляетсястрокой,которуюмыполучилисввода.Методtrim() утипаString удаляетвсёпустоепространствосначалаиконцанашейстроки.Этоважно,ведьдлянормальной работыread_line() нам необходимо нажатьEnter послеокончанияввода.

externcraterand;

usestd::io;usestd::cmp::Ordering;userand::Rng;

fnmain(){println!("Угадайтечисло!");

letsecret_number=rand::thread_rng().gen_range(1,101);

println!("Загаданноечисло:{}",secret_number);

println!("Пожалуйста,введитепредположение.");

letmutguess=String::new();

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

letguess:u32=guess.trim().parse().ok().expect("Пожалуйста,введитечисло!");

println!("Вашапопытка:{}",guess);

matchguess.cmp(&secret_number){Ordering::Less=>println!("Слишкоммаленькое!"),Ordering::Greater=>println!("Слишкомбольшое!"),Ordering::Equal=>println!("Вывыиграли!"),}}

letguess:u32=guess.trim().parse().ok().expect("Пожалуйста,введитечисло!");

guess.trim().parse()

ЯзыкпрограммированияRust

32Угадайка

Page 33: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этозначит,чтоеслимынабрали5 инажалиEnter , guess выглядитследующимобразом:5\n . \n обозначает«новуюстроку»(newline) —значениеклавишиEnter . trim() удалитегоиоставиттолько5 .Методparse() ,применяемыйкстроке,преобразуетеёвчисло.Онможет анализировать различные числа, но мы можем указать Rust какой именно тип намнужен.Поэтомумыуказалиletguess:u32 .Двоеточие: ,идущеепослеguess ,говоритRust, что мы указали тип значения.u32 - 32-битное беззнаковое целое число. У Rust естьнескольковстроенныхчисловыхтипов,номывыбралиименноu32 .Этодостаточнохорошийтип,чтобыхранитьнебольшиеположительныечисла.

Какиread_line() ,вызовparse() можетвызватьпроблемы.Что,еслинашастрокабудет содержатьA�%? Мы не сможем преобразовать её в число. Как и в случае сread_line() , мы будем использовать методыok() иexpect() на случай, еслиparse() несможетпреобразоватьстроку.

Давайтезапустимнашупрограмму!

$cargorunCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)Running`target/guessing_game`Угадайтечисло!Загаданноечисло:58Пожалуйста,введитепредположение.76Вашапопытка:76Слишкомбольшое!

Замечательно! Вы можете видеть, что мы добавили пробел перед нашим числом, нопрограмма поняла, что мы хотели сказать76 . Запустим программу ещё несколько раз ипроверим,чтозагадываниечислаработает.

Теперь большая часть нашей игры работает, но мы можем сделать только однопредположение.Давайтеизменимэто,добавивциклы!

ЗацикливаниеКлючевоесловоloop создаётбесконечныйцикл.Давайтедобавимего:

ЯзыкпрограммированияRust

33Угадайка

Page 34: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Ипосмотримнаработуприложения.Ноподождите,мыжедобавилибесконечныйцикл?Всё верно. Помните что мы говорили оparse()? Если мы введём не числовой ответ, мыпростовыйдемизпрограммы.Посмотрите:

externcraterand;

usestd::io;usestd::cmp::Ordering;userand::Rng;

fnmain(){println!("Угадайтечисло!");

letsecret_number=rand::thread_rng().gen_range(1,101);

println!("Загаданноечисло:{}",secret_number);

loop{println!("Пожалуйста,введитепредположение.");

letmutguess=String::new();

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

letguess:u32=guess.trim().parse().ok().expect("Пожалуйста,введитечисло!");

println!("Вашапопытка:{}",guess);

matchguess.cmp(&secret_number){Ordering::Less=>println!("Слишкоммаленькое!"),Ordering::Greater=>println!("Слишкомбольшое!"),Ordering::Equal=>println!("Вывыиграли!"),}}}

ЯзыкпрограммированияRust

34Угадайка

Page 35: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$cargorunCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)Running`target/guessing_game`Угадайтечисло!Загаданноечисло:59Пожалуйста,введитепредположение.45Вашапопытка:45Слишкоммаленькое!Пожалуйста,введитепредположение.60Вашапопытка:60Слишкомбольшое!Пожалуйста,введитепредположение.59Вашапопытка:59Вывыиграли!Пожалуйста,введитепредположение.quitthread'<main>'panickedat'Пожалуйста,введитечисло!'

Ха! Если мы введёмquit , то действительно выйдем из программы. Как и при вводелюбогодругогонечисловогозначения.Чтож,это,мягкоговоря,неоченьхорошо.Дляначала,давайтесделаемвыходизпрограммы,еслимывыигралиигру:

ЯзыкпрограммированияRust

35Угадайка

Page 36: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

С добавлением строкиbreak после выводаВы выиграли! , мы получиливозможность выхода из цикла, когда мы угадали загаданное число. Выход из цикла такжеозначаетизавершениенашейпрограммы,таккакэтопоследнее,чтоестьвmain() .Намнадосделать ещё одно улучшение — при любом не числовом вводе, мы не должны выходить изпрограммы, мы просто должны проигнорировать ввод. Мы можем сделать это следующимобразом:

externcraterand;

usestd::io;usestd::cmp::Ordering;userand::Rng;

fnmain(){println!("Угадайтечисло!");

letsecret_number=rand::thread_rng().gen_range(1,101);

println!("Загаданноечисло:{}",secret_number);

loop{println!("Пожалуйста,введитепредположение.");

letmutguess=String::new();

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

letguess:u32=guess.trim().parse().ok().expect("Пожалуйста,введитечисло!");

println!("Вашапопытка:{}",guess);

matchguess.cmp(&secret_number){Ordering::Less=>println!("Слишкоммаленькое!"),Ordering::Greater=>println!("Слишкомбольшое!"),Ordering::Equal=>{println!("Вывыиграли!");break;}}}}

ЯзыкпрограммированияRust

36Угадайка

Page 37: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этострока,которуюмыизменили:

Здесь показано, как мы можем перейти от «сбоя при ошибке» к «обработке ошибки»заменивok().expect() на инструкциюmatch . Result , возвращённый функциейparse() , как иOrdering , является перечислением. Однако в данном случае каждый извариантовимеетнекоторыеассоциированныеснимданные:Ok  —успех,Err  —ошибку.Укаждого есть некоторая дополнительная информация: преобразованное число, либо типошибки. Здесьмы проверили значение результата работыparse() при помощиmatch .Вслучае, если результат равенOk , тоmatch привяжет внутреннее значение результата

externcraterand;

usestd::io;usestd::cmp::Ordering;userand::Rng;

fnmain(){println!("Угадайтечисло!");

letsecret_number=rand::thread_rng().gen_range(1,101);

println!("Загаданноечисло:{}",secret_number);

loop{println!("Пожалуйста,введитепредположение.");

letmutguess=String::new();

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

letguess:u32=matchguess.trim().parse(){Ok(num)=>num,Err(_)=>continue,};

println!("Вашапопытка:{}",guess);

matchguess.cmp(&secret_number){Ordering::Less=>println!("Слишкоммаленькое!"),Ordering::Greater=>println!("Слишкомбольшое!"),Ordering::Equal=>{println!("Вывыиграли!");break;}}}}

letguess:u32=matchguess.trim().parse(){Ok(num)=>num,Err(_)=>continue,};

ЯзыкпрограммированияRust

37Угадайка

Page 38: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

(Ok(num) )кимениnum ивернётвпривязкуguess .Когдапроисходитошибка(Err),намневажно, какая именно это ошибка, поэтому мы используем вместо имени _. Так мыпроигнорируемошибкуивызовемcontinue ,которыйотправитнаснаследующуюитерациюцикла.

Теперьвсёдолжнобытьнормально!Давайтепосмотрим:

$cargorunCompilingguessing_gamev0.1.0(file:///home/you/projects/guessing_game)Running`target/guessing_game`Угадайтечисло!Загаданноечисло:61Пожалуйста,введитепредположение.10Вашапопытка:10Слишкоммаленькое!Пожалуйста,введитепредположение.99Вашапопытка:99Слишкомбольшое!Пожалуйста,введитепредположение.fooПожалуйста,введитепредположение.61Вашапопытка:61Вывыиграли!

Замечательно! Если мы ещё чуть-чуть подкрутим нашу программу, игра будет готова.Догадываетесь,чтонужнопоменять?Всёправильно,мынедолжнывыводитьнашесекретноечисло. Знание этого числа хорошо для тестирования, но оно портит всю игру. Так выглядитокончательныйвариантнашегокода:

ЯзыкпрограммированияRust

38Угадайка

Page 39: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Готово!Высделали«Угадайку»!Поздравляем!

Этот первый проект показал вам следующее:let , match , методы, привязанныефункции, использование внешних контейнеров и многое другое. Наш следующий проектпокажетещёбольше.

externcraterand;

usestd::io;usestd::cmp::Ordering;userand::Rng;

fnmain(){println!("Угадайтечисло!");

letsecret_number=rand::thread_rng().gen_range(1,101);

loop{println!("Пожалуйста,введитепредположение.");

letmutguess=String::new();

io::stdin().read_line(&mutguess).ok().expect("Неудалосьпрочитатьстроку");

letguess:u32=matchguess.trim().parse(){Ok(num)=>num,Err(_)=>continue,};

println!("Вашапопытка:{}",guess);

matchguess.cmp(&secret_number){Ordering::Less=>println!("Слишкоммаленькое!"),Ordering::Greater=>println!("Слишкомбольшое!"),Ordering::Equal=>{println!("Вывыиграли!");break;}}}}

ЯзыкпрограммированияRust

39Угадайка

Page 40: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ОбедающиефилософыДля нашего второго проекта мы выбрали классическую задачу с параллелизмом. Она

называется «Обедающие философы». Задача была сформулирована в 1965 году ЭдсгеромДейкстрой,номыбудемиспользоватьверсиюзадачи,адаптированнуюв1985годуРичардомХоаром.

В древние времена богатые филантропы пригласили погостить пятерыхвыдающихся философов. Им выделили каждому по комнате, в которой они моглизаниматься своейпрофессиональнойдеятельностью —мышлением.Такжебылаобщаястоловая,гдестоялбольшойкруглыйстол,авокругнегопятьстульев.Каждыйстулимелтабличку с именем философа, который должен был сидеть на нем. Слева от каждогофилософа лежала золотая вилка, а в центре стола стояла большая миска со спагетти,которая постоянно пополнялась. Как подобает философам, они большую часть своеговременипроводиливраздумьях.Нооднаждыонипочувствовалиголодиотправилисьвстоловую.Каждыйселнасвойстул,взялповилкеивоткнулеёвмискусоспагетти.Носущность запутанных спагетти такова, что необходима вторая вилка, чтобы отправлятьспагетти в рот. То есть философу требовалась еще и вилка справа от него.Философыположили свои вилкии всталииз-за стола, продолжая думать.Ведь вилкаможет бытьиспользованатолькооднимфилософомодновременно.Еслидругойфилософзахочетеёвзять,тоемупридетсяждатькогдаонаосвободится.

Эта классическая задача показывает различные элементы параллелизма. Сложностьреализации задачи состоит в том, что простая реализация может зайти в безвыходноесостояние.Давайтерассмотримпростойпримеррешенияэтойпроблемы:

1. Философберетвилкувсвоюлевуюруку.2. Затемберетвилкувсвоюправуюруку.3. Ест.4. Кладетвилкинаместо.

Теперьпредставимэтокакпоследовательностьдействийфилософов:

1. Философ1начинаетвыполнятьалгоритм,беретвилкувлевуюруку.2. Философ2начинаетвыполнятьалгоритм,беретвилкувлевуюруку.3. Философ3начинаетвыполнятьалгоритм,беретвилкувлевуюруку.4. Философ4начинаетвыполнятьалгоритм,беретвилкувлевуюруку.5. Философ5начинаетвыполнятьалгоритм,беретвилкувлевуюруку.6. ...?Всевилкизанятыиниктонеможетначатьесть!Безвыходноесостояние.

Есть различные пути решения этой задачи. Мы в этом руководстве покажем своерешение.Сначаладавайтеначнемсмоделированиязадачи.Начнемсфилософов:

ЯзыкпрограммированияRust

40Обедающиефилософы

Page 41: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Здесьмысоздаемstruct ,представляющуюфилософа.Наданныймоментнамнужновсего лишьимя.Мы выбрали типString , а не&str дляхраненияимени.Обычнопрощеработатьстипом,владеющимданными,чемстипом,использующимссылки.

Продолжим:

Этотблокimpl позволяетобъявитьчто-либодляструктурыPhilosopher .Внашемслучае мы объявляем «статическую функцию»new . Первая строка этой функции выглядиттак:

Она принимает один аргумент,name , типа&str . Это ссылка на другую строку. ОнавозвращаетновыйэкземплярнашейструктурыPhilosopher .

Этот код создаёт новый экземплярPhilosopher и присваивает его полюnameзначениепереданного аргументаname .Ноиспользуетсянесамаргумент,арезультатвызоваего метода.to_string() . Этот вызов создаёт копию строки, на которую указывает наш&str , и возвращает новый экземплярString , который и будет присвоен полюnameструктурыPhilosopher .

structPhilosopher{name:String,}

implPhilosopher{fnnew(name:&str)->Philosopher{Philosopher{name:name.to_string(),}}}

fnmain(){letp1=Philosopher::new("ДжудитБатлер");letp2=Philosopher::new("РаяДунаевская");letp3=Philosopher::new("ЗарубинаНаталья");letp4=Philosopher::new("ЭммаГольдман");letp5=Philosopher::new("АннаШмидт");}

implPhilosopher{fnnew(name:&str)->Philosopher{Philosopher{name:name.to_string(),}}}

fnnew(name:&str)->Philosopher{

Philosopher{name:name.to_string(),}

ЯзыкпрограммированияRust

41Обедающиефилософы

Page 42: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ПочемубысразунепередаватьстрокутипаString напрямую?Таклегчееевызывать.ЕслибымыпринималитипString ,атот,ктовызываетфункцию,имелбыссылкунастроку,&str , то ему пришлось бы приводить ее к типуString перед каждым вызовом. Этоуменьшит гибкость кода, и мы будем вынужденыкаждыйраз создавать копию строки. Дляэтой небольшой программы это не очень важно, так как мы знаем, что будем использоватьтолькокороткиестроки.

И последнее на что следует обратить внимание: мы просто объявляем структуруPhilosopher икажется,чтоничегобольшенеделаем.Rust —этоязыкпрограммирования,«ориентированныйнавыражения»,чтоозначает,чтокаждоевыражениевозвращаетзначение.Этоверноидляфункций,укоторыхавтоматическивозвращаетсяпоследнеевыражение.ТаккаквнашемпримеревпоследнемвыражениифункциимысоздаемструктуруPhilosopher ,тоонаибудетвозвращенафункцией.

Имяфункцииnew() нерегламентируетсяRust.Этопростосоглашениеобименованиифункций, которые возвращают новые экземпляры структур. Давайте снова посмотрим нафункциюmain() :

Здесьмысвязываемпятьименпеременныхспятьюновымифилософами.Здесьуказаныименанекоторыхизвестныхфилософов,новыможетеуказатьлюбыедругие.Еслибымынеобъявилисвоюреализациюфункцииnew() ,тонашкодвыгляделбытак:

Этоткодвыглядитнеслишкомизящно.Использованиестатическойфункцииnew имеетидругиепреимущества,нодажевэтомпростомслучае,еёиспользованиебылооправдано.

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

fnmain(){letp1=Philosopher::new("ДжудитБатлер");letp2=Philosopher::new("РаяДунаевская");letp3=Philosopher::new("ЗарубинаНаталья");letp4=Philosopher::new("ЭммаГольдман");letp5=Philosopher::new("АннаШмидт");}

fnmain(){letp1=Philosopher{name:"ДжудитБатлер".to_string()};letp2=Philosopher{name:"РаяДунаевская".to_string()};letp3=Philosopher{name:"ЗарубинаНаталья".to_string()};letp4=Philosopher{name:"ЭммаГольдман".to_string()};letp5=Philosopher{name:"АннаШмидт".to_string()};}

ЯзыкпрограммированияRust

42Обедающиефилософы

Page 43: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Давайте сначала рассмотрим функциюmain() . Вместо того чтобы создавать пятьотдельныхсвязанныхимендляфилософов,мысоздаемдлянихVec<T> . Vec<T> называют«вектор», он является расширяемой версией массива. Затем в циклеfor мы перебираемвектор,получаяссылкунаочередногофилософанакаждойитерации.

Втелецикламывызываемметодp.eat() ,которыйобъявленвыше:

ВRustметодыявнополучаютпараметрself .Вотпочемуeat() являетсяметодом,аnew  —статическойфункцией:new() неполучаетпараметрself .Длянашейпервойверсииметодаeat() мывыводимтолькоимяфилософаи сообщениео том,чтоон закончилесть.Запустивэтупрограммувыполучите:

ДжудитБатлерзакончилаесть.РаяДунаевскаязакончилаесть.ЗарубинаНатальязакончилаесть.ЭммаГольдманзакончилаесть.АннаШмидтзакончилаесть.

Этобылонесложно!Осталосьчуть-чутьиприступимксамойзадаче.

Дальшенамнужносделатьтак,чтобыфилософынетолькозаканчивали,ноиначинали

structPhilosopher{name:String,}

implPhilosopher{fnnew(name:&str)->Philosopher{Philosopher{name:name.to_string(),}}fneat(&self){println!("{}закончилаесть.",self.name);}}

fnmain(){letphilosophers=vec![Philosopher::new("ДжудитБатлер"),Philosopher::new("РаяДунаевская"),Philosopher::new("ЗарубинаНаталья"),Philosopher::new("ЭммаГольдман"),Philosopher::new("АннаШмидт"),];

forpin&philosophers{p.eat();}}

fneat(&self){println!("{}закончилаесть.",self.name);}

ЯзыкпрограммированияRust

43Обедающиефилософы

Page 44: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

есть.Этоноваяверсияпрограммы:

Появилисьнекоторыенебольшиеизменения.Давайтепосмотрим,чтожеизменилось:

Конструкцияuse предоставляет доступ к области видимости модуляthread изстандартной библиотеки.Мы собираемся использовать этот модуль далее в коде, и поэтомунамнужнообъявитьоегоиспользовании.

usestd::thread;

structPhilosopher{name:String,}

implPhilosopher{fnnew(name:&str)->Philosopher{Philosopher{name:name.to_string(),}}fneat(&self){println!("{}началаесть.",self.name);

thread::sleep_ms(1000);

println!("{}закончилаесть.",self.name);}}

fnmain(){letphilosophers=vec![Philosopher::new("ДжудитБатлер"),Philosopher::new("РаяДунаевская"),Philosopher::new("ЗарубинаНаталья"),Philosopher::new("ЭммаГольдман"),Philosopher::new("АннаШмидт"),];

forpin&philosophers{p.eat();}}

usestd::thread;

fneat(&self){println!("{}началаесть.",self.name);

thread::sleep_ms(1000);

println!("{}закончилаесть.",self.name);}

ЯзыкпрограммированияRust

44Обедающиефилософы

Page 45: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Здесь мы выводим на экран два сообщения и вызываем функциюsleep_ms междуними. Эта функция останавливает рабочий поток на 1000 миллисекунд, что симулируетпроцессприемапищифилософа.

Если вы запустите программу теперь, то увидите, что каждый философ, по очереди,начинаетесть,есткакое-товремяизаканчиваетесть:

ДжудитБатлерначалаесть.ДжудитБатлерзакончилаесть.РаяДунаевскаяначалаесть.РаяДунаевскаязакончилаесть.ЗарубинаНатальяначалаесть.ЗарубинаНатальязакончилаесть.ЭммаГольдманначалаесть.ЭммаГольдманзакончилаесть.АннаШмидтначалаесть.АннаШмидтзакончилаесть.

Превосходно! Теперь у нас осталась только одна проблема: наши философы едят поочереди,анеодновременно,тоестьмыпоканерешилизадачупараллелизма.

Для того, чтобы наши философы начали есть одновременно, нам нужно внестинекоторыеизменениявкод:

ЯзыкпрограммированияRust

45Обедающиефилософы

Page 46: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мыдобавилиещеодинциклвфункциюmain() .Теперьонавыглядиттак:

Тутдобавилисьтрудныекпониманиюпятьстроккода.Давайтеразбираться.

usestd::thread;

structPhilosopher{name:String,}

implPhilosopher{fnnew(name:&str)->Philosopher{Philosopher{name:name.to_string(),}}

fneat(&self){println!("{}началаесть.",self.name);

thread::sleep_ms(1000);

println!("{}закончилаесть.",self.name);}}

fnmain(){letphilosophers=vec![Philosopher::new("ДжудитБатлер"),Philosopher::new("РаяДунаевская"),Philosopher::new("ЗарубинаНаталья"),Philosopher::new("ЭммаГольдман"),Philosopher::new("АннаШмидт"),];

lethandles:Vec<_>=philosophers.into_iter().map(|p|{thread::spawn(move||{p.eat();})}).collect();

forhinhandles{h.join().unwrap();}}

lethandles:Vec<_>=philosophers.into_iter().map(|p|{thread::spawn(move||{p.eat();})}).collect();

lethandles:Vec<_>=

ЯзыкпрограммированияRust

46Обедающиефилософы

Page 47: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Объявляемновоесвязанноеимяhandles .Мызадалитакоеимя,потомучтособираемсясоздать несколько потоков, в результате чего получим для них дескрипторы, с помощьюкоторых сможемконтролироватьих выполнение. Здесьнамнужно явноуказать тип, а зачемэто необходимо, мы расскажем чуть позже._ - это заполнитель типа. Мы говоримкомпилятору «handles   — это вектор, содержащий элементы, тип которых Rust долженвывестисамостоятельно».

Мыберемнашсписокфилософовивызываемметодinto_iter() .Этотметодсоздаётитератор, который при каждой итерации забирает право владения на соответствующийэлемент. Это нужно для передачи элемента вектора в поток. Мы берем этот итератор ивызываем методmap , который принимает замыкание в качестве аргумента и вызывает этозамыканиедлякаждогоизэлементовитератора.

Вот здесь происходит сам параллелизм. Функцияthread::spawn принимает вкачестве аргумента замыкание и исполняет это замыкание в новом потоке. Это замыканиедополнительно нуждается в указании ключевого словаmove , которое сообщает, что этозамыкание получает владение переменными, которые оно захватывает. В данном случае  —переменнойp функцииmap .

Внутрипотокамы всего лишь вызываемметодeat() переменнойp .Такжеобратитевнимание,чтовызовthread::spawn неоканчиваетсяточкойсзапятой,чтопревращаетегов выражение.Этот нюанс важен, так как возвращается правильное значение.Для полученияболееподробнойинформации,прочитайтеглавуВыраженияиоператоры.

Позавершениимыполучаемрезультатвызоваmap исобираемполученныйрезультатвколлекциюспомощьюметодаcollect() .Методcollect() создаётколлекциюкакого-тотипа,идлятого,чтобыRustпонял,коллекциюкакоготипамыхотимполучить,мыуказалидляhandle типпринимаемогозначенияVec<T> .Элементамиколлекциибудутвозвращаемыеизметодовthread::spawn значения,которыеявляютсядескрипторамиэтихпотоков.Воттак!

Вконцефункцииmain() мывциклеперебираемкаждыйдескрипторивызываемдлянего методjoin() , который блокирует дальнейшее исполнение основного потока, пока незавершитсядочернийпоток.Этопозволяетнамбытьуверенными,чтопотокизавершатработудотогокакпроизойдетвыходизпрограммы.

Есливызапуститеэтупрограмму,товыувидите,чтофилософыедятнедожидаясьсвоейочереди!Унасмногопоточность!

philosophers.into_iter().map(|p|{

thread::spawn(move||{p.eat();})

}).collect();

forhinhandles{h.join().unwrap();}

ЯзыкпрограммированияRust

47Обедающиефилософы

Page 48: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ДжудитБатлерначалаесть.РаяДунаевскаяначалаесть.ЗарубинаНатальяначалаесть.ЭммаГольдманначалаесть.АннаШмидтначалаесть.ДжудитБатлерзакончилаесть.РаяДунаевскаязакончилаесть.ЗарубинаНатальязакончилаесть.ЭммаГольдманзакончилаесть.АннаШмидтзакончилаесть.

Нокакжебытьсвилками?Ихмыпокаещёнесмоделировали.

Давайтеженачнем.Сначаласделаемновуюструктуру:

СтруктураTable содержитвектормьютексов(Mutex ).Мьютекс —способуправлениядоступом к данным для параллельно выполняющихся потоков: только один поток можетполучить доступ к даннымв конкретныймомент времени.Это именно то свойство, котороенужно для реализации наших вилок. В коде мы используем пустой кортеж,() , внутримьютекса,таккакнесобираемсяиспользоватьэтозначение,амьютексиспользуетсятолькодляорганизациидоступа.

Давайтеизменимпрограмму,используяструктуруTable :

usestd::sync::Mutex;

structTable{forks:Vec<Mutex<()>>,}

usestd::thread;usestd::sync::{Mutex,Arc};

structPhilosopher{name:String,left:usize,right:usize,}

implPhilosopher{fnnew(name:&str,left:usize,right:usize)->Philosopher{Philosopher{name:name.to_string(),left:left,right:right,}}

fneat(&self,table:&Table){let_left=table.forks[self.left].lock().unwrap();let_right=table.forks[self.right].lock().unwrap();

println!("{}началаесть.",self.name);

thread::sleep_ms(1000);

ЯзыкпрограммированияRust

48Обедающиефилософы

Page 49: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Многоизменений!Однако, с этимиизменениямимыполучиликорректноработающуюпрограмму.Приступимкрассмотрению:

Нам далее понадобится структураArc<T> из модуля стандартной библиотекиstd::sync .Мыпоговоримонейчутьпозже.

println!("{}закончилаесть.",self.name);}}

structTable{forks:Vec<Mutex<()>>,}

fnmain(){lettable=Arc::new(Table{forks:vec![Mutex::new(()),Mutex::new(()),Mutex::new(()),Mutex::new(()),Mutex::new(()),]});

letphilosophers=vec![Philosopher::new("ДжудитБатлер",0,1),Philosopher::new("РаяДунаевская",1,2),Philosopher::new("ЗарубинаНаталья",2,3),Philosopher::new("ЭммаГольдман",3,4),Philosopher::new("АннаШмидт",0,4),];

lethandles:Vec<_>=philosophers.into_iter().map(|p|{lettable=table.clone();

thread::spawn(move||{p.eat(&table);})}).collect();

forhinhandles{h.join().unwrap();}}

usestd::sync::{Mutex,Arc};

structPhilosopher{name:String,left:usize,right:usize,}

ЯзыкпрограммированияRust

49Обедающиефилософы

Page 50: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Нам понадобилось добавить еще два поля в нашу структуруPhilosopher . Каждыйфилософдолжениметь две вилки: одну —для левой руки, другую —дляправой руки.Мыиспользуем типusize дляидентификациикаждойвилки.Мыиспользуемегоприсозданиифилософа, передавая идентификаторы двух вилок. Эти два значения будут использоватьсяполемforks структурыTable .

Мыиспользуемфункциюnew() длязаданиязначенийleft иright .

Здесь появились две новые строки. Мы также добавили один аргумент,table . МыполучаемдоступкспискувилокчерезструктуруTable .Затемиспользуемидентификаторывилокself.left иself.right для получения доступа к вилке по определенномуиндексу. В результате чего мы получаемMutex , который регулирует доступ к вилке, ивызываемдлянегометодlock() ,блокируядоступквилке.Есливнастоящеевремядоступквилке уже предоставлен кому-то еще, то мы будем блокированы, пока вилка не станетдоступной.

Вызовметодаlock() можетпотерпетьнеудачу,иеслиэтослучается,томыаварийнозавершаем работу программы. Может возникнуть ситуация, когда поток аварийно завершитсвою работу, а мьютекс при этом останется заблокированным. Такой мьютекс называется«отравленным(poisoned)».Новнашемслучаеэтонеможетпроизойти,потомукакмыпростоиспользуемметодunwrap() .

Результаты выполнения этих двух строк имеют имена_left и_rightсоответственно. Зачем мы используем знаки подчеркивания в начале имён? Это для того,чтобы сказать компилятору, что мы хотим получить значения, которые далеене планируемиспользовать. Таким образом Rust не будет выводить предупреждение о неиспользуемыхименах.

Когда же мьютекс будет освобождён? Это произойдет автоматически, когда_left и_right выйдутизобластивидимости,тоестьпоокончанииработыфункции.

fnnew(name:&str,left:usize,right:usize)->Philosopher{Philosopher{name:name.to_string(),left:left,right:right,}}

fneat(&self,table:&Table){let_left=table.forks[self.left].lock().unwrap();let_right=table.forks[self.right].lock().unwrap();

println!("{}началаесть.",self.name);

thread::sleep_ms(1000);

println!("{}закончилаесть.",self.name);}

ЯзыкпрограммированияRust

50Обедающиефилософы

Page 51: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Далее вmain() мы создаемновый экземпляр структурыTable и оборачиваемего вArc<T> .Это«атомарныйсчетчикссылок»(atomicreferencecount).ОннужендляобеспечениядоступакнашейструктуреTable изнесколькихпотоков.Когдаонпередаетсявновыйпоток,тосчетчикувеличивается,акогдаэтотпотокзавершаетработу,тосчетчикуменьшается.

Мыдобавилинашизначенияleft иright при создании структурыPhilosopher .Здесь естьочень важная деталь, на которую следует обратить внимание. Посмотрите напоследнюю строку созданияPhilosopher . Конструктор Анны Шмидт должен был быприниматьвкачествеаргументовзначения4 и0 ,новместоэтогоонпринимаетзначения0 и4 .Этопомешаетнашейпрограммепопастьвбезвыходноесостояние,есликаждыйвозьметпооднойвилкеодновременно.Такчтодавайтепредставим,чтоодинизфилософовунаслевша!Этоодинизспособоврешитьданнуюпроблему,и,намойвзгляд,самыйпростой.

Внутри нашего циклаmap() /collect() мы вызываем методtable.clone() .Методclone() структурыArc<T> клонируетзначениеиинкрементируетсчетчик,которыйавтоматическидекрементируется,когдаклонированное значениепокинетобластьвидимости.Этонеобходимодлятого,чтобымызнали,какмногоссылокнаtable существуютврамкахнашихпотоковнаданныймоментвремени.Еслибыунаснебылоподсчетассылок,томыбынезнали,какикогдаосвободитьхранимоезначение.

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

Теперь наша программа работает! Только два философа могут обедать одновременно.Послезапускапрограммывыможетеполучитьтакойрезультат.

lettable=Arc::new(Table{forks:vec![Mutex::new(()),Mutex::new(()),Mutex::new(()),Mutex::new(()),Mutex::new(()),]});

letphilosophers=vec![Philosopher::new("ДжудитБатлер",0,1),Philosopher::new("РаяДунаевская",1,2),Philosopher::new("ЗарубинаНаталья",2,3),Philosopher::new("ЭммаГольдман",3,4),Philosopher::new("АннаШмидт",0,4),];

lethandles:Vec<_>=philosophers.into_iter().map(|p|{lettable=table.clone();

thread::spawn(move||{p.eat(&table);})}).collect();

ЯзыкпрограммированияRust

51Обедающиефилософы

Page 52: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

РаяДунаевскаяначалаесть.ЭммаГольдманначалаесть.ЭммаГольдманзакончилаесть.РаяДунаевскаязакончилаесть.ДжудитБатлерначалаесть.ЗарубинаНатальяначалаесть.ДжудитБатлерзакончилаесть.АннаШмидтначалаесть.ЗарубинаНатальязакончилаесть.АннаШмидтзакончилаесть.

Поздравляем!ВыреализоваликлассическуюзадачупараллелизманаязыкеRust.

ЯзыкпрограммированияRust

52Обедающиефилософы

Page 53: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ВызовкоданаRustиздругихязыковДля нашего третьего проекта мы собираемся выбрать что-то, что подчеркнёт одну из

самыхсильныхсторонвRust:фактическоеотсутствиесредыисполнения.

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

Существует несколько областей, где многие языки программирования слабы в планепроизводительности выполнения программ. Часто компромисс заключается в том, чтобыиспользовать более медленный язык, который взамен способствует повышениюпроизводительности программиста. Чтобы решить эту проблему, часть кода системы можнонаписать на C, а затем вызвать этот код, написанный на C, как если бы он был написан наязыке высокого уровня. Это называется «интерфейс внешних функций» (foreign functioninterface),частосокращаетсядоFFI.

RustвключаетподдержкуFFIвобоихнаправлениях:онлегкоможетвызватьCкод,ионтакжелегко,какиCкод,можетбытьвызванизвне.Rustсочетаетвсебеотсутствиесборщикамусораинизкиетребованияксредеисполнения,чтоделаетRustотличнымкандидатомнарольвызываемогоиздругихязыков,когданужнынекоторыедополнительныевозможности.

В этой книге есть целаяглава, посвящённая FFI и его специфике, а в этой главе мырассмотрим именно конкретный частный случай FFI, с тремя примерами, наRuby, Python иJavaScript.

ПроблемаЕстьмногоразличныхпроектов,которыемымоглибывыбрать,номыхотимподобрать

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

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

Во-вторых, многие языки имеют «глобальную блокировку интерпретатора» (globalinterpreterlock),котораяограничиваетпараллелизмвомногихситуациях.Этоделаетсявоимябезопасности,чтооказываетположительныйэффект,ноэтотакжеиограничиваетобъемработ,

ЯзыкпрограммированияRust

53ВызовкоданаRustиздругихязыков

Page 54: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Чтобыподчеркнутьэтидвааспекта,мысобираемсясоздатьнебольшойпроект,которыйвзначительнойстепениихиспользует.Посколькувниманиев этомпримересфокусированонавстраиваниеRust в другие языки, а не самойпроблеме,мыбудемиспользовать игрушечныйпример:

Запустить десять потоков. Внутри каждого потока считать от одного до пятимиллионов.Послетогокаквседесятьпотоковзавершатся,напечатать"сделано!".

Мывыбралипятьмиллионовруководствуясьтем,скольковременизанимаетэтаработанасовременномкомпьютере.ВотпримерэтогокоданаRuby:

threads=[]

10.timesdothreads<<Thread.newdocount=0

5_000_000.timesdocount+=1endendend

threads.each{|t|t.join}puts"сделано!"

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

На выбранной нами системе эта программа работает2.156 секунд. И если мывоспользуемся какой-нибудь утилитой для мониторинга процессов (например,top ), тоувидим,чтоонаиспользуеттолькоодноядро.ЭтоGILделаетсвоедело.

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

БиблиотеканаRustДавайте перепишем эту задачу на Rust. Во-первых, давайте сделаем новый проект с

помощьюCargo:

ЯзыкпрограммированияRust

54ВызовкоданаRustиздругихязыков

Page 55: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$cargonewembed$cdembed

ЭтупрограммулегкопереписатьнаRust:

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

Сейчас, однако, этопросто библиотекаRust, котораяне включает всенеобходимоедляуспешного вызова из другого языка.Еслимыпопытаемсяподключить её к другому языку втомвиде,вкоторомонасейчас,тоэтонебудетработать.Намнужносделатьдванебольшихизменения,чтобыисправитьэто.Первое,чтомыдолжнысделать,этоизменитьначалонашегокода:

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

Другимизменением, котороемыдобавили, являетсяpubextern . pub означает, чтоэта функция может быть вызвана за пределами этого модуля, аextern говорит, что еёвозможновызватьизС.Вотивсе!Нетакимногоизменений.

Второе,чтомыдолжнысделать,этоизменитьнастройкивCargo.toml .Добавьтеэтовконецфайла:

usestd::thread;

fnprocess(){lethandles:Vec<_>=(0..10).map(|_|{thread::spawn(||{letmutx=0;for_in(0..5_000_000){x+=1}x})}).collect();

forhinhandles{println!("Threadfinishedwithcount={}",h.join().map_err(|_|"Couldnotjoinathread!").unwrap());}println!("done!");

#[no_mangle]pubexternfnprocess(){

ЯзыкпрограммированияRust

55ВызовкоданаRustиздругихязыков

Page 56: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

[lib]name="embed"crate-type=["dylib"]

Это говоритRust, чтомы хотим скомпилировать нашу библиотеку в виде стандартнойдинамической библиотеки. По умолчанию, Rust компилирует в rlib, Rust- специфичныйформат.

Давайтетеперьсоберемпроект:

$cargobuild--releaseCompilingembedv0.1.0(file:///home/steve/src/embed)

Мы ввели командуcargo build --release , которая выполняет сборку свключеннойоптимизацией.Мыхотим,чтобыкодбылкакможноболеебыстрым!Выможетенайтисобраннуюбиблиотекувtarget/release :

$lstarget/release/builddepsexampleslibembed.sonative

Файлlibembed.so   — и есть наша динамическая библиотека (shared object). Мыможем использовать этот файл также как и любую другую динамическую библиотеку,написанную на C! Попутно следует отметить, это может бытьembed.dll илиlibembed.dylib ,взависимостиотплатформы.

Теперь,когдамыполучилинашусобраннуюбиблиотекуRust,давайтеиспользуемеёизнашегокоданаRuby.

RubyОткройтефайлembed.rb внутринашегопроекта,исделайтеследующее:

require'ffi'

moduleHelloextendFFI::Libraryffi_lib'target/release/libembed.so'attach_function:process,[],:voidend

Hello.process

puts'сделано!'

Преждечеммысможемзапуститьэтоткод,намнужноустановитьпакетffi :

ЯзыкпрограммированияRust

56ВызовкоданаRustиздругихязыков

Page 57: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$geminstallffi#thismayneedsudoFetching:ffi-1.9.8.gem(100%)Buildingnativeextensions.Thiscouldtakeawhile...Successfullyinstalledffi-1.9.8Parsingdocumentationforffi-1.9.8Installingridocumentationforffi-1.9.8Doneinstallingdocumentationforffiafter0seconds1geminstalled

И,наконец,мыможемпопробоватьзапуститьего:

$rubyembed.rbсделано!$

Ничего себе, это было быстро! На моей системе это заняло0.086 секунд, а не двесекундыкакэтобылоначистомRuby.ДавайтеразберемэтотRubyкод:

require'ffi'

Первый делом, нам надо объявить пакетffi . Он предоставляет нам интерфейс дляиспользованиянашейбиблиотекинаRust,какбиблиотекунаC.

moduleHelloextendFFI::Libraryffi_lib'target/release/libembed.so'

Автор пакетаffi рекомендует использовать модуль, чтобы ограничить областьдействия функции, которую мы импортировали из разделяемой библиотеки. Внутри мыуказалиextend , чтобы воспользоваться необходимым модулемFFI::Library , а затемвызвалиffi_lib , чтобы подгрузить нашу библиотеку. Мы просто передаем путь кбиблиотеке,которыймыужевиделираньше,этоtarget/release/libembed.so .

attach_function:process,[],:void

Методattach_function предоставляется пакетомFFI . Здесь соединяются нашафункцияprocess() , написанная на Rust, и одноименная функция на Ruby. Так какprocess() не принимает аргументов, второй параметр является пустым массивом, ипоскольку функция ничего не возвращает, мы передаем:void в качестве завершающегоаргумента.

Hello.process

ЗдесьмысовершаемвызовнашейRustфункции.Сочетаниенашегоmodule ивызовакattach_function завершаетподготовку.ЭтовыглядиткакфункцияRuby,нонасамомделеэтоRust!

puts'сделано!'

Наконец, в соответствие с нашими требованиями к проекту, мы пишемсделано! поокончаниюработыпрограммы.

ЯзыкпрограммированияRust

57ВызовкоданаRustиздругихязыков

Page 58: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

ТеперьдавайтепопробуемнаPython!

PythonСоздайтефайлembed.py вэтойдиректорииипоместитевнегоследующее:

fromctypesimportcdll

lib=cdll.LoadLibrary("target/release/libembed.so")

lib.process()

print("сделано!")

Довольно просто! Мы импортируемcdll из модуляctypes . Затем вызваемLoadLibrary .Итеперьмыможемвызватьprocess() .

Намоейсистемеэтозаняло0.017 секунд.Быстро!

Node.jsNode —этонеязык,но,внастоящеевремя,этодоминирующаяреализацияисполнения

JavaScriptнасервере.

Длятого,чтобысделатьFFIвNode,намсначаланадоустановитьбиблиотеку:

$npminstallffi

Послеустановки,мыможемейвоспользоваться:

varffi=require('ffi');

varlib=ffi.Library('target/release/libembed',{'process':['void',[]]});

lib.process();

console.log("сделано!");

Пример больше похож на Ruby, чем на Python. Мы используем модульffi , чтобыполучить доступ кffi.Library() , который загружает нашу библиотеку. Нам нужноуказать тип возвращаемого значения и типы аргументов функции:void для возвращаемогозначения и пустой массив для указания отсутствия аргументов. После этого мы простовызываемфункциюипечатаемрезультат.

Намоейсистемеэтозаняло0.092 секунды.

ЯзыкпрограммированияRust

58ВызовкоданаRustиздругихязыков

Page 59: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЗаключениеКак вы можете видеть, основы, рассмотренные здесь, являютсяочень простыми.

Конечно,мымоглибысделатькудабольшетого,чтомыздесьпоказали.ПосмотритеглавуFFIдляболееподробнойинформации.

ЯзыкпрограммированияRust

59ВызовкоданаRustиздругихязыков

Page 60: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЭффективноеиспользованиеRustИтак,выузнали,какписатькоднаRust.Ноестьразницамеждунаписаниемкакого-то

коданаRustинаписаниемхорошегокоданаRust.

Этотразделсостоитизотносительносамостоятельныхуроков,которыепоказывают,какповысить уровень вашего кода наRust. В нем представлены общиешаблоны и стандартныефункциибиблиотеки.Главывэтомразделемогутбытьпрочитанывлюбомпорядкеповашемувыбору.

ЯзыкпрограммированияRust

60ЭффективноеиспользованиеRust

Page 61: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СтекикучаКаклюбойсистемныйязыкпрограммирования,Rustработаетнанизкомуровне.Есливы

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

УправлениепамятьюЭтидватерминакасаютсяуправленияпамятью.Стекикуча —этоабстракции,которые

помогаютвамопределить,когдатребуетсявыделениеиосвобождениепамяти.

Вотвысокоуровневоесравнение.

Стекработаеточеньбыстро;вRustпамятьвыделяетсявстекепоумолчанию.Выделениепамятивстекеявляетсялокальнымпоотношениюквызовуфункции,иимеетограниченныйразмер. Куча, с другой стороны, работает медленнее, а выделение памяти в кучеосуществляется в программе явно. Но такая память имеет теоретически неограниченныйразмер,идоступнаглобально.

СтекДавайтепоговоримоследующейпрограмменаRust:

Эта программа имеет одно связанное имя,x . Память для него необходимо где-товыделить. Rust по умолчанию «выделяет память в стеке», что означает, что переменные«помещаютсявстеке».Чтоэтозначит?

Когда функция вызывается, то выделяется некоторый объем памяти для всех еёлокальныхпеременныхинекоторойдополнительнойинформации.Этоназывается«стековыйкадр» (stack frame). В этом руководстве мы будем игнорировать эту дополнительнуюинформацию, и будем рассматривать лишь локальные переменные, которые мы определяем.Такимобразом,вэтомслучае,когдавыполняетсяmain() ,мывыделяемодно32-битноецелоечисло в нашем кадре стека. Как выможете видеть, это происходит автоматически —мы недолжныписатькакой-либоспециальныйкоднаRustдляэтого.

Когда функция завершается, её стековый кадр освобождается. Это происходитавтоматически —дляэтогонамненадопредприниматьникакихдействий.

fnmain(){letx=42;}

ЯзыкпрограммированияRust

61Стекикуча

Page 62: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

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

А ещёмы не говорили о том, чтоже означает название «стек».Для этогомы должныпривестинемногоболеесложныйпример:

Эта программа имеет в общей сложности три переменные: две вfoo() и одну вmain() . Так же как и раньше, когда вызываетсяmain() , в её стековом кадре выделяетсяодно целое число. Но, прежде чем мы сможем показать, что происходит, когда вызываетсяfoo() , мы должны визуализировать то, что происходит с памятью. Ваша операционнаясистема представляет отображение памяти для вашей программы. Это довольно просто:огромныйсписокадресов,от0добольшогочисла,представляющегоколичествооперативнойпамятиувашегокомпьютера.Например,еслиувасестьгигабайтоперативнойпамяти,товашиадресабудутот0 до1073741823 .Эточислоравно2 ,количествубайтоввгигабайте.

Эта память вроде гигантского массива: адреса начинаются с нуля и продолжаются доконечногочисла.Таквотсхеманашегопервогокадрастека:

Адрес Имя Значение

0 x 42

Унасестьпеременнаяx ,расположеннаяпоадресу0 ,имеющаязначение42 .

Когдавызываетсяfoo() ,выделяетсяновыйстековыйкадр:

Адрес Имя Значение

2 z 100

1 y 5

0 x 42

Поскольку0 было задействовановпервомкадре,длякадраfoo() используются1 и2 .Придальнейшихвызовахфункцийстекбудетрастивверх.

fnfoo(){lety=5;letz=100;}

fnmain(){letx=42;

foo();}

30

ЯзыкпрограммированияRust

62Стекикуча

Page 63: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Здесь необходимо принять к сведению некоторые важные замечания. Адреса 0, 1 и 2приведены исключительно в иллюстративных целях, и не имеют никакого отношения кфактическим адресам, которые компьютербудетиспользовать.В частности, набор адресов вдействительности включает выравнивающие разделители, состоящие из некоторого числабайтов, которые отделяют каждый из адресов. Размер этого разделителя может дажепревышатьразмерхранящегосязначения.

Послетого,какfoo() завершается,еёкадрбудетосвобожден:

Адрес Имя Значение

0 x 42

Апотом,послеmain() ,дажеэтопоследнеезначениеуходит.Легко!

Этоназывается«стек» (по-русски,стопка),потомучтоонработаеткакстопкатарелок:первая тарелка, которую вы положили, будет последней тарелкой, которую вы возьметеобратно. По этой причине стек иногда называют очередью «последним пришел, первымвышел».Последнеезначение,котороевыположиливстек,будетпервым,котороевыполучитеизнего.

Давайтепопробуемтрёх-уровневыйпример:

Сначалавызываетсяmain() :

Адрес Имя Значение

0 x 42

Затемизmain() вызываетсяfoo() :

Адрес Имя Значение

3 c 1

2 b 100

1 a 5

fnbar(){leti=6;}

fnfoo(){leta=5;letb=100;letc=1;

bar();}

fnmain(){letx=42;

foo();}

ЯзыкпрограммированияRust

63Стекикуча

Page 64: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

0 x 42

Изатемизfoo() вызываетсяbar() :

Адрес Имя Значение

4 i 6

3 c 1

2 b 100

1 a 5

0 x 42

Вотчтомыимеливвидураньше,говоря,чтонашстекрастетвверх.

Послетого,какbar() завершается,еёкадрбудетосвобожден,оставляятолькоfoo()иmain() :

Адрес Имя Значение

3 c 1

2 b 100

1 a 5

0 x 42

Азатемзавершаетсяfoo() ,оставляятолькоmain() :

Адрес Имя Значение

0 x 42

Ивотмызакончили.Уловилисуть?Этокакстопкатарелок:выкладетенаверх,иберётесверху.

КучаТакойспособвыделенияпамятиработаеточеньхорошо,ноонможетбытьиспользован

невсегда.Иногдавамнеобходимопередатьнекоторуюпамятьмеждуразличнымифункциямиили сохранить её валидность после окончания выполнения функции. Для этого мы можемиспользоватькучу.

В Rust, вы можете выделить память в куче с помощью упаковки, т.е.типаBox<T> .(Примечаниепереводчика:мыназываемBox<T> упаковкой,потомучтоT какбы«упакован»вBox :упаковказнаетразмертого,чтолежитвнутри.ЭтаинформациязакодированавтипеT ,поэтомувовремяисполнения,дляразмерныхтипов,этопростоуказатель.)Вотпример:

fnmain(){letx=Box::new(5);lety=42;}

ЯзыкпрограммированияRust

64Стекикуча

Page 65: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вотчтопроисходитспамятью,когдавызываетсяmain() :

Адрес Имя Значение

1 y 42

0 x ??????

Мывыделяемместодлядвухпеременныхвстеке.y представляетсобой42 ,тутвсёкакобычно.Ночтонасчётx?Нашx представляетсобойBox<i32> ,аупаковкавыделяетпамятьв куче. Фактическое значение упаковки  — структура, которая хранит указатель на «кучу».Когданачинаетвыполнятьсяфункция,осуществляетсявызовBox::new() ,которыйвыделяетнекоторый объем памяти в куче, и кладет туда5 . Теперь память выглядит следующимобразом:

Адрес Имя Значение

(2 )-1 5

... ... ...

1 y 42

0 x →(2 )-1

Внашемгипотетическомкомпьютереc1Гбоперативнойпамятиимеется2 адресов.Атак как наш стек растет от нуля, то проще всего выделить память с другого конца. Такимобразом, наше первое значение находится на самом высоком месте в памяти. Посколькуструктураx хранитсыройуказатель (rawpointer)наадрес,которыймывыделиливкуче, тозначениеx равно(2 )-1 —этотосамоеместоположениевпамяти.

Мы не слишком много говорили о том, что на самом деле означает «выделить» и«освободить память» в этом контексте. Чрезмерное углубление в детали по этому вопросувыходит за рамки данного руководства, но важно отметить, что куча — это не просто стек,который растет с противоположного конца. Как мы увидим в дальнейших примерах в этойкниге,памятьизкучиможетбытьвыделенаиосвобожденавлюбомпорядке,чтовконечномитогеможетпривестик«дыркам».Вотсхемаразмещенияпамятипрограммы,проработавшейвтечениенекотороговремени:

Адрес Имя Значение

(2 )-1 5

(2 )-2

(2 )-3

(2 )-4 42

... ... ...

3 y →(2 )-4

2 y 42

30

30

30

30

30

30

30

30

30

ЯзыкпрограммированияRust

65Стекикуча

Page 66: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

1 y 42

0 x →(2 )-1

Вэтомпримеремывыделиличетыреэлементавкуче,ноосвободилилишьдваизних.Отсюда разрыв между (2 ) - 1 и (2 ) - 4, который в настоящее время не используется.Конкретныедетали того, какипочему этопроисходит, зависят от того, какуюстратегиювыиспользуете для управления кучей. Различные программы могут использовать различные«распределители памяти», которые представляют собой библиотеки, которые управляютпамятьюзавас.ПрограммынаRustиспользуютдляэтогоjemalloc.

Ладно,вернемсякнашемупримеру.Таккакэтапамятьрасположенавкуче,тоонаможетоставаться валидной дольше, чем функция, которая выделяет упаковку. В данном случае,однако,этонетак.[ Когдафункциязавершается,мыдолжныосвободитькадрстекадляmain() . Хотя уBox<T> для этого есть свой трюк:Drop. РеализацияDrop дляBoxосвобождаетпамять,котораябылавыделенаприсоздании.Отлично!Поэтому,когдаx уходит,сначалаосвобождаетсяпамять,выделеннаявкуче:

Адрес Имя Значение

1 y 42

0 x ??????

[moving]:Мыможемпродлитьвремяжизнипамятипутемпередачиправасобственности,что иногда называют «перемещение из упаковки» («movingout of the box»). Более сложныепримерыбудутрассмотреныпозже.

Апотомкадрстекауходит,освобождаявсюнашупамять.

АргументыизаимствованиеУ нас есть некоторые простые примеры со стеком и кучей, но что насчёт аргументов

функцииизаимствования?ВотнебольшаяпрограмманаRust:

Когдамывходимвmain() ,памятьвыглядитследующимобразом:

Адрес Имя Значение

1 y →0

0 x 5

30

30 30

moving]

fnfoo(i:&i32){letz=42;}

fnmain(){letx=5;lety=&x;

foo(y);}

ЯзыкпрограммированияRust

66Стекикуча

Page 67: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Значениемx является5 , аy представляет собой ссылку наx . То есть, ее значениемявляетсяадреспамяти,покоторомурасположенx .Вданномслучаеэто0 .

Ачтонасчётслучая,когдамывызываемfoo() ,передаваяy вкачествеаргумента?

Адрес Имя Значение

3 z 42

2 i →0

1 y →0

0 x 5

Кадры стека используются не только для локальныхимён, но такжеи для аргументов.Такимобразом,вэтомслучае,нашкадрдолженсодержатькакi ,нашаргумент,такиz ,нашелокальноеимя.i  —этокопияаргументаy .Соответственно,значениемi ,какизначениемy ,является0 .

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

СложныйпримерХорошо,давайтерассмотримследующую,болеесложнуюпрограммушагзашагом:

ЯзыкпрограммированияRust

67Стекикуча

Page 68: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Сначаламывызываемmain() :

Адрес Имя Значение

(2 )-1 20

... ... ...

2 j →0

1 i →(2 )-1

0 h 3

Мывыделяемпамятьдляj ,i ,иh .i выделенавкучеипоэтомусодержитуказательназначениевкуче.

Далее,вконцевызоваmain() ,вызываетсяfoo() :

Адрес Имя Значение

(2 )-1 20

... ... ...

5 z →4

4 y 10

3 x →0

fnfoo(x:&i32){lety=10;letz=&y;

baz(z);bar(x,z);}

fnbar(a:&i32,b:&i32){letc=5;letd=Box::new(5);lete=&d;

baz(e);}

fnbaz(f:&i32){letg=100;}

fnmain(){leth=3;leti=Box::new(20);letj=&h;

foo(j);}

30

30

30

ЯзыкпрограммированияRust

68Стекикуча

Page 69: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

2 j →0

1 i →(2 )-1

0 h 3

Пространствовыделяетсядляx ,y иz .Аргументx имееттакоежезначение,какиj ,таккакмыпередалиj вкачествеаргумента.Этоуказательнаадрес0 ,таккакj указываетнаh .

Далее,foo() вызываетbaz() ,передаваяz :

Адрес Имя Значение

(2 )-1 20

... ... ...

7 g 100

6 f →4

5 z →4

4 y 10

3 x →0

2 j →0

1 i →(2 )-1

0 h 3

Мы выделили память дляf иg . baz() оченькороткая,и когдаона завершается,мыизбавляемсяотеёкадрастека:

Адрес Имя Значение

(2 )-1 20

... ... ...

5 z →4

4 y 10

3 x →0

2 j →0

1 i →(2 )-1

0 h 3

Далееfoo() вызываетbar() саргументамиx иz :

Адрес Имя Значение

(2 )-1 20

(2 )-2 5

30

30

30

30

30

30

30

ЯзыкпрограммированияRust

69Стекикуча

Page 70: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

... ... ...

10 e →9

9 d →(2 )-2

8 c 5

7 b →4

6 a →0

5 z →4

4 y 10

3 x →0

2 j →0

1 i →(2 )-1

0 h 3

Тутмывыделяемдругоезначениевкуче,ипоэтомумывычитаемединицуиз(2 )-1.Это выражение написать легче, чем1 073 741 822 . В любом случае, переменныесоздаются,какобычно.

Вконцеbar() вызываетbaz() :

Адрес Имя Значение

(2 )-1 20

(2 )-2 5

... ... ...

12 g 100

11 f →9

10 e →9

9 d →(2 )-2

8 c 5

7 b →4

6 a →0

5 z →4

4 y 10

3 x →0

2 j →0

1 i →(2 )-1

0 h 3

30

30

30

30

30

30

30

ЯзыкпрограммированияRust

70Стекикуча

Page 71: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Сейчасмынанаибольшейглубине!Поздравляемсдостижениемданнойточки.

Послезавершенияbaz() ,мыизбавляемсяотf иg :

Адрес Имя Значение

(2 )-1 20

(2 )-2 5

... ... ...

10 e →9

9 d →(2 )-2

8 c 5

7 b →4

6 a →0

5 z →4

4 y 10

3 x →0

2 j →0

1 i →(2 )-1

0 h 3

Далеемывыполняемвозвратизbar() .Вэтомслучаеd представляетсобойBox<T> ,поэтомуонтакжеосвобождаетито,начтоонуказывает:(2 )-2.

Адрес Имя Значение

(2 )-1 20

... ... ...

5 z →4

4 y 10

3 x →0

2 j →0

1 i →(2 )-1

0 h 3

Ипослеэтогопроисходитвозвратизfoo() :

Адрес Имя Значение

(2 )-1 20

... ... ...

2 j →0

30

30

30

30

30

30

30

30

ЯзыкпрограммированияRust

71Стекикуча

Page 72: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

1 i →(2 )-1

0 h 3

И вот, наконец,main() , которая очищает все остальное. Когда освобождаетсяi(Drop ),будеттакжеочищениконецкучи.

Ачтоделаютдругиеязыки?Большинствоязыковсосборщикоммусорапоумолчаниювыделяетпамятьизкучи.Это

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

Чтоиспользовать?Но,еслистекбыстрееипрощевуправлении,зачемтогданужнакуча?Весомаяпричина

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

В общем случае, следует предпочитать выделение в стеке, и поэтому, Rust используетвыделениевстекепоумолчанию.LIFOмодельстека(«последнимпришёл —первымвышел»)фундаментальнопроще.Этозначит,чтопрограммабыстрееисполняется,ипрощепосмыслу.

ЭффективностьвовремявыполненияУправлениепамятьюдлястекатривиально:машинапростоувеличиваетилиуменьшает

однозначение,такназываемый«указательстека»(stackpointer).Управлениепамятьюдлякучисложнее: память, выделенная в куче, освобождается в произвольные моменты, а каждаяобласть выделенной в куче памяти может быть произвольного размера. Распределителюпамяти,какправило,требуетсяприложитьгораздобольшеусилийдляопределенияобластей,которыеможноиспользоватьзаново.

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

ПростотапрограммыВыделениепамятивстекевоздействуеткакнасамязыкRust,такинамодельмышления

разработчиков.Стековая семантика —ключевое понятиеRust.Мыполучаем автоматическоеуправление памятью без усложнения среды исполнения. Именно этот механизм позволяетосвободитьпамятьвкуче,кактолькоеёвладелецвышелизобластивидимости —посути,как

30

ЯзыкпрограммированияRust

72Стекикуча

Page 73: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

толькосхлопнулсястеккадра,накоторомонжил.Ксожалению,внекоторыхситуацияхстеканедостаточно. Если нужна большая гибкость во владении памятью, можно воспользоватьсясчётчикамиссылокRc<T> иArc<T> .

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

ЯзыкпрограммированияRust

73Стекикуча

Page 74: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Тестирование

Program testing can be a very effective way to show the presence of bugs, but it ishopelesslyinadequateforshowingtheirabsence.

EdsgerW.Dijkstra,"TheHumbleProgrammer"(1972)

Тестирование программы может быть очень эффективным способом показатьналичиеошибок,ноонобезнадёжнонеподходящедлядоказательстваихотсутствия.

Дейкстра,ЭдсгерВибе,«TheHumbleProgrammer»(1972)

Давайтепоговоримотом,кактестироватькоднаRust.Мынебудемрассказыватьотом,какой подход к тестированию Rust кода является верным. Есть много подходов, каждый изкоторых имеет свое представление о правильном написании тестов. Но все эти подходыиспользуют одни и те же основные инструменты, и мы покажем вам синтаксис ихиспользования.

ТестысатрибутомtestВсамомпростомслучае,тествRust —этофункция,аннотированнаяатрибутомtest .

ДавайтесоздадимновыйпроектCargo,которыйбудетназыватьсяadder :

$cargonewadder$cdadder

При создании нового проекта, Cargo автоматически сгенерирует простой тест. Нижепредставленосодержимоеsrc/lib.rs :

Обратите вниманиена#[test] .Этотатрибутуказывает,чтоэтотестоваяфункция.Вэтомпримереонанеимееттела.Нотакоговидафункциидостаточно,чтобыудачновыполнитьтест.Запусктестовосуществляетсякомандойcargotest .

#[test]fnit_works(){}

ЯзыкпрограммированияRust

74Тестирование

Page 75: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$cargotestCompilingadderv0.0.1(file:///home/you/projects/adder)Runningtarget/adder-91b3e234d4ed382a

running1testtestit_works...ok

testresult:ok.1passed;0failed;0ignored;0measured

Doc-testsadder

running0tests

testresult:ok.0passed;0failed;0ignored;0measured

Cargo скомпилировал и запустил наши тесты. В результате мы получили выходныеданные, поделенные на два раздела: один содержит информацию о тесте, который мынаписали, а другой — информацию о тестах из документации. Но об этом позже. А сейчаспосмотримнаэтустроку:

testit_works...ok

Обратитевниманиенаit_works .Этоназваниенашейфункции:

Мытакжеполучилиитоговуюстроку:

testresult:ok.1passed;0failed;0ignored;0measured

Так почему же наш ничего не делающий тест был выполнен удачно? Любой тест,которыйневызываетpanic! ,выполняетсяудачно,алюбойтест,которыйвызываетpanic! ,выполняетсянеудачно.Давайтесделаемтест,которыйвыполнитсянеудачно:

assert!  — это макрос, определенный в Rust, и принимающий один аргумент: еслиаргументимеетзначениеtrue ,тоничегонепроисходит;еслиаргументявляетсяfalse ,товызываетсяpanic! .Давайтезапустимнашитестыснова:

fnit_works(){

#[test]fnit_works(){assert!(false);}

ЯзыкпрограммированияRust

75Тестирование

Page 76: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$cargotestCompilingadderv0.0.1(file:///home/you/projects/adder)Runningtarget/adder-91b3e234d4ed382a

running1testtestit_works...FAILED

failures:

----it_worksstdout----thread'it_works'panickedat'assertionfailed:false',/home/steve/tmp/adder/src/lib.rs:3

failures:it_works

testresult:FAILED.0passed;1failed;0ignored;0measured

thread'<main>'panickedat'Sometestsfailed',/home/steve/src/rust/src/libtest/lib.rs:247

Rustсообщает,чтонаштествыполненнеудачно:

testit_works...FAILED

Этожеотражаетсявитоговойстроке:

testresult:FAILED.0passed;1failed;0ignored;0measured

Мытакжеполучаемненулевойкодсостояния.Можноиспользовать$? наOSXиLinux:

$echo$?101

НаWindows,есливыиспользуетеcmd :

echo%ERRORLEVEL%

ИесливыиспользуетеPowerShell:

echo$LASTEXITCODE#самкодecho$?#логическое,успешноилинеуспешно

Это бывает полезно, если вы хотите интегрироватьcargo test в стороннийинструмент.

Можно инвертировать ожидаемый результат теста с помощью атрибута:should_panic :

#[test]#[should_panic]fnit_works(){assert!(false);}

ЯзыкпрограммированияRust

76Тестирование

Page 77: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Теперь этот тест будет выполнен удачно, если вызываетсяpanic! , и неудачно, еслиpanic! невызывается.Давайтепопробуем:

$cargotestCompilingadderv0.0.1(file:///home/you/projects/adder)Runningtarget/adder-91b3e234d4ed382a

running1testtestit_works...ok

testresult:ok.1passed;0failed;0ignored;0measured

Doc-testsadder

running0tests

testresult:ok.0passed;0failed;0ignored;0measured

Rustпредоставляетидругоймакрос,assert_eq! ,которыйпроверяетравенстводвухаргументов:

А теперь этот тест будет выполнен удачно или неудачно? Из-за атрибутаshould_panic онзавершитсяудачно:

$cargotestCompilingadderv0.0.1(file:///home/you/projects/adder)Runningtarget/adder-91b3e234d4ed382a

running1testtestit_works...ok

testresult:ok.1passed;0failed;0ignored;0measured

Doc-testsadder

running0tests

testresult:ok.0passed;0failed;0ignored;0measured

Тестыshould_panic могутбытьхрупкими,посколькутрудногарантировать,чтотестне вызовет панику по неожиданной причине. Чтобы помочь в этом аспекте, к атрибутуshould_panic может быть добавлен необязательный параметрexpected . Тогда тесттакже будет проверять, что сообщение об ошибке содержит ожидаемый текст. Нижепредставленболеебезопасныйвариантприведенноговышепримера:

#[test]#[should_panic]fnit_works(){assert_eq!("Hello","world");}

ЯзыкпрограммированияRust

77Тестирование

Page 78: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вотивсе,чтокасаетсяоснов!Давайтенапишемодин«настоящий»тест:

Это распространенное использование макросаassert_eq! : вызывать некоторуюфункциюсизвестнымиаргументамиисравнитьрезультатеёвызовасожидаемымрезультатом.

ТестысатрибутомignoreНекоторыетестымогузаниматьмноговременинавыполнение.Такиетестымогутбыть

отключеныпоумолчаниюспомощьюатрибутаignore :

Теперьзапустимнашитестыивидим,чтоit_works запускается,аexpensive_testнет:

$cargotestCompilingadderv0.0.1(file:///home/you/projects/adder)Runningtarget/adder-91b3e234d4ed382a

running2teststestexpensive_test...ignoredtestit_works...ok

testresult:ok.1passed;0failed;1ignored;0measured

Doc-testsadder

running0tests

testresult:ok.0passed;0failed;0ignored;0measured

#[test]#[should_panic(expected="assertionfailed")]fnit_works(){assert_eq!("Hello","world");}

pubfnadd_two(a:i32)->i32{a+2}

#[test]fnit_works(){assert_eq!(4,add_two(2));}

fnit_works(){assert_eq!(4,add_two(2));}

#[test]#[ignore]fnexpensive_test(){//код,которыйзанимаетчаснавыполнение}

ЯзыкпрограммированияRust

78Тестирование

Page 79: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Дорогостоящиетестымогутбытьзапущеныспомощьюкомандыcargotest----ignored :

$cargotest----ignoredRunningtarget/adder-91b3e234d4ed382a

running1testtestexpensive_test...ok

testresult:ok.1passed;0failed;0ignored;0measured

Doc-testsadder

running0tests

testresult:ok.0passed;0failed;0ignored;0measured

Аргумент--ignored — это аргумент для тестового исполняемого файла, а не дляCargo,именнопоэтомукомандавыглядиттакcargotest----ignored .

ТестывмодулеtestЕстьодиннюанс,из-закоторогонашпримернельзяназватьидиоматичным:отсутствует

модультестирования.Идиоматичныйвариантнашегопримерабудетвыглядетьпримернотак:

Здесьестьнесколькоизменений.Первое —этовведениеmodtest с атрибутомcfg .Модуль позволяет сгруппировать все наши тесты вместе, а также, если нужно, определитьвспомогательныефункции, которыебудут отделеныот остальнойчасти контейнера.Атрибутcfg указываетна то, что тестбудет скомпилирован, толькокогдамыпопытаемся запуститьтесты. Это может сэкономить время компиляции, а также гарантирует, что наши тестыполностьюисключеныизобычнойсборки.

Второеизменениезаключаетсявобъявленииuse .Таккакмынаходимсявовнутреннеммодуле,томыдолжныобъявитьиспользованиетестируемойфункциивегообластивидимости.Этоможетраздражать,еслиувасбольшоймодуль,ипоэтомуобычноиспользуютвозможностьglob .Давайтеизменимsrc/lib.rs соответствующимобразом:

pubfnadd_two(a:i32)->i32{a+2}

#[cfg(test)]modtest{usesuper::add_two;

#[test]fnit_works(){assert_eq!(4,add_two(2));}}

ЯзыкпрограммированияRust

79Тестирование

Page 80: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Обратитевниманиенаразличиевстрокесuse .Теперьзапустимнашитесты:

$cargotestUpdatingregistry`https://github.com/rust-lang/crates.io-index`Compilingadderv0.0.1(file:///home/you/projects/adder)Runningtarget/adder-91b3e234d4ed382a

running1testtesttest::it_works...ok

testresult:ok.1passed;0failed;0ignored;0measured

Doc-testsadder

running0tests

testresult:ok.0passed;0failed;0ignored;0measured

Работает!

Данный подход представляет собой использование модуляtest , содержащегомодульныетесты(unittests).Любойкод,задачейкоторогоявляетсятольколишьтестированиенебольшогокусочкафункциональности,имеетсмыслперенестивэтотмодуль.Ночтоеслимыхотим написать «интеграционные тесты» (integration tests)? Для этого следует использоватьдиректориюtests .

ТестывдиректорииtestsЧтобынаписатьинтеграционныйтест,давайтесоздадимдиректориюtests ,иположим

внеефайлtests/lib.rs соследующимсодержимым:

pubfnadd_two(a:i32)->i32{a+2}

#[cfg(test)]modtest{usesuper::*;

#[test]fnit_works(){assert_eq!(4,add_two(2));}}

externcrateadder;

#[test]fnit_works(){assert_eq!(4,adder::add_two(2));}

ЯзыкпрограммированияRust

80Тестирование

Page 81: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Выглядитпримернотакже,какинашипредыдущиетесты,ноестьнекоторыеотличия.Теперьсверхуунасexterncrateadder .Этопотому,чтотестывдиректорииtests  —этоотдельныйконтейнер,и,следовательно,мыдолжныкомпоноватьсяснашейбиблиотекой.Этотакжеобъясняет,почемудиректорияtests  —наиболееподходящееместодлянаписанияинтеграционных тестов: они используют библиотеку, как это делал бы любой другойпотребитель.

Давайтезапустимих:

$cargotestCompilingadderv0.0.1(file:///home/you/projects/adder)Runningtarget/adder-91b3e234d4ed382a

running1testtesttest::it_works...ok

testresult:ok.1passed;0failed;0ignored;0measured

Runningtarget/lib-c18e7d3494509e74

running1testtestit_works...ok

testresult:ok.1passed;0failed;0ignored;0measured

Doc-testsadder

running0tests

testresult:ok.0passed;0failed;0ignored;0measured

Теперьунаспоявилосьтрираздела:запускаетсястарыймодульныйтест,атакженовыйинтеграционныйтест.

Этовсе, чтокасаетсядиректорииtests . Модульtest здесьненужен, таккак здесьвсёотноситсяктестам.

Давайте,наконец,перейдемктретьейчасти:тестывдокументации.

ТестывдокументацииНет ничего лучше, чем документация с примерами. Нет ничего хуже, чем примеры,

которыенасамомделенеработают,потомучтокодизменилсястехпор,какбыланаписанадокументация. Для того, чтобы такой ситуации не возникало, Rust поддерживаетавтоматический запуск примеров в документации (имейте ввиду, что это работает только сбиблиотеками).Вотдополненныйsrc/lib.rs спримерами:

ЯзыкпрограммированияRust

81Тестирование

Page 82: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Обратите внимание на документацию уровня модуля, начинающуюся с//! и надокументацию уровня функции, начинающуюся с/// . Документация Rust поддерживаетMarkdown в комментариях, поэтому блоки кода помечают тройными символами `. Вкомментарии документации обычно включают раздел#Examples , содержащий примеры,такиекакэтот.(Примечаниепереводчика:заголовок#Examples имеетособоезначение:егонельзя написать по-другому или написать на русском языке, иначеRust не найдёт примеровкодавдокументации.)

Давайтезапустимтестыснова:

//!Контейнер`adder`предоставляетфункциисложениячисел.//!//!#Examples//!//!```//!assert_eq!(4,adder::add_two(2));//!```

///Этафункцияприбавляет2ксвоемуаргументу.//////#Examples//////```///useadder::add_two;//////assert_eq!(4,add_two(2));///```pubfnadd_two(a:i32)->i32{a+2}

#[cfg(test)]modtest{usesuper::*;

#[test]fnit_works(){assert_eq!(4,add_two(2));}}

ЯзыкпрограммированияRust

82Тестирование

Page 83: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$cargotestCompilingadderv0.0.1(file:///home/steve/tmp/adder)Runningtarget/adder-91b3e234d4ed382a

running1testtesttest::it_works...ok

testresult:ok.1passed;0failed;0ignored;0measured

Runningtarget/lib-c18e7d3494509e74

running1testtestit_works...ok

testresult:ok.1passed;0failed;0ignored;0measured

Doc-testsadder

running2teststestadd_two_0...oktest_0...ok

testresult:ok.2passed;0failed;0ignored;0measured

Теперь у нас запускаются все три вида тестов!Обратите вниманиена имена тестов издокументации:_0 генерируется для модульных тестов, иadd_two_0   — дляфункциональных тестов. Цифры на конце будут увеличиваться автоматически, если выдобавите еще примеров. Например, при добавлении ещё одного функционального теста, онполучитимяadd_two_1 .

Мынерассмотреливседеталинаписаниятестоввдокументации.ПодробнеесмотритеглавуДокументация.

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

ЯзыкпрограммированияRust

83Тестирование

Page 84: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

УсловнаякомпиляцияВ Rust есть специальный атрибут,#[cfg] , который позволяет компилировать код в

зависимостиотфлагов,переданныхкомпилятору.Онимеетдвеформы:

Надатрибутамиконфигурацииопределенылогическиеоперации:

Онимогутбытькакугодновложены:

Чтожекасаетсятого,каквключитьилиотключитьэтифлаги:есливыиспользуетеCargo,тоониустанавливаютсявразделе[features] вашегоCargo.toml :

[features]#поумолчанию,никакихдополнительныхвозможностейdefault=[]

#возможность«secure-password»зависитотпакетаbcryptsecure-password=["bcrypt"]

Есливыопределитетакиевозможности,Cargoпередастфлагвrustc :

--cfgfeature="${feature_name}"

Совокупность этих флагов конфигурации (cfg ) будет определять, какие из них будутактивны,и,следовательно,какойкодбудетскомпилирован.Давайтерассмотримтакойкод:

Если скомпилировать его с помощьюcargo build --features "foo" , то вrustc будет передан флаг--cfgfeature="foo" , и результат будет содержать модульmod foo . Если скомпилировать его с помощью обычной командыcargo build , тоникаких дополнительных флагов передано не будет, и поэтому, модульmod foo будетотсутствовать.

cfg_attr

#[cfg(foo)]

#[cfg(bar="baz")]

#[cfg(any(unix,windows))]

#[cfg(all(unix,target_pointer_width="32"))]

#[cfg(not(foo))]

#[cfg(any(not(unix),all(target_os="macos",target_arch="powerpc")))]

#[cfg(feature="foo")]modfoo{}

ЯзыкпрограммированияRust

84Условнаякомпиляция

Page 85: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вы также можете установить другой атрибут в зависимости от переменнойcfg спомощьюатрибутаcfg_attr :

Этоткодбудетравносиленатрибуту#[b] ,есливатрибутеcfg установленфлагa ,или«безатрибута»впротивномслучае.

cfg!Расширениесинтаксиса cfg! позволяетиспользоватьданныевидыфлагови вдругом

местевкоде:

Значение флага будет заменено наtrue илиfalse во время компиляции, взависимостиотнастройкиконфигурации.

#[cfg_attr(a,b)]

ifcfg!(target_os="macos")||cfg!(target_os="ios"){println!("ThinkDifferent!");}

ЯзыкпрограммированияRust

85Условнаякомпиляция

Page 86: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ДокументацияДокументация является важной частью любого программного проекта, и в Rust ей

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

ОrustdocДистрибутив Rust включает в себя инструмент,rustdoc , который генерирует

документацию.rustdoc такжеиспользуетсяCargoчерезcargodoc .

Документация может быть сгенерирована двумя методами: из исходного кода, и изотдельныхфайловвформатеMarkdown.

ДокументированиеисходногокодаОсновной способ документирования проекта на Rust заключается в комментировании

исходногокода.Дляэтойцеливыможетеиспользоватьдокументирующиекомментарии:

Этот код генерирует документацию, которая выглядиттак. В приведенном кодереализацияметодабылазамененанаобычныйкомментарий.Первое,начтоследуетобратитьвнимание в этомпримере, это на использование/// вместо// . Символы/// указывают,чтоэтодокументирующийкомментарий.

ДокументирующиекомментариипишутсянаMarkdown.

Rustотслеживаеттакиекомментарии,ииспользуетихприсозданиидокументации.

При документировании таких вещей, как перечисления, нужно учитывать некоторыеособенностиработыrustdoc .Такойкодработает:

///Создаётновый`Rc<T>`.//////#Examples//////```///usestd::rc::Rc;//////letfive=Rc::new(5);///```pubfnnew(value:T)->Rc<T>{//здесьреализация}

ЯзыкпрограммированияRust

86Документация

Page 87: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Атакой —нет:

Выполучитеошибку:

hello.rs:4:1:4:2error:expectedident,found`}`hello.rs:4}^

Эта досаднаяошибка заключается в следующем: комментарии документациираспространяются на элементы, расположенные за ними, а в данном примере нет элемента,расположенногопослепоследнегокомментария.

НаписаниекомментариевдокументацииДавайтерассмотримкаждуючастьприведенногокомментариявдеталях:

Первая строка документирующего комментария должна представлять из себя краткуюинформацию о функциональности. Одно предложение. Только самое основное.Высокоуровневое.

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

Специальныеразделы

///Тип`Option`.Подробнеесмотрите[документациюуровнямодуля](http://doc.rust-lang.org/).enumOption<T>{///НетзначенияNone,///Некотороезначение`T`Some(T),}

///Тип`Option`.Подробнеесмотрите[документациюуровнямодуля](http://doc.rust-lang.org/).enumOption<T>{None,///НетзначенияSome(T),///Некотороезначение`T`}

///Создаётновый`Rc<T>`.

//////Подробностисоздания`Rc<T>`,возможно,описывающиесложностисемантики,///дополнительныеопции,ивсёостальное.///

///#Examples

ЯзыкпрограммированияRust

87Документация

Page 88: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Далееидутспециальныеразделы.Ониобознаютсязаголовком,которыйначинаетсяс# .Существуюттривидазаголовков,которыеобычноиспользуются.Онинеявляютсякаким-либоспециальнымсинтаксисом,наданныймоментэтопростосоглашение.

РазделPanics . Неустранимые ошибки при неправильном вызове функции (такназываемые ошибки программирования) в Rust, как правило, вызывают панику, которая, вкрайнем случае, убивает весь текущий поток (thread). Если ваша функция имеет подобноенетривиальное поведение  — т.е. обнаруживает/вызывает панику, то очень важнозадокументироватьэто.

РазделFailures . Если ваша функция или метод возвращаетResult<T, E> , тохорошим тоном является описание условий, при которыхона возвращаетErr(E) .Эточутьменееважно,чемописаниеPanics ,потомукакнеудачакодируетсявсистеметипов,ноэтонезначит,чтостоитпренебрегатьданнойвозможностью.

РазделSafety . Если ваша функция являетсяunsafe , необходимо пояснить, какиеинвариантывызовадолжныподдерживаться.

РазделExamples .Включитевэтотразделодинилинесколькопримеровиспользованияфункции или метода, и ваши пользователи будут вам благодарны. Примеры должныразмещаться внутри блоков кода, о которыхмы сейчас поговорим.Этот разделможет иметьболееодногоподраздела:

///#Panics

///#Failures

///#Safety

///#Examples//////```///usestd::rc::Rc;//////letfive=Rc::new(5);///```

///#Examples//////Простыеобразцытипа`&str`://////```///letv:Vec<&str>="ИбылаунихкурочкаРяба".split('').collect();///assert_eq!(v,vec!["И","была","у","них","курочка","Ряба"]);///```//////Болеесложныеобразцысзамыканиями://////```///letv:Vec<&str>="абв1где2жзи".split(|c:char|c.is_numeric()).collect();///assert_eq!(v,vec!["абв","где","жзи"]);///```

ЯзыкпрограммированияRust

88Документация

Page 89: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Давайтеподробнообсудимблокикода.

БлоккодаЧтобынаписатькоднаRustвкомментарии,используйтесимволы```:

Есливыхотитенаписатькодналюбомдругомязыке (ненаRust), выможетедобавитьаннотацию:

Это позволит использовать подсветку синтаксиса, соответствующую тому языку,который был указан в аннотации. Если же это простой текст, то в аннотации указываетсяtext .

Важно выбрать правильную аннотацию, потому чтоrustdoc использует ееинтересным способом: Rust может выполнять проверку работоспособности примеров намоментсозданиядокументации.Этопозволяетизбежатьустареванияпримеров.Предположим,у вас есть код на C. Если вы опустите аннотацию, указывающую, что это код на C, тоrustdoc будет думать, что это код на Rust, поэтому он пожалуется при попытке созданиядокументации.

ТестывдокументацииДавайтеобсудимнашпримердокументации:

Заметьте, что здесь нет нужды вfn main() или чём-нибудь подобном.rustdocавтоматически добавит оборачивающийmain() вокруг вашего кода в нужном месте.Например:

Вконечномитогеэтобудеттест:

///```///println!("Привет,мир");///```

///```c///printf("Hello,world\n");///```

///```///println!("Привет,мир");///```

///```///usestd::rc::Rc;//////letfive=Rc::new(5);///```

fnmain(){usestd::rc::Rc;letfive=Rc::new(5);}

ЯзыкпрограммированияRust

89Документация

Page 90: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вотполныйалгоритм,которыйrustdoc используетдляобработкипримеров:

1. Любые ведущие (leading) атрибуты#![foo] остаются без изменений в качествеатрибутовконтейнера.

2. Будут вставлены некоторые общие атрибутыallow , в том числе:unused_variables , unused_assignments , unused_mut ,unused_attributes , dead_code . Небольшие примеры часто приводят ксрабатываниюэтиханализов.

3. Если пример не содержитextern crate , то будет вставленоextern crate<mycrate>; .

4. Наконец, если пример не содержитfn main , то оставшаяся часть текста будетобернутавfnmain(){your_code}

Хотяиногдаэтогонедостаточно.Например,чтонасчётвсехэтихпримеровкодас/// ,окоторыхмыговорили?Простойтекст,обработанныйrustdoc ,выглядиттак:

///Некотораядокументация.#fnfoo(){}

АисходныйтекстнаRustпослеобработкивыглядиттак:

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

Нижеприведеноотрисованноеобъяснениеэтогокода.

Спервамыустанавливаемx равнымпяти:

Затеммыустанавливаемy равнымшести:

Вконцемыпечатаемсуммуx иy :

Авоттожесамоеобъяснение,новвидепростоготекста:

///Некотораядокументация.

letx=5;lety=6;println!("{}",x+y);

letx=5;

lety=6;

println!("{}",x+y);

ЯзыкпрограммированияRust

90Документация

Page 91: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Спервамыустанавливаемx равнымпяти:

letx=5;#lety=6;#println!("{}",x+y);

Затеммыустанавливаемy равнымшести:

#letx=5;lety=6;#println!("{}",x+y);

Вконцемыпечатаемсуммуx иy :

#letx=5;#lety=6;println!("{}",x+y);

Повторяявсечастипримера,выможетебытьуверены,чтовашпримеркомпилируется,ане просто отображает кусочки кода, которые как-то относятся к той или иной части вашегообъяснения.

ДокументированиемакросовВотпримердокументированиямакроса:

Внемвыможетезаметитьтривещи.Во-первых,мыдолжнысобственноручнодобавитьстроку сexterncrate для того, чтобы мы могли указать атрибут#[macro_use] .Во-вторых, мы также собственноручно должны добавитьmain() . И наконец, разумно будет

///Паниковатьсданнымсообщением,еслитольковыражениенеявляетсяистиной.//////#Examples//////```///##[macro_use]externcratefoo;///#fnmain(){///panic_unless!(1+1==2,"Математикасломалась.");///#}///```//////```should_panic///##[macro_use]externcratefoo;///#fnmain(){///panic_unless!(true==false,"Ясломан.");///#}///```#[macro_export]macro_rules!panic_unless{($condition:expr,$($rest:expr),+)=>({if!$condition{panic!($($rest),+);}});}

ЯзыкпрограммированияRust

91Документация

Page 92: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

ЗапусктестоввдокументацииДлязапускатестовможноиспользоватьоднуиздвухкомманд

$rustdoc--testpath/to/my/crate/root.rs#или$cargotest

Все верно,cargotest такжевыполняеттесты,встроенныевдокументацию.Темнеменее,cargotest не будет тестировать исполняемые контейнеры, только библиотечные.Это связано с тем, как работаетrustdoc : он компонуется с библиотекой, которую надопротестировать,новслучаесисполняемымфайломкомпоноватьсянесчем.

Есть еще несколько полезных аннотаций, которые помогаютrustdoc работатьправильнопритестированиикода:

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

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

Аннотацияno_run указывает, что код должен компилироваться, но запускать его навыполнение не требуется. Это важно для таких примеров, которые должны успешнокомпилироваться,новыполнениекоторыхоказываетсябесконечнымциклом!Например:«Воткакзапуститьсетевойсервис».

ДокументированиемодулейRust предоставляет ещё один вид документирующих комментариев,//! . Этот

комментарийотноситсянекследующемузанимэлементу,акэлементу,которыйеговключает.Другимисловами:

///```ignore///fnfoo(){///```

///```should_panic///assert!(false);///```

///```no_run///loop{///println!("Привет,мир");///}///```

ЯзыкпрограммированияRust

92Документация

Page 93: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Приведённый пример демонстрирует наиболее распространённое использование//! :документированиемодуля.Еслижемодульрасположенвфайлеfoo.rs ,товы,открываяегокод,частобудетевидетьследующее:

СтильдокументирующихкомментариевИзучитеRFC505дляполученияполных сведенийо соглашенияхпо стилюиформату

документации.

ДругаядокументацияВсе эти правила поведения также применимы и в отношении исходных файлов не на

Rust.ТаккаккомментариипишутсянаMarkdown,точастоэтифайлыимеютрасширение.md .

Когда выпишетедокументациювфайлахMarkdown, вамненужнодобавлятьпрефиксдокументирующегокомментария,/// .Например:

преобразуетсяв

#Examples

```usestd::rc::Rc;

letfive=Rc::new(5);```

когдаоннаходитсявфайлеMarkdown.Однакоестьодиннедостаток:файлыMarkdownдолжныиметьзаголовокнаподобиеэтого:

%Заголовок

Этопримердокументации.

modfoo{//!Этодокументациядлямодуля`foo`.//!//!#Examples

//...}

//!Модульиспользованияразных`foo`.//!//!Модуль`foo`содержитмногополезнойфункциональностила-ла-ла

///#Examples//////```///usestd::rc::Rc;//////letfive=Rc::new(5);///```

ЯзыкпрограммированияRust

93Документация

Page 94: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Строка,начинающаясяс% ,должнабытьсамойпервойстрокойфайла.

АтрибутыdocНаболееглубокомуровне,комментариидокументации —этосинтаксическийсахардля

атрибутовдокументации:

Т.е.представленныевышекомментарииидентичны,такжекакиниже:

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

Ре-экспортrustdoc будет показывать документацию для общедоступного (public) ре-экспорта в

двухместах:

Это создаст документацию дляbar как в документации для контейнераfoo , таки вдокументацииквашемуконтейнеру.Тоестьвобоихместахбудетиспользованаоднаитажедокументация.

Такоеповедениеможетбытьподавленоспомощьюno_inline :

УправлениеHTMLВы можете управлять некоторыми аспектами HTML, который генерируетrustdoc ,

черезатрибут#![doc] :

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

///this

#[doc="this"]

//!this

#![doc="///this"]

externcratefoo;

pubusefoo::bar;

externcratefoo;

#[doc(no_inline)]pubusefoo::bar;

#![doc(html_logo_url="http://www.rust-lang.org/logos/rust-logo-128x128-blk-v2.png",html_favicon_url="http://www.rust-lang.org/favicon.ico",html_root_url="http://doc.rust-lang.org/")];

ЯзыкпрограммированияRust

94Документация

Page 95: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Опциигенерацииrustdoc также содержит несколько опций командной строки для дальнейшей

настройки:

--html-in-header FILE : включить содержимое FILE в конец раздела<head>...</head> .--html-before-content FILE : включить содержимое FILE сразу после<body> ,передотображаемымсодержимым(втомчислестрокипоиска).--html-after-content FILE : включить содержимое FILE после всегоотображаемогосодержимого.

ЗамечаниепобезопасностиКомментарии в документации в формате Markdown помещаются в конечную веб-

страницубезобработки.БудьтеосторожнысHTML-литералами:

///<script>alert(document.cookie)</script>

ЯзыкпрограммированияRust

95Документация

Page 96: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ИтераторыДавайтепоговоримоциклах.

Помнитециклfor вRust?Вотпример:

Теперь,когдавызнаетеоRustнемногобольше,мыможемдетальнообсудить,какжеэтоработает. Диапазоны (0..10 ) являются «итераторами». Итератор  — это сущность, длякоторой мы можем неоднократно вызвать метод.next() , в результате чего мы получимпоследовательностьэлементов.

Какпредставленониже:

Мы связываем с диапазоном изменяемое имя, которая и является нашим итератором.Затем мы используем циклloop с внутренней конструкциейmatch . Здесьmatchприменяется к результатуrange.next() , который выдает нам ссылку на следующеезначениеитератора.Вданномслучаеnext возвращаетOption<i32> ,которыйпредставляетсобойSome(i32) когдаунасестьзначениеиNone когдапереборэлементовзакончен.Еслимы получаемSome(i32) , топечатаемего, а еслиNone , топрекращаемвыполнениециклаоператоромbreak .

Этотпример,побольшомусчету,делаеттожесамое,чтоипримерсцикломfor .Циклfor  —простоудобныйспособзаписиконструкцииloop /match /break .

Однако, циклfor не является единственной конструкцией, которая используетитераторы. Написание своего собственного итератора заключается в реализации типажаIterator .Хотя эта темаи выходит за рамкиданногоруководства,Rustпредоставляетрядполезныхитераторовдлявыполненияразличныхзадач.Преждечеммыпоговоримоних,мыдолжны рассказать о плохой практике в Rust, связанной с использованием диапазонов. Онапродемонстрированавпримерениже.

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

forxin0..10{println!("{}",x);}

letmutrange=0..10;

loop{matchrange.next(){Some(x)=>{println!("{}",x);},None=>{break}}}

ЯзыкпрограммированияRust

96Итераторы

Page 97: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Есть две причины предпочесть прямое использование итератора. Во-первых, это яснеевыражает наше намерение. Мы обходим элементы вектора, а не индексы с последующейиндексациейвектора.Во-вторых,этаверсияявляетсяболееэффективной:перваяверсиябудетвыполнятьдополнительныепроверкиграниц,потомучтоиспользуетсяиндексация,nums[i] .Вовторомпримеренетникакихпроверокграниц,посколькумыполучаемссылкинакаждыйэлемент вектора, одну за одной, по мере итерирования. Это очень распространенный приемработыситераторами:мыможемигнорироватьненужныепроверкиграниц,новсеещебытьуверенными,чтомывбезопасности.

Остается неясной еще одна деталь работыprintln! . На самом делеnum имеет тип&i32 . То есть, это ссылка наi32 , а не самi32 . println! выполняет разыменованиепеременной за нас, поэтому мы не видим его в исходном коде. Этот код также прекрасноработает:

Здесь мы явно разыменовываемnum . Почему&nums выдаетнамссылки?Во-первых,потомучтомыявнопопросилиегообэтомспомощью& .Во-вторых,еслионбудетвыдаватьнам сами данные, то мы должны быть их владельцем, что подразумевает создание копииданныхивыдачуэтойкопиинам.Соссылкамижемыпростозаимствуемссылкунаданные,ипоэтомубудетвыданапростоссылка,безнеобходимостиперемещатьданные.

Теперь,когдамыустановили,чтозачастуюдиапазоны —этонето,чтонужно,давайтепоговоримотом,чтожеможноиспользоватьвместодиапазонов.

Есть три основных класса объектов, которые имеют отношение к данному вопросу:итераторы,адаптерыитераторовипотребители.Вотнекоторыеопределения:

итераторывыдаютпоследовательностьзначений;адаптерыитераторовприменяютсякитераторуивыдаютновыйитераторсдругойвыходнойпоследовательностью;

letnums=vec![1,2,3];

foriin0..nums.len(){println!("{}",nums[i]);}

letnums=vec![1,2,3];

fornumin&nums{println!("{}",num);}

letnums=vec![1,2,3];

fornumin&nums{println!("{}",*num);}

ЯзыкпрограммированияRust

97Итераторы

Page 98: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

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

ПотребителиПотребитель применяется к итератору, возвращая какое-то значение или значения.

Наиболеераспространеннымпотребителемявляетсяcollect() .Этоткоднекомпилируется,ноонпоказываетидею:

Как выможете видеть,мы вызываемcollect() для нашего итератора.collect()принимает столько значений, сколько выдаститератор, и возвращает коллекциюрезультатов.Так почему же этот код не компилируется? Rust не может определить, в какую коллекцию(например, вектор, список, и т.д.) вы хотите собрать элементы, и поэтому тип необходимоуказатьявно.Вотверсия,котораякомпилируется:

Если помните, синтаксис::<> позволяет задать подсказку типа. Поэтому вприведенномпримеремыуказали,чтохотимвекторцелыхчисел.Хотяневсегдабываетнужнозадавать весь тип целиком. Использование символа_ позволит вам задать частичнуюподсказкутипа:

Эта запись говорит компилятору Rust: «Пожалуйста, собери элементы вVec<T> , авывод типаT сделай самостоятельно». По этой причине символ_ иногда называют«заполнителемтипа».

collect() является наиболее распространенным из потребителей, но есть и другие.Напримерfind() :

find принимает замыкание, которое обрабатывает ссылку на каждый элементитератора.Замыканиевозвращаетtrue ,еслиэлементявляетсяискомымэлементом,иfalseв противном случае. Так как нам не всегда удается найти соответствующий элемент,findвозвращаетOption ,анесамэлемент.

Ещеодинважныйпотребитель —fold .Воткаконвыглядит:

letone_to_one_hundred=(1..101).collect();

letone_to_one_hundred=(1..101).collect::<Vec<i32>>();

letone_to_one_hundred=(1..101).collect::<Vec<_>>();

letgreater_than_forty_two=(0..100).find(|x|*x>42);

matchgreater_than_forty_two{Some(_)=>println!("Унасестьнесколькочисел!"),None=>println!("Числаненайдены:("),}

ЯзыкпрограммированияRust

98Итераторы

Page 99: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

fold()   — это потребитель, который схематично можно представить в виде:fold(base,|accumulator,element|...) .Онпринимаетдвааргумента:первый-это элемент, называемыйбазой; второй  — это замыкание, которое, в свою очередь, самопринимает два аргумента: первый называетсяаккумулятор, а второй -элемент. На каждойитерации вызывается замыкание, результат выполнения которого становится значениемаккумуляторанаследующейитерации.Напервойитерациизначениеаккумулятораравнобазе.

Этонемногозапутанно.Давайтерассмотримзначениявсехэлементовитератора:

база аккумулятор элемент результатзамыкания

0 0 1 1

0 1 2 3

0 3 3 6

Мывызвалиfold() сэтимиаргументами:

Таким образом,0  —этобаза,sum  —это аккумулятор, аx  —этоэлемент.Напервойитерации мы устанавливаемsum равной0 , аx становится первым элементомnums , 1 .Затеммыприбавляемx кsum ,чтодаетнам0+1=1 .Навторойитерацииэтозначениестановится значением аккумулятора,sum , а элемент становится вторым элементоммассива,2 .1+2=3 ,результатэтоговыражениястановитсязначениемаккумуляторанапоследнейитерации.Наэтойитерации,x становитсяпоследнимэлементом,3 ,азначениевыражения3+3=6 являетсяконечнымзначениемнашейсуммы.1+2+3=6  —эторезультат,которыймыполучили.

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

Потребители имеют очень большое значение в связи с одним свойством итераторов, окотороммыещенеговорили:ленивость.Давайтеещёнемногопоговоримобитераторах,ивыпоймете,почемупотребителитакважны.

ИтераторыКак мы уже говорили ранее, итератор являются сущностью, для которой мы можем

неоднократно вызвать метод.next() , в результате чего мы получим последовательностьэлементов.Дляполучениякаждогоследующегоэлементанужновызватьметод,аэтоозначает,чтоитераторыленивы —онинеобязанысоздаватьвсезначениязаранее.Например,этоткоднасамом деле не генерирует номера1-99 , а просто создает значение, представляющее этупоследовательность:

letsum=(1..4).fold(0,|sum,x|sum+x);

.fold(0,|sum,x|sum+x);

letnums=1..100;

ЯзыкпрограммированияRust

99Итераторы

Page 100: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Теперьcollect() потребует, чтобы диапазон выдавал ему какие-нибудь числа,поэтомуонсгенерируетпоследовательность.

Диапазоны —этоодиниздвухосновныхтиповитераторов.Другойчастоиспользуемыйитератор  —iter() . iter() может преобразовать вектор в простой итератор, которыйвыдаетвамкаждыйэлементпоочереди:

Этидваосновныхитераторахорошопослужатвам.Естьиболеепродвинутыеитераторы,втомчислеите,которыегенерируютбесконечнуюпоследовательность.

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

АдаптерыитераторовАдаптерыитераторов получают итератор и изменяют его каким-то образом, выдавая

новыйитератор.Простейшийизнихназываетсяmap :

map вызывается для итератора, и создает новый итератор, каждый элемент которогополучаетсяврезультатевызовазамыкания,вкачествеаргументакоторомупередаетсяссылкана исходный элемент. Так что этот код выдаст нам числа2-100 . Ну, почти! Если выскомпилируетепример,этоткодвыдастпредупреждение:

warning:unusedresultwhichmustbeused:iteratoradaptorsarelazyanddonothingunlessconsumed,#[warn(unused_must_use)]onbydefault(1..100).map(|x|x+1);^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Причина этого  — ленивость итераторов! То замыкание никогда не будет выполнено.Примернижененапечатаетниодногозначения:

Если вы пытаетесь выполнить замыкание ради побочных эффектов (вроде печати), товместоэтогопростоиспользуйтеfor .

Есть масса интересных адаптеров итераторов.take(n) вернет итератор,представляющийследующиеn элементовисходногоитератора.Обратитевнимание,чтоэтонеоказываетникакоговлияниянаоригинальныйитератор.Давайтепопробуемприменитьегодля

letnums=(1..100).collect::<Vec<i32>>();

letnums=vec![1,2,3];

fornuminnums.iter(){println!("{}",num);}

(1..100).map(|x|x+1);

(1..100).map(|x|println!("{}",x));

ЯзыкпрограммированияRust

100Итераторы

Page 101: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

бесконечныхитераторов,которыемыупоминалираньше:

Этоткоднапечатает

16111621

filter() представляет собой адаптер, который принимает замыкание в качествеаргумента. Это замыкание возвращаетtrue илиfalse . Новый итератор, полученныйприменениемfilter() , будет выдавать только те элементы, для которых замыканиевозвращаетtrue :

Этотпримербудетпечататьвсечетныечислаотодногодоста.(Обратитевнимание,чтомы используем образец&x , чтобы извлечь само целое число. Это необходимо, посколькуfilter не потребляет элементы, которые выдаются во время итерации, а лишь выдаётссылку.)

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

Этоткодвыдаствектор,содержащий6 ,12 ,18 ,24 ,30 .

Это просто небольшой обзор того, как итераторы, адаптеры итераторов и потребителимогутпомочьвам.Уженаписаномножестводействительнополезныхитераторов,ивытакжеможете написать свой собственный итератор. Итераторы обеспечивают безопасный иэффективный способ работы со всеми видами списков. Сперва работать с ними немногонепривычно, но чем больше вы с ними сталкиваетесь, тем больше они вас цепляют. Дляполучения полного списка различных итераторов, адаптеров и потребителей смотритедокументациюмодуляiter.

foriin(1..).step_by(5).take(5){println!("{}",i);}

foriin(1..100).filter(|&x|x%2==0){println!("{}",i);}

(1..).filter(|&x|x%2==0).filter(|&x|x%3==0).take(5).collect::<Vec<i32>>();

ЯзыкпрограммированияRust

101Итераторы

Page 102: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЯзыкпрограммированияRust

102Итераторы

Page 103: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

МногозадачностьМногозадачность и параллелизм являются невероятно важными проблемами в

информатике.Этоактуальнаятемадлясовременнойиндустрии.Укомпьютероввсебольшеибольшеядер,номногиепрограммистынеготовывполноймереиспользоватьих.

СредстваRustдлябезопаснойработыспамятьювполноймереприменимыиприработевмногозадачнойсреде.ДажемногозадачныепрограммынаRustдолжныбезопасноработатьспамятью,инесоздаватьсостоянийгонокподанным.СистематиповRustдостаточномощна,чтобысправитьсясэтимизадачаминаэтапекомпиляции.

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

Справочнаяинформация:Send иSyncРассуждатьомногозадачностидовольнотрудно.Rustстрогостатическитипизирован,и

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

SendПервыйтипаж,окотороммыбудемговорить,называетсяSend .КогдатипT реализует

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

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

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

SyncВторойизэтихтипажейназываетсяSync .КогдатипT реализуетSync ,этоуказывает

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

ЯзыкпрограммированияRust

103Многозадачность

Page 104: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Например, совместное использование неизменяемых данных с помощью атомарногосчетчика ссылок является потокобезопасным. Rust обеспечивает такой тип,Arc<T> , и онреализуетSync , так что при помощи этого типа можно безопасно обмениваться даннымимеждупотоками.

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

ПотокиСтандартная библиотека Rust предоставляет библиотеку многопоточности, которая

позволяет запускать код на Rust параллельно. Вот простой пример использованияstd::thread :

Методthread::spawn() вкачествеединственногоаргументапринимаетзамыкание,котороевыполняетсявновомпотоке.Онвозвращаетдескрипторпотока,которыйиспользуетсядляожиданиязавершенияэтогопотокаиизвлеченияегорезультата:

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

Безопасное совместное использование изменяемогосостояния

usestd::thread;

fnmain(){thread::spawn(||{println!("Hellofromathread!");});}

usestd::thread;

fnmain(){lethandle=thread::spawn(||{"Hellofromathread!"});

println!("{}",handle.join().unwrap());}

ЯзыкпрограммированияRust

104Многозадачность

Page 105: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вчитайтесь:«безопасноесовместноеиспользованиеизменяемогосостояния».Похоженаложь, не так ли?Многие программисты считают, что организоватьмногопоточную работу сизменяемымсостояниемоченьсложноипочтиневозможно.НоблагодарясистеметиповRust,этовсёжеправда —безопасноработатьсизменяемымиданнымиможно.

Кто-тооднаждысказалэто:

Совместно используемое изменяемое состояние является корнем всех зол.Большинство языков пытаются решить эту проблему через часть, отвечающую за«изменяемое»,ноRustрешаетеечерезчасть,отвечающуюза«совместноиспользуемое».

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

ВкачествепримераприведемпрограммунаRust,котораявходилабывсостояниегонкиподаннымнамногихязыках.НаRustонанескомпилируется:

Онавыдаетошибку:

8:17error:captureofmovedvalue:`data`data[i]+=1;^~~~

В данном случае мы знаем, что наш коддолжен быть безопасным, но Rust в этом неуверен.И,насамомделе,оннеявляетсябезопасным:мыработаемсdata вкаждомпотоке.Приэтом,потокстановитсявладельцемтого,чтоонполучаеткакчастьокружениязамыкания.Аэтозначит,чтоунасестьтривладельца!Этоплохо.МыможемисправитьэтоспомощьютипаArc<T> , который является атомарным указателем со счетчиком ссылок. «Атомарный»означает,чтоимбезопаснообмениватьсямеждупотоками.

Чтобы гарантировать, что его можно безопасно использовать из нескольких потоков,Arc<T> предполагаетналичиеещеодногосвойстваувложенноготипа.Онпредполагает,чтоT реализуеттипажSync .Внашемслучаемытакжехотим,чтобыбылавозможностьизменятьвложенное значение. Нам нужен тип, который может обеспечить изменение своего

usestd::thread;

fnmain(){letmutdata=vec![1u32,2,3];

foriin0..3{thread::spawn(move||{data[i]+=1;});}

thread::sleep_ms(50);}

ЯзыкпрограммированияRust

105Многозадачность

Page 106: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

содержимоголишьоднимпользователемодновременно.ДляэтогомыможемиспользоватьтипMutex<T> . Вот вторая версия нашего кода. Она по-прежнему не работает, но по другойпричине:

Вотошибка:

<anon>:9:9:9:22error:thetrait`core::marker::Send`isnotimplementedforthetype`std::sync::mutex::MutexGuard<'_,collections::vec::Vec<u32>>`[E0277]<anon>:11thread::spawn(move||{^~~~~~~~~~~~~<anon>:9:9:9:22note:`std::sync::mutex::MutexGuard<'_,collections::vec::Vec<u32>>`cannotbesentbetweenthreadssafely<anon>:11thread::spawn(move||{^~~~~~~~~~~~~

Вы можете видеть, чтоMutex содержит методlock , который имеет следующуюсигнатуру:

Так как типажSend не был реализован дляMutexGuard<T> , мы не можемперемещатьохранноезначениемьютексачерезграницыпотоков,чтоисказановсообщенииобошибке.

МыможемиспользоватьArc<T> ,чтобыисправитьэто.Вотрабочаяверсия:

usestd::thread;usestd::sync::Mutex;

fnmain(){letmutdata=Mutex::new(vec![1u32,2,3]);

foriin0..3{letdata=data.lock().unwrap();thread::spawn(move||{data[i]+=1;});}

thread::sleep_ms(50);}

fnlock(&self)->LockResult<MutexGuard<T>>

ЯзыкпрограммированияRust

106Многозадачность

Page 107: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Теперьмывызываемclone() длянашегоArc ,чтоувеличиваетвнутреннийсчетчик.Затемполученная ссылкаперемещается вновыйпоток.Давайтеболееподробнорассмотримтелопотока:

Во-первых, мы вызываем методlock() , который захватывает блокировку мьютекса.Таккаквызовданногометодаможетпотерпетьнеудачу,онвозвращаетResult<T,E> , но,посколькуэтопростопример,мыиспользуемunwrap() ,чтобыполучитьссылкунаданные.Реальныйкоддолжениметьболеенадежнуюобработкуошибоквтакойситуации.Послеэтогомысвободноизменяемданные,таккакунасестьблокировка.

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

Есть более точные способы синхронизациипотоков, и несколько из них реализовано встандартнойбиблиотекеRust.Давайтепоговоримободномизних:каналах.

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

чтобыждатьвтечениеопределенноговремени:

usestd::sync::{Arc,Mutex};usestd::thread;

fnmain(){letdata=Arc::new(Mutex::new(vec![1u32,2,3]));

foriin0..3{letdata=data.clone();thread::spawn(move||{letmutdata=data.lock().unwrap();data[i]+=1;});}

thread::sleep_ms(50);}

thread::spawn(move||{letmutdata=data.lock().unwrap();data[i]+=1;});

ЯзыкпрограммированияRust

107Многозадачность

Page 108: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мыиспользуемметодmpsc::channel() ,чтобысоздатьновыйканал.Вэтомпримеремы в каждом из десяти потоков вызываемметодsend , которыйпередает по каналу пустойкортеж() ,азатемвглавномпотокеждем,поканебудутпринятывседесятьзначений.

Хотя по этому каналу посылается просто сигнал (пустой кортеж() не несёт никакихданных),вобщемслучаемыможемотправитьпоканалулюбоезначение,котороереализуеттипажSend !

usestd::sync::{Arc,Mutex};usestd::thread;usestd::sync::mpsc;

fnmain(){letdata=Arc::new(Mutex::new(0u32));

let(tx,rx)=mpsc::channel();

for_in0..10{let(data,tx)=(data.clone(),tx.clone());

thread::spawn(move||{letmutdata=data.lock().unwrap();*data+=1;

tx.send(());});}

for_in0..10{rx.recv();}}

usestd::thread;usestd::sync::mpsc;

fnmain(){let(tx,rx)=mpsc::channel();

for_in0..10{lettx=tx.clone();

thread::spawn(move||{letanswer=42u32;

tx.send(answer);});}

rx.recv().ok().expect("Couldnotreceiveanswer");}

ЯзыкпрограммированияRust

108Многозадачность

Page 109: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

u32 реализуетSend , потому что мыможем сделать копию.Итак, создаётся поток, вкоторомвычисляетсяответ,азатемэтотответспомощьюметодаsend() передаётсяобратнопоканалу.

Паникаpanic! аварийно завершает выполняемый в данный момент поток. Вы можете

использоватьпотокиRustкакпростоймеханизмизоляции:

Используемый в коде выше методjoin() структурыThread возвращаетResult ,чтопозволяетнампроверить,паниковаллипоток,илионзавершилсянормально.

usestd::thread;

letresult=thread::spawn(move||{panic!("oops!");}).join();

assert!(result.is_err());

ЯзыкпрограммированияRust

109Многозадачность

Page 110: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ОбработкаошибокКак и многие языки программирования, Rust призывает разработчика определенным

способомобрабатыватьошибки.Вообще,существуетдваобщихподходаобработкиошибок:спомощью исключений и через возвращаемые значения. И Rust предпочитает возвращаемыезначения.

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

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

СодержаниеЭта глава очень длинная, в основном потому, что мы начнем с самого начала —

рассмотрения типов-сумм (sum type) и комбинаторов, и далее попытаемся последовательнообъяснитьподходRustкобработкеошибок.Такчторазработчики,которыеимеютопытработыс другими выразительными системами типов, могут свободно перескакивать от раздела кразделу.

ОсновыОбъяснениеunwrapТипOption

СовмещениезначенийOption<T>ТипResult

ПреобразованиестрокивчислоСозданиепсевдониматипаResult

Короткоеотступление:unwrap —необязательнозлоРаботаснесколькимитипамиошибок

СовмещениеOption иResultОграничениякомбинаторовПреждевременныйreturnМакросtry!Объявлениесобственноготипаошибки

Типажиизстандартнойбиблиотеки,используемыедляобработкиошибокТипажErrorТипажFromНастоящиймакросtry!Совмещениесобственныхтиповошибок

ЯзыкпрограммированияRust

110Обработкаошибок

Page 111: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

РекомендациидляавторовбиблиотекПрактическийпример:ПрограммадлячтениядемографическихданныхЗаключение

ОсновыОбработку ошибок можно рассматривать каквариативный анализ того, было ли

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

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

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

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

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

Еслипопробоватьзапуститьэтоткод,топрограммааварийнозавершитсяссообщениемвродеэтого:

//Попробуйтеугадатьчислоот1до10.//Еслизаданноечислосоответствуеттому,чтомызагадали,возвращаетсяtrue.//Впротивномслучаевозвращаетсяfalse.fnguess(n:i32)->bool{ifn<1||n>10{panic!("Неверноечисло:{}",n);}n==5}

fnmain(){guess(11);}

ЯзыкпрограммированияRust

111Обработкаошибок

Page 112: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

thread'<main>'panickedat'Неверноечисло:11',src/bin/panic-simple.rs:6

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

Есливызапуститеэтупрограммубезпараметров(ошибка1)илиеслипервыйпараметрбудет не целым числом (ошибка 2), программа завершится паникой, также, как и в первомпримере.

Обработка ошибок в подобном стиле подобна слону в посудной лавке. Слон будетнестисьвнаправлении,вкоторомемувздумается,икрушитьвсенасвоемпути.

ОбъяснениеunwrapВпредыдущемпримеремы утверждали, что программа будет просто паниковать, если

будетвыполненоодноиздвухусловийдлявозникновенияошибки,хотя,вотличииотпервогопримера,вкодепрограммынетявноговызоваpanic .Темнеменее,вызовpanic встроенввызовunwrap .

Вызыватьunwrap вRustподобнотому,чтосказать:"Вернимнерезультатвычислений,аеслипроизошлаошибка,простопаникуйиостанавливайпрограмму".Мымоглибыпростопоказатьисходныйкодфункцииunwrap ,ведьэтодовольнопросто,нопередэтиммыдолжныразобратсястипамиOption иResult .Обаэтихтипаимеютопределенныйдлянихметодunwrap .

ТипOptionТипOption объявленвстандартнойбиблиотеке:

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

usestd::env;

fnmain(){letmutargv=env::args();letarg:String=argv.nth(1).unwrap();//ошибка1letn:i32=arg.parse().unwrap();//ошибка2println!("{}",2*n);}

enumOption<T>{None,Some(T),}

ЯзыкпрограммированияRust

112Обработкаошибок

Page 113: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Обратите внимание, что когда эта функция находит соответствующий символ, онавозвращаетнепростоoffset .ВместоэтогоонавозвращаетSome(offset) . Some —этовариант иликонструктор значения для типаOption . Его можно интерпретировать какфункцию типаfn<T>(value:T)->Option<T> . Соответственно,None — это такжеконструктор значения, только у него нет параметров. Его можно интерпретировать какфункциютипаfn<T>()->Option<T> .

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

Этот код используетсопоставление с образцом чтобы выполнитьвариативный анализдлявозвращаемогофункциейfind значенияOption<usize> .Насамомделе,вариативныйанализ является единственным способом добраться до значения, сохраненного внутриOption<T> . Это означает, что вы, как разработчик, обязаны обработать случай, когдазначениеOption<T> равноNone ,анеSome(t) .

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

//ПоискUnicode-символа`needle`в`haystack`.Когдапервыйсимволнайден,//возвращаетсяпобайтовоесмещениедляэтогосимвола.Иначевозвращается`None`.fnfind(haystack:&str,needle:char)->Option<usize>{for(offset,c)inhaystack.char_indices(){ifc==needle{returnSome(offset);}}None}

fnmain(){letfile_name="foobar.rs";matchfind(file_name,'.'){None=>println!("Расширениефайланенайдено."),Some(i)=>println!("Расширениефайла:{}",&file_name[i+1..]),}}

ЯзыкпрограммированияRust

113Обработкаошибок

Page 114: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Методunwrap абстрагируетвариативныйанализ.Этоименното,чтоделаетunwrapудобнымвиспользовании.Ксожалению,panic! означает,чтоunwrap неудобносочетатьсдругимкодом:этослонвпосуднойлавке.

СовмещениезначенийOption<T>В предыдущем примере мы рассмотрели, как можно воспользоватсяfind для того,

чтобыполучитьрасширениеименифайла.Конечно,невовсехименахфайловможнонайти. ,так что существует вероятность, что имя некоторого файла не имеет расширения. Этавозможность отсутствия интерпретируется на уровне типов через использованиеOption<T> .Другимисловами,компиляторзаставитнасрассмотретьвозможностьтого,чторасширениенесуществует.Внашемслучаемыпростопечатаемсообщениеобэтом.

Получение расширения именифайла— довольно распространенная операция, так чтоимеетсмыслвынестикодвотдельнуюфункцию:

(Подсказка: не используйте этот код. Вместо этого используйте методextension изстандартнойбиблиотеки.)

Код выглядит простым, но его важный аспект заключается в том, что функцияfindзаставляет нас рассмотреть вероятность отсутствия значения. Это хорошо, поскольку этоозначает, что компилятор не позволит нам случайно забыть о том варианте, когда в именифайлаотсутствуетрасширение.Сдругойстороны,каждыйразвыполнятьявныйвариативныйанализ, подобно тому, как мы делали это вextension_explicit , может стать немногоутомительным.

enumOption<T>{None,Some(T),}

impl<T>Option<T>{fnunwrap(self)->T{matchself{Option::Some(val)=>val,Option::None=>panic!("called`Option::unwrap()`ona`None`value"),}}}

//Возвращаетрасширениезаданногоименифайла,аименновсесимволы,//идущиезапервымвхождением`.`вимяфайла.//Еслив`file_name`нетниодноговхождения`.`,возвращается`None`.fnextension_explicit(file_name:&str)->Option<&str>{matchfind(file_name,'.'){None=>None,Some(i)=>Some(&file_name[i+1..]),}}

ЯзыкпрограммированияRust

114Обработкаошибок

Page 115: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

На самом деле, вариативный анализ вextension_explicit является оченьраспространенным паттерном: еслиOption<T> владеет определенным значениемT , товыполнитьегопреобразованиеспомощьюфункции,аеслинет—топростовернутьNone .

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

Вдействительности,map определенвстандартнойбиблиотекекакметодOption<T> .

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

Естьещеодноповедение,котороеможночастовстретить—этоиспользованиезначенияпо-умолчанию в случае, когда значениеOption равноNone . К примеру, ваша программаможетсчитать,чторасширениефайларавноrs вслучае,еслинасамомделеоноотсутствует.

Легко представить, что этот случай вариативного анализа не специфичен только длярасширенийфайлов—такойподходможетработатьслюбымOption<T> :

Хитрость только в том, что значение по-умолчанию должно иметь тот же тип, что изначение, которое может находится внутриOption<T> . Использование этого методаэлементарно:

(Обратите внимание, чтоunwrap_or объявленкакметод Option<T> в стандартнойбиблиотеке,такчтомывоспользовалисьимвместофункции,которуюмыобъявилиранее.Незабудьтетакжеизучитьболееобщийметодunwrap_or_else ).

fnmap<F,T,A>(option:Option<T>,f:F)->Option<A>whereF:FnOnce(T)->A{matchoption{None=>None,Some(value)=>Some(f(value)),}}

//Возвращаетрасширениезаданногоименифайла,аименновсесимволы,//идущиезапервымвхождением`.`вимяфайла.//Еслив`file_name`нетниодноговхождения`.`,возвращается`None`.fnextension(file_name:&str)->Option<&str>{find(file_name,'.').map(|i|&file_name[i+1..])}

fnunwrap_or<T>(option:Option<T>,default:T)->T{matchoption{None=>default,Some(value)=>value,}}

fnmain(){assert_eq!(extension("foobar.csv").unwrap_or("rs"),"csv");assert_eq!(extension("foobar").unwrap_or("rs"),"rs");}

ЯзыкпрограммированияRust

115Обработкаошибок

Page 116: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Существует еще один комбинатор, на который, как мы думаем, стоит обратить особоевнимание:and_then . Он позволяет легко сочетать различные вычисления, которыедопускаютвозможностьотсутствия.Пример—большаячастькодавэтомразделе,которыйсвязансопределениемрасширениязаданногоименифайла.Чтобыделатьэто,намдляначаланеобходимо узнать имя файла, которое как правило извлекается изфайлового пути. Хотябольшинствофайловыхпутейсодержатимяфайла,подобноенельзясказатьобовсехфайловыхпутях.Примероммогутпослужитьпути. ,.. или/ .

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

Можноподумать,мымоглибыпростоиспользоватькомбинаторmap ,чтобыуменьшитьвариативныйанализ,ноеготипнесовсемподходит.Деловтом,чтоmap принимаетфункцию,которая делает что-то только с внутренним значением. Результат такой функциивсегдаоборачивается вSome .Вместоэтого,намнуженметод,похожийmap ,нокоторыйпозволяетвызывающемупередатьещеодинOption .Егообщаяреализациядажепроще,чемmap :

Теперьмыможемпереписатьнашуфункциюfile_path_ext безявноговариативногоанализа:

Т и пOption имеет много других комбинаторовопределенных в стандартнойбиблиотеке.Оченьполезнопросмотретьэтотсписокиознакомитьсясдоступнымиметодами—онинеразпомогутвамсократитьколичествовариативногоанализа.Ознакомлениесэтимикомбинаторами окупится еще и потому, что многие из них определены с аналогичнойсемантикойидлятипаResult ,окотороммыпоговоримдалее.

fnfile_path_ext_explicit(file_path:&str)->Option<&str>{matchfile_name(file_path){None=>None,Some(name)=>matchextension(name){None=>None,Some(ext)=>Some(ext),}}}

fnfile_name(file_path:&str)->Option<&str>{unimplemented!()//опустимреализацию}

fnand_then<F,T,A>(option:Option<T>,f:F)->Option<A>whereF:FnOnce(T)->Option<A>{matchoption{None=>None,Some(value)=>f(value),}}

fnfile_path_ext(file_path:&str)->Option<&str>{file_name(file_path).and_then(extension)}

ЯзыкпрограммированияRust

116Обработкаошибок

Page 117: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Комбинаторы упрощают использование типов вродеOption , ведь они сокращаютявный вариативный анализ. Они также соответствуют требованиям сочетаемости, посколькуонипозволяютвызывающемуобрабатыватьвозможностьотсутствиярезультатасобственнымспособом.Такиеметоды,какunwrap ,лишаютэтойвозможности,ведьонибудутпаниковатьвслучае,когдаOption<T> равенNone .

ТипResultТипResult такжеопределенвстандартнойбиблиотеке:

Т и пResult — это продвинутая версияOption . Вместо того, чтобы выражатьвозможностьотсутствия, как этоделаетOption , Result выражает возможностьошибки.Как правило,ошибки необходимы для объяснения того, почему результат определенноговычисления не был получен. Строго говоря, это более общая формаOption . Рассмотримследующийпсевдонимтипа,которыйвовсехсмыслахсемантическиэквивалентенреальномуOption<T> :

Здесь второй параметр типаResult фиксируется и определяется через()(произноситсякак"unit"или"пустойкортеж").Тип() имеетровнооднозначение—() .(Да,этотипизначениеэтоготипа,которыевыглядятодинаково!)

ТипResult —этоспособвыразитьодиниздвухвозможныхисходоввычисления.Посоглашению,одинисходозначаетожидаемыйрезультатили"Ok ",втовремякакдругойисходозначаетисключительнуюситуациюили"Err ".

ПодобноOption , типResult имеет методunwrap , определенный в стандартнойбиблиотеке.Давайтеобъявимегосамостоятельно:

Это фактически то же самое, что иопределениеOption::unwrap , за исключениемтого,чтомыдобавилизначениеошибкивсообщениеpanic! .Этоупрощаетотладку,ноэтотакжевынуждаетнастребоватьоттипа-параметраE (которыйпредставляетнаштипошибки)реализацииDebug . Поскольку подавляющее большинство типов должны реализовывать

enumResult<T,E>{Ok(T),Err(E),}

typeOption<T>=Result<T,()>;

impl<T,E:::std::fmt::Debug>Result<T,E>{fnunwrap(self)->T{matchself{Result::Ok(val)=>val,Result::Err(err)=>panic!("called`Result::unwrap()`onan`Err`value:{:?}",err),}}}

ЯзыкпрограммированияRust

117Обработкаошибок

Page 118: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Debug ,обычнонапрактикетакоеограничениенемешает.(РеализацияDebug длянекотороготипа просто означает, что существует разумный способ печати удобочитаемого описаниязначенияэтоготипа.)

Окей,давайтеперейдемкпримеру.

ПреобразованиестрокивчислоСтандартная библиотека Rust позволяет элементарно преобразовывать строки в целые

числа.Насамомделеэтонастолькопросто,чтовозникаетсоблазннаписатьчто-товроде:

Здесьвыдолжныбытьскептическинастроеныпо-поводувызоваunwrap .Еслистрокунельзяпреобразоватьвчисло,выполучитепанику:

thread'<main>'panickedat'called`Result::unwrap()`onan`Err`value:ParseIntError{kind:InvalidDigit}',/home/rustbuild/src/rust-buildbot/slave/beta-dist-rustc-linux/build/src/libcore/result.rs:729

Это довольно неприятно, и если бы подобное произошло в используемой вамибиблиотеке, вы могли бы небезосновательно разгневаться. Так что нам стоит попытатьсяобработатьошибкувнашейфункции,ипустьвызывающийсамрешитчтосэтимделать.Этоозначает необходимость изменения типа, который возвращаетсяdouble_number . Но накакой? Чтобы понять это, необходимо посмотреть на сигнатуруметода parse изстандартнойбиблиотеки:

Хмм.Покрайнеймеремызнаем,чтодолжныиспользоватьResult .Вполневозможно,чтометодмогвозвращатьOption .Вконцеконцов,строкалибопарситсякакчисло,либонет,не так ли? Это, конечно, разумный путь, но внутренняя реализация знаетпочему строка неможетбытьпреобразованавцелоечисло.(Этоможетбытьпустаястрока,илинеправильныецифры,слишкомбольшаяилислишкоммаленькаядлинаит.д.)Такимобразом,использованиеResult имеет смысл, ведь мы хотим предоставить больше информации, чем просто"отсутствие". Мы хотим сказать,почему преобразование не удалось. Вам стоит рассуждатьпохожим образом, когда вы сталкиваетесь с выбором междуOption иResult . Если выможете предоставить подробную информацию об ошибке, то вам, вероятно, следует этосделать.(Позжемыпоговоримобэтомподробнее.)

fndouble_number(number_str:&str)->i32{2*number_str.parse::<i32>().unwrap()}

fnmain(){letn:i32=double_number("10");assert_eq!(n,20);}

implstr{fnparse<F:FromStr>(&self)->Result<F,F::Err>;}

ЯзыкпрограммированияRust

118Обработкаошибок

Page 119: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Хорошо,нокакмызапишемнаштипвозвращаемогозначения?Методparse являетсяобобщенным(generic)длявсехразличныхтиповчиселизстандартнойбиблиотеки.Мымоглибы (и, вероятно, должны) также сделать нашу функцию обобщенной, но давайте покаостановимся на конкретной реализации. Нас интересует только типi32 , так что нам стоитнайти его реализациюFromStr (выполнитепоискввашембраузерепостроке"FromStr")ипосмотретьнаегоассоциированныйтип Err .Мыделаемэто,чтобыопределитьконкретныйтип ошибки. В данном случае, этоstd::num::ParseIntError . Наконец, мы можемпереписатьнашуфункцию:

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

Комбинаторы спешат на помощь! ПодобноOption , Result имеет многокомбинаторов,определенныхвкачествеметодов.Существуетбольшойсписоккомбинаторов,общихмеждуResult иOption .Иmap входитвэтотсписок:

ВсеожидаемыеметодыреализованыдляResult , включаяunwrap_or иand_then .Кроме того, посколькуResult имеет второй параметр типа, существуют комбинаторы,которые влияют толькона значение ошибки, такие какmap_err (аналогmap ) иor_else(аналогand_then ).

СозданиепсевдониматипаResult

usestd::num::ParseIntError;

fndouble_number(number_str:&str)->Result<i32,ParseIntError>{matchnumber_str.parse::<i32>(){Ok(n)=>Ok(2*n),Err(err)=>Err(err),}}

fnmain(){matchdouble_number("10"){Ok(n)=>assert_eq!(n,20),Err(err)=>println!("Error:{:?}",err),}}

usestd::num::ParseIntError;

fndouble_number(number_str:&str)->Result<i32,ParseIntError>{number_str.parse::<i32>().map(|n|2*n)}

fnmain(){matchdouble_number("10"){Ok(n)=>assert_eq!(n,20),Err(err)=>println!("Error:{:?}",err),}}

ЯзыкпрограммированияRust

119Обработкаошибок

Page 120: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

В стандартной библиотеке можно часто увидеть типы вродеResult<i32> . Нопостойте,ведьмыопределилиResult сдвумяпараметрамитипа.Какмыможемобойтиэто,указывая только один из них? Ответ заключается в определении псевдонима типаResult ,которыйфиксируетодинизпараметровконкретнымтипом.Обычнофиксируетсятипошибки.Например,нашпредыдущийпримерспреобразованиемстроквчисламожнопереписатьтак:

Зачем мы это делаем? Что ж, если у нас есть много функций, которые могут вернутьParseIntError , то гораздо удобнее определить псевдоним, который всегда используетParseIntError ,такчтомынебудемповторятьсявсевремя.

Самый заметный случай использования такого подхода в стандартной библиотеке —псевдонимio::Result . Как правило, достаточно писатьio::Result<T> , чтобы былопонятно, что вы используете псевдоним типа из модуляio , а не обычное определение изstd::result .(Этотподходтакжеиспользуетсядляfmt::Result )

Короткоеотступление:unwrap —необязательнозлоЕсли вы были внимательны, то возможно заметили, что я занял довольно жесткую

позициюпоотношениюкметодамвродеunwrap ,которыемогутвызватьpanic ипрерватьисполнениевашейпрограммы.Восновном,этохорошийсовет.

Тем не менее,unwrap все-таки можно использовать разумно. Факторы, которыеоправдывают использованиеunwrap , являются несколько туманными, и разумные людимогутсомнойнесогласиться.Якраткоизложусвоемнениепоэтомувопросу:

Примерыи"грязный"код.Когдавыпишетепростопримерилибыстрыйскрипт,обработкаошибокпростонетребуется.Дляподобныхслучаевтруднонайтичто-либоудобнеечемunwrap ,такчтоздесьегоиспользованиеоченьпривлекательно.Паника указывает на ошибку в программе. Если логика вашего кода должнапредотвращать определенное поведение (скажем, получение элемента из пустогостека), то использованиеpanic также допустимо. Дело в том, что в этом случаепаника будет сообщать о баге в вашей программе. Это может происходить явно,например от неудачного вызоваassert! , илипроисходить потому, что индекс помассивунаходитсязапределамивыделеннойпамяти.

Вероятно, это не исчерпывающий список. Кроме того, при использованииOptionзачастуюлучшеиспользоватьметодexpect .Этотметодделаетровнотоже,чтоиunwrap ,за исключением того, что в случае паникинапечатает ваше сообщение.Это позволит лучше

usestd::num::ParseIntError;usestd::result;

typeResult<T>=result::Result<T,ParseIntError>;

fndouble_number(number_str:&str)->Result<i32>{unimplemented!();}

ЯзыкпрограммированияRust

120Обработкаошибок

Page 121: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

понять причину ошибки, ведь будет показано конкретное сообщение, а не просто "calledunwraponaNone value".

Мой совет сводится к следующему: используйте здравый смысл. Есть причины, покоторым слова вроде "никогда не делать X" или "Y считается вредным" не появятся в этойстатье. У любых решений существуют компромиссы, и это ваша задача, как разработчика,определить,чтоименноявляетсяприемлемымдлявашегослучая.Мояцельсостоиттольковтом,чтобыпомочьвамоценитькомпромиссыкакможноточнее.

Теперь, когда мы рассмотрели основы обработки ошибок в Rust и разобрались сunwrap ,давайтеподробнееизучимстандартнуюбиблиотеку.

РаботаснесколькимитипамиошибокДо этого момента мы расматривали обработку ошибок только для случаев, когда все

сводилось либо только кOption<T> , либо только кResult<T, SomeError> . Но чтоделать,когдаувасестьиOption ,иResult?ИлиеслиувасестьResult<T,Error1> иResult<T,Error2>?Нашаследующуязадача—обработкакомпозицииразличныхтиповошибок,иэтобудетглавнойтемойнапротяжениивсейэтойглавы.

СовмещениеOption иResultПока что мы говорили о комбинаторах, определенных дляOption , и комбинаторах,

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

Конечно, в реальном коде все происходит не так гладко. Иногда у вас есть сочетаниятиповOption иResult . Должны ли мы прибегать к явному вариативному анализу, илиможнопродолжитьиспользоватькомбинаторы?

Давайтенавремявернемсякодномуизпервыхпримероввэтойглаве:

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

Ньюанс заключается в том, чтоargv.nth(1) возвращаетOption , в то время какarg.parse() возвращаетResult . Они не могут быть скомпонованы непосредственно.Когда вы сталкиваетесь одновременно сOption иResult ,обычно наилучшее решение—

usestd::env;

fnmain(){letmutargv=env::args();letarg:String=argv.nth(1).unwrap();//ошибка1letn:i32=arg.parse().unwrap();//ошибка2println!("{}",2*n);}

ЯзыкпрограммированияRust

121Обработкаошибок

Page 122: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

преобразоватьOption вResult .Внашемслучае,отсутствиепараметракоманднойстроки(изenv::args() )означает,чтопользовательнеправильновызвалпрограмму.МымоглибыпростоиспользоватьString дляописанияошибки.Давайтепопробуем:

Раcсмотрим пару новых моментов на этом примере. Во-первых, использованиекомбинатораOption::ok_or . Это один из способов преобразованияOption вResult .Такое преобразование требует явного определения ошибки, которую необходимо вернуть вслучае, когда значениеOption равноNone . Как и для всех комбинаторов, которые мырассматривали,егообъявлениеоченьпростое:

Второйновыйкомбинатор,которыймыиспользовали—Result::map_err .Этотожесамое, что иResult::map , за исключением того, функция применяется кошибке внутриResult .ЕслизначениеResult равноОk(...) ,тооновозвращаетсябезизменений.

Мы используемmap_err , потому что нам необходимо привести все ошибки кодинаковому типу (из-за нашего использованияand_then ). Поскольку мы решилипреобразовыватьOption<String> (изargv.nth(1) ) вResult<String, String> ,мытакжеобязаныпреобразовыватьParseIntError изarg.parse() вString .

ОграничениякомбинаторовРаботасIOианализвходныхданных—оченьтипичныезадачи,иэтото,чемличноя

много занимаюсьсRust.Такчтомыбудемиспользовать IOиразличныепроцедурыанализакакпримерыобработкиошибок.

Давайтеначнемспростого.Поставимзадачуоткрытьфайл,прочестьвсеегосодержимоеипреобразоватьэтосодержимоевчисло.Послеэтогонужнобудетумножитьзначениена2 ираспечататьрезультат.

usestd::env;

fndouble_arg(mutargv:env::Args)->Result<i32,String>{argv.nth(1).ok_or("Pleasegiveatleastoneargument".to_owned()).and_then(|arg|arg.parse::<i32>().map_err(|err|err.to_string()))}

fnmain(){matchdouble_arg(env::args()){Ok(n)=>println!("{}",n),Err(err)=>println!("Error:{}",err),}}

fnok_or<T,E>(option:Option<T>,err:E)->Result<T,E>{matchoption{Some(val)=>Ok(val),None=>Err(err),}}

ЯзыкпрограммированияRust

122Обработкаошибок

Page 123: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Хоть я и пытался убедить вас не использоватьunwrap , иногда бывает полезным дляначаланаписатькодсunwrap .Этопозволяетсосредоточитьсянапроблеме,аненаобработкеошибок,иэтовыявляетместа,гденадлежащаяобработкаошибокнеобходима.Давайтеначнемстого,чтонапишемпростоработающийкод,азатемотрефакторимегодлялучшейобработкиошибок.

(Замечание: Мы используемAsRef потем же причинам, почему он используется вstd::fs::File::open .Этопозволяетудобноиспользоватьлюбойтипстрокивкачествепутикфайлу.)

Унасестьтрипотенциальныеошибки,которыемогутвозникнуть:

1. Проблемаприоткрытиифайла.2. Проблемапричтенииданныхизфайла.3. Проблемаприпреобразованииданныхвчисло.

Первые две проблемы определяются типомstd::io::Error .Мы знаем этоиз типавозвращаемого значения методовstd::fs::File::open иstd::io::Read::read_to_string . (Обратите внимание, что они оба используютконцепцию с псевдонимом типаResult , описанную ранее. Если вы кликните на типResult , выувидитепсевдонимтипа,иследовательно,лежащийвосноветипio::Error .)Третья проблема определяется типомstd::num::ParseIntError . Кстати, типio::Error частоиспользуетсяповсейстандартнойбиблиотеке.Выбудетевидетьегосноваиснова.

Давайте начнем рефакторинг функцииfile_double . Для того, чтобы эту функциюможно было сочетать с остальным кодом, онане должна паниковать, если какие-либо изперечисленных выше ошибок действительно произойдут. Фактически, это означает, чтофункция должнавозвращать ошибку, если любая из возможных операций завершиласьнеудачей.Проблемасостоитвтом,чтотипвозвращаемогозначениясейчасi32 , которыйнедаетнамникакогоразумногоспособасообщитьобошибке.Такимобразом,мыдолжныначатьсизменениятипавозвращаемогозначениясi32 начто-тодругое.

usestd::fs::File;usestd::io::Read;usestd::path::Path;

fnfile_double<P:AsRef<Path>>(file_path:P)->i32{letmutfile=File::open(file_path).unwrap();//ошибка1letmutcontents=String::new();file.read_to_string(&mutcontents).unwrap();//ошибка2letn:i32=contents.trim().parse().unwrap();//ошибка32*n}

fnmain(){letdoubled=file_double("foobar");println!("{}",doubled);}

ЯзыкпрограммированияRust

123Обработкаошибок

Page 124: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Первое, чтомы должны решить: какой из типов использовать:Option илиResult?Мы,конечно,моглибыслегкостьюиспользоватьOption .Есликакая-либоизтрехошибокпроисходит,мымоглибыпростовернутьNone .Этобудетработать,иэтолучше,чемпростопаниковать,номыможемсделатьгораздолучше.Вместоэтого,мыбудемсообщатьнекоторыедетали о возникшей проблеме. Поскольку мы хотим выразитьвозможность ошибки, мыдолжны использоватьResult<i32,E> . Но каким должен быть типE?Посколькуможетвозникнуть дваразныхтипаошибок,мыдолжныпреобразоватьихкобщемутипу.ОднимизтакихтиповявляетсяString .Давайтепосмотрим,какэтоотразитсянанашемкоде:

Выглядит немного запутанно.Может потребоваться довольно много практики, преждевы сможете писать такое. Написание кода в таком стиле называетсяследованием затипом.Когда мы изменили тип возвращаемого значенияfile_double наResult<i32,String> , нампришлось начать подбирать правильные комбинатороы.В данном случаемыиспользовалитолькотриразличныхкомбинатора:and_then ,map иmap_err .

Комбинаторand_then используется для объединения по цепочке несколькихвычислений,гдекаждоевычислениеможетвернутьошибку.Послеоткрытияфайлаестьещедва вычисления, которые могут завершиться неудачей: чтение из файла и преобразованиесодержимоговчисло.Соответственно,имеемдвавызоваand_then .

Комбинаторmap используется, чтобыприменитьфункциюк значениюOk(...) типаResult . Например, в самом последнем вызове,map умножает значениеOk(...) (типаi32 )на2 .Еслиошибкапроизошладоэтогомомента,этаоперациябылабыпропущена.Это

usestd::fs::File;usestd::io::Read;usestd::path::Path;

fnfile_double<P:AsRef<Path>>(file_path:P)->Result<i32,String>{File::open(file_path).map_err(|err|err.to_string()).and_then(|mutfile|{letmutcontents=String::new();file.read_to_string(&mutcontents).map_err(|err|err.to_string()).map(|_|contents)}).and_then(|contents|{contents.trim().parse::<i32>().map_err(|err|err.to_string())}).map(|n|2*n)}

fnmain(){matchfile_double("foobar"){Ok(n)=>println!("{}",n),Err(err)=>println!("Ошибка:{}",err),}}

ЯзыкпрограммированияRust

124Обработкаошибок

Page 125: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

следуетизопределенияmap .

Комбинаторmap_err — это уловка, котораяпозволяют всему этому заработать.Этоткомбинатор,такойже,какиmap ,заисключениемтого,чтоприменяетфункциюкErr(...)значениюResult .Вданномслучаемыхотимпривестивсенашиошибкикодномутипу—String .Посколькукакio::Error ,такиnum::ParseIntError реализуютToString ,мыможемвызватьметодto_string ,чтобывыполнитьпреобразование.

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

ПреждевременныйreturnДавайтевозьмемкодизпредыдущегоразделаиперепишемегосприменениемраннего

возврата. Раннийreturn позволяетвыйтиизфункциидосрочно.Мынеможемвыполнитьreturn дляfile_double внутризамыкания,поэтомунамнеобходимовернутьсякявномувариативномуанализу.

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

usestd::fs::File;usestd::io::Read;usestd::path::Path;

fnfile_double<P:AsRef<Path>>(file_path:P)->Result<i32,String>{letmutfile=matchFile::open(file_path){Ok(file)=>file,Err(err)=>returnErr(err.to_string()),};letmutcontents=String::new();ifletErr(err)=file.read_to_string(&mutcontents){returnErr(err.to_string());}letn:i32=matchcontents.trim().parse(){Ok(n)=>n,Err(err)=>returnErr(err.to_string()),};Ok(2*n)}

fnmain(){matchfile_double("foobar"){Ok(n)=>println!("{}",n),Err(err)=>println!("Ошибка:{}",err),}}

ЯзыкпрограммированияRust

125Обработкаошибок

Page 126: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Разве этонешагназад?Ранеемы говорили, чтоключкудобнойобработкеошибок—сокращение явного вариативного анализа, но здесь мы вернулись к тому, с чего начинали.Оказывается, существуетнесколько способов его уменьшения. И комбинаторы — неединственныйпуть.

Макросtry!Краеугольный камень обработки ошибок в Rust — это макросtry! . Этот макрос

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

Вотупрощенноеопределениемакроса`try!:

(Реальноеопределениевыглядитнемногосложнее.Мыобсудимэтодалее).

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

Вы з о вmap_err по-прежнему необходим, учитываянаше определениеtry! ,поскольку ошибки все еще должны быть преобразованы вString . Хорошей новостьюявляетсято,чтовближайшеевремямыузнаем,какубратьвсеэтивызовыmap_err !Плохаяновостьсостоитвтом,чтодляэтогонампридетсякое-чтоузнатьопареважныхтипажейизстандартнойбиблиотеки.

macro_rules!try{($e:expr)=>(match$e{Ok(val)=>val,Err(err)=>returnErr(err),});}

usestd::fs::File;usestd::io::Read;usestd::path::Path;

fnfile_double<P:AsRef<Path>>(file_path:P)->Result<i32,String>{letmutfile=try!(File::open(file_path).map_err(|e|e.to_string()));letmutcontents=String::new();try!(file.read_to_string(&mutcontents).map_err(|e|e.to_string()));letn=try!(contents.trim().parse::<i32>().map_err(|e|e.to_string()));Ok(2*n)}

fnmain(){matchfile_double("foobar"){Ok(n)=>println!("{}",n),Err(err)=>println!("Ошибка:{}",err),}}

ЯзыкпрограммированияRust

126Обработкаошибок

Page 127: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ОбъявлениесобственноготипаошибкиПрежде чеммыпогрузимся в аспектынекоторых типажейиз стандартной библиотеки,

связанныхсошибками,ябыхотелзавершитьэтотразделотказомотиспользованияStringкактипаошибкивнашихпримерах.

ИспользованиеString в том стиле, в котором мы использовали его в предыдущихпримерахудобнопотому,чтодостаточнолегкоконвертироватьлюбыеошибкивстроки,илидажесоздаватьсвоисобственныеошибкинаходу.Темнеменее,использованиетипаStringдляошибокимеетнекоторыенедостатки.

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

Второй и более важный недостаток заключается в том, что использованиеStringчреватопотерей информации. Другими словами, если все ошибки будут преобразованы встроки,токогдамыбудемвозвращатьихвызывающейстороне,онинебудутиметьникакогосмысла. Единственное разумное, что вызывающая сторона может сделать с ошибкой типаString —этопоказатьеепользователю.Безусловно,можнопроверитьстрокупозначению,чтобыопределитьтипошибки,нотакойподходнеможетпохвастатьсянадежностью.(Правда,вгораздобольшейстепениэтонедостатокдлябиблиотек,чемдляконечныхприложений).

Например, типio::Error включает в себя типio::ErrorKind , которыйявляетсяструктурированнымиданными,представляющимито,чтопошлонетаквовремявыполненияоперации ввода-вывода. Это важно, поскольку может возникнуть необходимость по-разномуреагироватьнаразличныепричиныошибки.(Например,ошибкаBrokenPipe можетизящнозавершатьпрограмму,втовремякакошибкаNotFound будетзавершатьпрограммускодомошибки и показывать соответствующее сообщение пользователю.) Благодаряio::ErrorKind , вызывающая сторона может исследовать тип ошибки с помощьювариативного анализа, и это значительно лучше попытки вычленить детали об ошибке изString .

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

Идеальным способом представленияодного варианта из многих является определениенашего собственного типа-суммы с помощьюenum . В нашем случае, ошибка представляетсобой либоio::Error , либоnum::ParseIntError , из чего естественным образомвытекаетопределение:

ЯзыкпрограммированияRust

127Обработкаошибок

Page 128: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Единственное изменение здесь — замена вызоваmap_err(|e| e.to_string())(который преобразовывал ошибки в строки) наmap_err(CliError::Io) илиmap_err(CliError::Parse) . Теперьвызывающая сторона определяет уровеньдетализации сообщения об ошибке для конечного пользователя. В действительности,использованиеString как типа ошибки лишает вызывающего возможности выбора, в товремя использование собственного типаenum , на подобиеCliError , дает вызывающемутот же уровень удобства, который был ранее, и кроме этогоструктурированные данные,описывающиеошибку.

Практическоеправилозаключаетсявтом,чтонеобходимоопределятьсвойсобственныйтипошибки,атипString дляошибокиспользоватьвкрайнемслучае,восновномкогдавыпишетеконечноеприложение.Есливыпишетебиблиотеку,определениесвоегособственноготипа ошибки наиболее предпочтительно. Таким образом, вы не лишите пользователя вашейбиблиотеки возможности выбирать наиболее предпочтительное для его конкретного случаяповедение.

usestd::io;usestd::num;

//Мыреализуем`Debug`поскольку,повсейвидимости,всетипыдолжныреализовывать`Debug`.//ЭтодаетнамвозможностьполучитьадекватноеичитаемоеописаниезначенияCliError#[derive(Debug)]enumCliError{Io(io::Error),Parse(num::ParseIntError),}

usestd::fs::File;usestd::io::Read;usestd::path::Path;

fnfile_double<P:AsRef<Path>>(file_path:P)->Result<i32,CliError>{letmutfile=try!(File::open(file_path).map_err(CliError::Io));letmutcontents=String::new();try!(file.read_to_string(&mutcontents).map_err(CliError::Io));letn:i32=try!(contents.trim().parse().map_err(CliError::Parse));Ok(2*n)}

fnmain(){matchfile_double("foobar"){Ok(n)=>println!("{}",n),Err(err)=>println!("Ошибка:{:?}",err),}}

ЯзыкпрограммированияRust

128Обработкаошибок

Page 129: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Типажи из стандартной библиотеки,используемыедляобработкиошибок

Стандартная библиотека определяет два встроенных типажа, полезных для обработкиошибокstd::error::Error иstd::convert::From . И еслиError разработанспециально для создания общего описания ошибки, то типажFrom играетширокуюрольвпреобразованиизначениймеждуразличнымитипами.

ТипажErrorТипажError объявленвстандартнойбиблиотеке:

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

Получатьстроковоепредставлениеошибкидляразработчика(Debug ).Получатьпонятноедляпользователяпредставлениеошибки(Display ).Получатькраткоеописаниеошибки(методdescription ).Изучатьпоцепочкепервопричинуошибки,еслионасуществует(методcause ).

Первыедвевозможностивозникаютврезультатетого,чтотипажError требуетвсвоюочередь реализации типажейDebug иDisplay . Последние два факта исходят из двухметодов, определенных в самомError . МощьЕrror заключается в том, что всесуществующиетипыошибокегореализуют,чтовсвоюочередьозначаетчтолюбыеошибкимогут быть сохранены кактипажи-объекты (trait object). Обычно это выглядит какBox<Error> ,либо&Error .Например,методcause возвращает&Error ,которыйкакразявляетсятипажом-объектом.ПозжемывернемсякприменениюError кактипажа-объекта.

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

usestd::fmt::{Debug,Display};

traitError:Debug+Display{///Ashortdescriptionoftheerror.fndescription(&self)->&str;

///Thelowerlevelcauseofthiserror,ifany.fncause(&self)->Option<&Error>{None}}

ЯзыкпрограммированияRust

129Обработкаошибок

Page 130: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Данныйтипошибкиотражает возможность возникновениядвухдругих типовошибок:ошибкаработысIОилиошибкапреобразованиястрокивчисло.Определениеошибкиможетотражать столько других видов ошибок, сколько необходимо, за счет добавления новыхвариантоввобъявленииenum .

РеализацияError довольно прямолинейна и главным образом состоит из явногоанализавариантов:

usestd::io;usestd::num;

//Мыреализуем`Debug`поскольку,повсейвидимости,всетипыдолжныреализовывать`Debug`.//ЭтодаетнамвозможностьполучитьадекватноеичитаемоеописаниезначенияCliError#[derive(Debug)]enumCliError{Io(io::Error),Parse(num::ParseIntError),}

usestd::error;usestd::fmt;

implfmt::DisplayforCliError{fnfmt(&self,f:&mutfmt::Formatter)->fmt::Result{match*self{//Обаизначальныхтипаошибокужереализуют`Display`,//такчтомыможемиспользоватьихреализацииCliError::Io(referr)=>write!(f,"IOerror:{}",err),CliError::Parse(referr)=>write!(f,"Parseerror:{}",err),}}}

implerror::ErrorforCliError{fndescription(&self)->&str{//Обаизначальныхтипаошибокужереализуют`Error`,//такчтомыможемиспользоватьихреализациейmatch*self{CliError::Io(referr)=>err.description(),CliError::Parse(referr)=>err.description(),}}

fncause(&self)->Option<&error::Error>{match*self{//Вобоихслучаяхпросходитнеявноепреобразованиезначения`err`//изконкретноготипа(`&io::Error`или`&num::ParseIntError`)//втипаж-обьект`&Error`.Этоработаетпотомучтообатипареализуют`Error`.CliError::Io(referr)=>Some(err),CliError::Parse(referr)=>Some(err),}}}

ЯзыкпрограммированияRust

130Обработкаошибок

Page 131: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Хочется отметить, что это очень типичная реализацияError : реализация методовdescription иcause всоответствиискаждымвозможнымвидомошибки.

ТипажFromТипажstd::convert::From объявленвстандартнойбиблиотеке:

Очень просто, не правда ли? ТипажFrom чрезвычайно полезен, поскольку создаетобщийподходдляпреобразованияиз определенного типаТ вкакой-тодругойтип(вданномслучае,"другимтипом"являетсятип,реализующийданныйтипаж,илиSelf ).СамоеважноевтипажеFrom —множествоегореализаций,предоставляемыхстандартнойбиблиотекой.

Вотнесколькопростыхпримеров,демонстрирующихработуFrom :

Итак,From полезен для выполнения преобразований между строками. Но как насчетошибок?Оказывается,существуетоднаважнаяреализация:

Эта реализация говорит, чтолюбой тип, который реализуетError , можноконвертировать в типаж-объектBox<Error> . Выглядит не слишком впечатляюще, но этооченьполезновобщемконтексте.

Помнитетедвеошибки,скоторымимыимелиделоранее, аименно,io::Error andnum::ParseIntError? Поскольку обе они реализуютError , они также работают сFrom :

Здесь нужно разобрать очень важный паттерн. Переменныеerr1 иerr2 имеютодинаковыйтип—типаж-объект.Этоозначает,чтоихреальныетипыскрытыоткомпилятора,такчтопофактуонрассматриваетerr1 иerr2 какодинаковыесущности.Крометого,мы

traitFrom<T>{fnfrom(T)->Self;}

letstring:String=From::from("foo");letbytes:Vec<u8>=From::from("foo");letcow:::std::borrow::Cow<str>=From::from("foo");

impl<'a,E:Error+'a>From<E>forBox<Error+'a>

usestd::error::Error;usestd::fs;usestd::io;usestd::num;

//Получаемзначенияошибокletio_err:io::Error=io::Error::last_os_error();letparse_err:num::ParseIntError="notanumber".parse::<i32>().unwrap_err();

//Собственно,конвертацияleterr1:Box<Error>=From::from(io_err);leterr2:Box<Error>=From::from(parse_err);

ЯзыкпрограммированияRust

131Обработкаошибок

Page 132: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

создалиerr1 иerr2 ,используяодинитотжевызовфункции—From::from .Мыможемтакделать,посколькуфункцияFrom::from перегруженапоееаргументуивозвращаемомутипу.

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

Насталовремявернутьсякнашемустаромудругу—макросуtry! .

Настоящиймакросtry!Доэтогомыпривелитакоеопределениеtry! :

Но это не настоящее определение. Реальное определение можно найти встандартнойбиблиотеке:

Здесьестьодномаленькое,нооченьважноеизменение:значениеошибкипропускаетсячерезвызовFrom::from .Этоделаетмакросtry! оченьмощныминструментом,посколькуондаетнамвозможностьбесплатновыполнятьавтоматическоепреобразованиетипов.

Вооружившись более мощным макросомtry! , давайте взглянем на код, написанныйнамиранее,которыйчитаетфайликонвертируетегосодержимоевчисло:

Ранеемыговорили,чтомыможемизбавитьсяотвызововmap_err .Насамомделе,всечтомыдолжныдляэтогосделать—этонайтитип,которыйработаетсFrom .Какмыувиделивпредыдущемразделе,From имеетреализацию,котораяпозволяетпреобразоватьлюбойтип

macro_rules!try{($e:expr)=>(match$e{Ok(val)=>val,Err(err)=>returnErr(err),});}

macro_rules!try{($e:expr)=>(match$e{Ok(val)=>val,Err(err)=>returnErr(::std::convert::From::from(err)),});}

usestd::fs::File;usestd::io::Read;usestd::path::Path;

fnfile_double<P:AsRef<Path>>(file_path:P)->Result<i32,String>{letmutfile=try!(File::open(file_path).map_err(|e|e.to_string()));letmutcontents=String::new();try!(file.read_to_string(&mutcontents).map_err(|e|e.to_string()));letn=try!(contents.trim().parse::<i32>().map_err(|e|e.to_string()));Ok(2*n)}

ЯзыкпрограммированияRust

132Обработкаошибок

Page 133: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ошибкивBox<Error> :

Мы уже очень близки к идеальной обработке ошибок. Наш код имеет очень малонакладных расходов из-за обработки ошибок, ведь макросtry! инкапсулирует сразу тривещи:

1. Вариативныйанализ.2. Потоквыполнения.3. Преобразованиетиповошибок.

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

Но осталась одна маленькая деталь: типBox<Error> не несет никакой информации.Если мы возвращаемBox<Error> вызывающей стороне, нет никакой возможности (легко)узнатьбазовыйтипошибки.Ситуация,конечно,лучше,чемсоString ,посолькупоявиласьвозможность вызыватьметоды, вродеdescription илиcause , ноограничениеостается:Box<Error> не предоставляет никакой информации о сути ошибки. (Замечание: Это несовсем верно, поскольку вRust есть инструменты рефлексии во время выполнения, которыеполезныпринекоторыхсценариях,ноихрассмотрениевыходитзарамкиэтойглавы).

НасталовремявернутьсякнашемусобственномутипуCliError исвязатьвсеводноцелое.

СовмещениесобственныхтиповошибокВ последнем разделе мы рассмотрели реальный макросtry! и то, как он выполняет

автоматическоепреобразованиезначенийошибокспомощьювызоваFrom::from .ВнашемслучаемыконвертировалиошибкивBox<Error> ,которыйработает,ноегозначениескрытодлявызывающейстороны.

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

usestd::error::Error;usestd::fs::File;usestd::io::Read;usestd::path::Path;

fnfile_double<P:AsRef<Path>>(file_path:P)->Result<i32,Box<Error>>{letmutfile=try!(File::open(file_path));letmutcontents=String::new();try!(file.read_to_string(&mutcontents));letn=try!(contents.trim().parse::<i32>());Ok(2*n)}

ЯзыкпрограммированияRust

133Обработкаошибок

Page 134: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Обратите внимание, что здесь у нас еще остались вызовыmap_err . Почему?Вспомнитеопределенияtry! иFrom .Проблемавтом,чтонесуществуеттакойреализацииFrom , которая позволяет конвертировать типы ошибокio::Error иnum::ParseIntError в наш собственный типCliError . Но мы можем легко этоисправить!ПосколькумыопределилитипCliError ,мыможемтакжереализоватьдлянеготипажFrom :

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

Наконец,мыможемпереписатьfile_double :

usestd::fs::File;usestd::io::{self,Read};usestd::num;usestd::path::Path;

//Мыреализуем`Debug`поскольку,повсейвидимости,всетипыдолжныреализовывать`Debug`.//ЭтодаетнамвозможностьполучитьадекватноеичитаемоеописаниезначенияCliError#[derive(Debug)]enumCliError{Io(io::Error),Parse(num::ParseIntError),}

fnfile_double_verbose<P:AsRef<Path>>(file_path:P)->Result<i32,CliError>{letmutfile=try!(File::open(file_path).map_err(CliError::Io));letmutcontents=String::new();try!(file.read_to_string(&mutcontents).map_err(CliError::Io));letn:i32=try!(contents.trim().parse().map_err(CliError::Parse));Ok(2*n)}

usestd::io;usestd::num;

implFrom<io::Error>forCliError{fnfrom(err:io::Error)->CliError{CliError::Io(err)}}

implFrom<num::ParseIntError>forCliError{fnfrom(err:num::ParseIntError)->CliError{CliError::Parse(err)}}

ЯзыкпрограммированияRust

134Обработкаошибок

Page 135: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Единственное, что мы сделали— это удалили вызовыmap_err . Они нам больше ненужны, поскольку макросtry! выполняетFrom::from над значениями ошибок. И этоработает, поскольку мы предоставили реализацииFrom для всех типов ошибок, которыемогутвозникнуть.

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

ИдобавитьновуюреализациюдляFrom :

Вотивсе!

РекомендациидляавторовбиблиотекЕсли в вашей библиотеке могут возникать специфические ошибки, то вы наверняка

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

usestd::fs::File;usestd::io::Read;usestd::path::Path;

fnfile_double<P:AsRef<Path>>(file_path:P)->Result<i32,CliError>{letmutfile=try!(File::open(file_path));letmutcontents=String::new();try!(file.read_to_string(&mutcontents));letn:i32=try!(contents.trim().parse());Ok(2*n)}

usestd::io;usestd::num;

enumCliError{Io(io::Error),ParseInt(num::ParseIntError),ParseFloat(num::ParseFloatError),}

usestd::num;

implFrom<num::ParseFloatError>forCliError{fnfrom(err:num::ParseFloatError)->CliError{CliError::ParseFloat(err)}}

ЯзыкпрограммированияRust

135Обработкаошибок

Page 136: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Как минимум, вы скорее всего должны реализовать типажError . Это дастпользователямвашейбиблиотекинекоторуюминимальнуюгибкостьприсовмещенииошибок.Реализация типажаError также означает, что пользователям гарантируется возможностьполучения строкового представления ошибки (это следует из необходимости реализацииfmt::Debug иfmt::Display ).

Кроме того, может быть полезным реализоватьFrom для ваших типов ошибок. Этопозволит вам (как автору библиотеки) и вашим пользователямсовмещать более детальныеошибки. Например,csv::Error реализуетFrom дляio::Error иbyteorder::Error .

Наконец, на свое усмотрение, вы также можете определитьпсевдоним типаResult ,особенно, если в вашей библиотеке определен только один тип ошибки. Такой подходиспользуетсявстандартнойбиблиотекедляio::Result иfmt::Result .

ЗаключениеПосколькуэтодовольнодлиннаяглава,небудетлишнимсоставитькороткийконспектпо

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

Есливыпишетекороткийпримеркода,которыйможетбытьперегруженобработкойошибок, это, вероятно, отличная возможность использоватьunwrap (будь-тоResult::unwrap , Option::unwrap илиOption::expect ). Те, для когопредназначенпример,должныосознавать,чтонеобходимореализоватьнадлежащуюобработкуошибок.(Еслинет,отправляйтеихсюда!)Если вы пишете одноразовую программу, также не зазорно использоватьunwrap .Но будьте внимательны: если ваш код попадет в чужие руки, не удивляйтесь, есликто-тобудетрасстроениз-заскудныхсообщенийобошибках!Если вы пишете одноразовый код, но вам все-равно стыдно из-за использованияunwrap , воспользуйтесьлибоString вкачестветипаошибки,либоBox<Error+Send+Sync> (из-задоступныхреализацийFrom .)В остальных случаях, определяйте свои собственные типы ошибок ссоответствующимиреализациямиFrom иError ,делаяиспользованиеtry! болееудобным.Если вы пишете библиотеку и ваш код может выдавать ошибки, определите вашсобственный тип ошибкии реализуйте типажstd::error::Error .Там,гдеэтоуместно, реализуйтеFrom , чтобы вам и вашим пользователям было легче с нимиработать. (Из-за правил когерентности в Rust, пользователи вашей библиотеки несмогут реализоватьFrom для ваших ошибок, поэтому это должна сделать вашабиблиотека.)Изучитекомбинаторы,определенныедляOption иResult .Писатькод,пользуясь

ЯзыкпрограммированияRust

136Обработкаошибок

Page 137: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

тольконимиможетбытьнемногоутомительно,нояличнонашелдлясебяхорошийбаланс между использованиемtry! и комбинаторами (and_then , map иunwrap_or —моилюбимые).

ЯзыкпрограммированияRust

137Обработкаошибок

Page 138: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

расходамиигарантиямипрограммы.

ВстандартнойбиблиотекеRustестьразличные«обёрточныетипы»,которыереализуютмножествокомпромиссовмеждунакладнымирасходами,эргономикой,игарантиями.Многиепозволяют выбирать между проверками во время компиляции и проверками во времяисполнения.Этаглаваподробнообъяснитнесколькоизбранныхабстракций.

Перед тем, как продолжить, крайне рекомендуем познакомиться свладением изаимствованиемвRust.

Основныетипыуказателей

Box<T>Box<T>   — «владеющий» указатель, или, по-другому, «упаковка». Хотя он и может

выдаватьссылкинасодержащиесявнёмданные,он —единственныйвладелецэтихданных.Вчастности,когдапроисходитчто-товродеэтого:

Здесь упаковка былаперемещена вy . Посколькуx больше не владеет ею, с этогомоментакомпиляторнепозволитиспользоватьx .Упаковкатакжеможетбытьперемещенаизфункции —дляэтогофункциявозвращаетеёкаксвойрезультат.

Когда упаковка, которая не была перемещена, выходит из области видимости,выполняютсядеструкторы.Этидеструкторыосвобождаютсодержащиесяданные.

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

&T и&mutTЭтонеизменяемыеиизменяемыессылки,соответственно.Ониреализуютшаблон«read-

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

letx=Box::new(1);lety=x;//xбольшенедоступен

ЯзыкпрограммированияRust

138Выборгарантий

Page 139: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

При копировании эти указатели сохраняют связанное с ними время жизни — они всёравнонемогутпрожитьдольше,чемисходноезначение,накотороеониссылаются.

*constT и*mutTЭтосырыеуказателивстилеC,неимеющиесвязаннойинформацииовременижизнии

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

Ониполезныпри созданиибезопасныхнизкоуровневыхабстракцийвродеVec<T> , ноихследуетизбегатьвбезопасномкоде.

Rc<T>Этоперваярассматриваемаяобёртка,использованиекоторойвлечётзасобойнакладные

расходывовремяисполнения.

Rc<T>  —этоуказательсосчётчикомссылок.Другимисловами,онпозволяетсоздаватьнесколько«владеющих»указателейнаодниитежеданные,иэтиданныебудутуничтожены,когдавсеуказателивыйдутизобластивидимости.

Собственно, внутри у него счётчик ссылок (reference count, или сокращённо refcount),которыйувеличиваетсякаждыйраз,когдапроисходитклонированиеRc ,иуменьшаетсякогдаRc выходит из области видимости. Основная ответственностьRc<T>  — удостовериться втом,чтодляразделяемыхданныхвызываютсядеструкторы.

Хранимыеданныеприэтомнеизменяемы,иеслисоздаётсяциклссылок,данныеутекут.Если нам нужно отсутствие утечек в присутствие циклов, нужно использовать сборщикмусора.

ГарантииЗдесьглавнаягарантиявтом,чтоданныенебудутуничтожены,покавсессылкинаних

неисчезнут.

Счётчик ссылок нужно использовать, когда мы хотим динамически выделить какие-тоданные и предоставить ссылки на эти данные только для чтения, и при этом неясно, какаячасть программы последней закончит использование ссылки. Это подходящая альтернатива&T , когданевозможностатическидоказатьправильность&T ,иликогдаэтосоздаётслишкомбольшиенеудобствавнаписаниикода,накоторыйразработчикнехочеттратитьсвоёвремя.

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

ЯзыкпрограммированияRust

139Выборгарантий

Page 140: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Есть похожий умный указатель,Weak<T> . Это невладеющий, но и не заимствуемый,умныйуказатель.Онтожепохожна&T ,нонеограниченвременемжизни —Weak<T> можнонеотпускать.Однако,возможнаситуация,когдапопыткадоступакхранимымвнёмданнымпровалитсяивернётNone ,посколькуWeak<T> можетпережитьвладеющиеRc .Егоудобноиспользоватьвслучаециклическихструктурданныхинекоторыхдругих.

НакладныерасходыЧто касается памяти,Rc<T>   — это одно выделение, однако оно будет включать два

лишнихслова(т.е.двазначениятипаusize )посравнениюсобычнымBox<T> .Этоверноидля«сильных»,идля«слабых»счётчиковссылок.

Расходы наRc<T> заключаютсявувеличениииуменьшениисчётчикассылоккаждыйраз, когдаRc<T> клонируетсяиливыходитизобластивидимости,соответственно.Отметим,что клонирование не выполняет глубокое копирование, а просто увеличивает счётчик ивозвращаеткопиюRc<T> .

Типы-ячейки(celltypes)Ти пыCell предоставляют «внутреннюю» изменяемость. Другими словами, они

содержат данные, которые можно изменять даже если тип не может быть получен визменяемомвиде(например,когдаонзауказателем& илизаRc<T> ).

Документациямодуляcell довольнохорошообъясняетэтивещи.

Эти типыобычно используют в полях структур, но они не ограничены такимиспользованием.

Cell<T>Cell<T>  —этотип,которыйобеспечиваетвнутреннююизменяемостьбезнакладных

расходов,но толькодля типов, реализующихтипажCopy .Посколькукомпиляторзнает,чтовседанные,вложенныевCell<T> ,находятсянастеке,ихможнопростозаменятьбезстрахаутечкиресурсов.

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

ЯзыкпрограммированияRust

140Выборгарантий

Page 141: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Заметьте, что здесь мы смогли изменить значение через различные ссылки без праваизменения.

Впланезатратвовремяисполнения,такойкоданалогиченнижеследующему:

ноимеетпреимуществовтом,чтоондействительнокомпилируется.

ГарантииЭтот тип ослабляет правило отсутствия совпадающих указателей с правом записи там,

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

Этоприменяетсяприизменениипримитивовидругихтипов,реализующихCopy ,когданетлёгкогоспособасделатьэтовсоответствиисстатическимиправилами& и&mut .

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

НакладныерасходыНакладные расходы при использованииCell<T> отсутствуют, однако если вы

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

RefCell<T>RefCell<T> такжепредоставляетвнутреннююизменяемость,нонеограничентолько

типами,реализующимиCopy .

usestd::cell::Cell;

letx=Cell::new(1);lety=&x;letz=&x;x.set(2);y.set(3);z.set(4);println!("{}",x.get());

letmutx=1;lety=&mutx;letz=&mutx;x=2;*y=3;*z=4;println!("{}",x);

ЯзыкпрограммированияRust

141Выборгарантий

Page 142: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Однако, у этого решения есть накладные расходы.RefCell<T> реализует шаблон«read-writelock»вовремяисполнения,аневовремякомпиляции,как&T /&mutT .Онпохожна однопоточный мьютекс. У него есть функцииborrow() иborrow_mut() , которыеизменяют внутрений счётчик ссылок и возвращают умный указатель, который может бытьразыменован без права изменения или с ним, соответственно. Счётчик ссылоквосстанавливается,когдаумныеуказателивыходятизобластивидимости.Сэтойсистемоймыможемдинамическигарантировать,чтововремязаимствованиясправомизмененияникакихдругихссылокназначениебольшенет.Еслипрограммистпытаетсяпозаимствоватьзначениевэтотмомент,потокзапаникует.

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

Для больших, сложных программ, есть смысл положить некоторые вещи вRefCell ,чтобы упростить работу с ними. Например, многие словари в структуреctxtctxt вкомпилятореRustобёрнутывэтоттип.Ониизменяютсятолькооднажды —вовремясоздания,ноневовремяинициализации,илинесколькоразвявноотдельныхместах.Однако,посколькуэтаструктураповсеместноиспользуетсявезде,жонглированиеизменяемымиинеизменяемымиуказателямибылобыоченьсложным(илиневозможным),инавернякасоздалобымешанинууказателей& , которую сложно было бы расширять. С другой стороны,RefCellпредоставляетдешёвый (нонебесплатный) способобращатьсяк такимданным.Вбудущем,если кто-то добавит код, который пытается изменить ячейку, пока она заимствована, этовызывет панику, источник которой можно отследить. И такая паника обычно происходитдетерминированно.

Похожим образом, в DOM Servo много изменения данных, большая часть которогопроисходит внутри типа DOM, но часть выходит за его границы и изменяет произвольныевещи. ИспользованиеRefCell иCell для ограждения этих изменений позволяет намизбежать необходимости беспокоиться об изменяемости везде, и одновременно обозначаетместа,гдеизменениедействительнопроисходит.

Заметьте, что стоит избегать использованияRefCell , если возможно достаточнопростоерешениеспомощьюуказателей& .

usestd::cell::RefCell;

letx=RefCell::new(vec![1,2,3,4]);{println!("{:?}",*x.borrow())}

{letmutmy_ref=x.borrow_mut();my_ref.push(1);}

ЯзыкпрограммированияRust

142Выборгарантий

Page 143: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ГарантииRefCell ослабляетстатические ограничения, предотвращающие совпадение

изменяемыхуказателей,изаменяетихнадинамическиеограничения.Самигарантииприэтомнеизменяются.

НакладныерасходыRefCell не выделяет память, но содержит дополнительный индикатор «состояния

заимствования»(размеромводнослово)вместесданными.

Во время исполнения каждое заимствование вызывает изменение и проверку счётчикассылок.

СинхронизированныетипыМногие из вышеперечисленных типов не могут быть использованы потокобезопасным

образом. В частности,Rc<T> иRefCell<T> , оба из которых используют не-атомарныесчётчикиссылок,немогутбытьиспользованытак.(Атомарныесчётчикиссылок —этотакие,которыемогут быть увеличеныиз нескольких потоков, не вызывая при этом гонку данных.)Благодаря этому они привносят меньше накладных расходов, но нам также потребуются ипотокобезопасные варианты этих типов. Они существуют  — этоArc<T> иMutex<T> /RWLock<T> .

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

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

Arc<T>Arc<T>   — это вариантRc<T> , который использует атомарный счётчик ссылок

(поэтому«Arc»).Егоможносвободнопередаватьмеждупотоками.

shared_ptr из C++ похож наArc , но в случае C++ вложенные данные всегдаизменяемы. Чтобы получить семантику, похожую на семантику C++, нужно использоватьArc<Mutex<T>> , Arc<RwLock<T>> , илиArc<UnsafeCell<T>> .(UnsafeCell<T>  — это тип-ячейка, который может содержать любые данные и не имеетнакладныхрасходов,нодоступкегосодержимомупроизводитсятольковнутринебезопасныхблоков.)Последнийстоитиспользоватьтолькотогда,когдамыуверенывтом,чтонашаработаневызыветнарушениябезопасностипамяти.Учитывайте,чтозаписьвструктурунеатомарна,а многие функции вродеvec.push() могут выделятьпамять заново в процессе работы, итемсамымвызыватьнебезопасноеповедение.

Гарантии

1

ЯзыкпрограммированияRust

143Выборгарантий

Page 144: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Как иRc , этоттипгарантирует,чтодеструкторхранимыхвнёмданныхбудетвызван,когдапоследнийArc выходитизобластивидимости(заисключениемслучаевсциклами).ВотличиеотRc ,Arc предоставляетэтугарантиюивмногопоточномокружении.

НакладныерасходыНакладные расходы увеличиваются по сравнению сRc , т.к. теперь для изменения

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

Mutex<T> andRwLock<T>Mutex<T> иRwLock<T> предоставляют механизм взаимоисключения с помощью

охранныхзначенийRAII.Охранныезначения —этообъекты,имеющиенекотороесостояние,какзамок,поканевыполнитсяихдеструктор.Вобоихслучаях,мьютекснепрозрачен,покананёмневызовутlock() ,послечегопотокостановитсядомомента,когдамьютексможетбытьзакрыт, после чего возвращается охранное значение. Оно может быть использовано длядоступа к вложенным данным с правом изменения, а мьютекс будет снова открыт, когдаохранноезначениевыйдетизобластивидимости.

RwLock имеет преимущество  — он эффективно работает в случае множественныхчтений. Ведь читать из общих данных всегда безопасно, пока в эти данные никто не хочетписать;иRwLock позволяетчитающимполучить«правочтения».Правочтенияможетбытьполученомногимипотокамиодновременно,изачитающимиследитсчётчикссылок.Тотже,кто хочет записать данные, должен получить «право записи», а оно может быть полученотолькокогдавсечитающиевышлиизобластивидимости.

ГарантииОба этих типа предоставляют безопасное изменение данных из разных потоков, но не

защищают от взаимной блокировки (deadlock). Некоторая дополнительная безопасностьпротоколаработысданнымиможетбытьполученаспомощьюсистемытипов.

НакладныерасходыДля поддержания состояния прав чтения и записи эти типы используют в своей

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

{letguard=mutex.lock();//охранноезначениеразыменовываетсявизменяемоезначение//вложенноговмьютекстипа*guard+=1;}//мьютексоткрываетсякогдавыполняетсядеструктор

ЯзыкпрограммированияRust

144Выборгарантий

Page 145: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

СочетаниеРаспространённая жалоба на код на Rust  — это сложность чтения типов вроде

Rc<RefCell<Vec<T>>> (или ещё более сложных сочетаний похожих типов). Не всегдапонятно, что делает такая комбинация, или почему автор решил использовать именно такойтип.Неясноито,вкакихслучаяхсампрограммистдолжениспользоватьпохожиесочетаниятипов.

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

Например,одноизтакихсочетаний —этоRc<RefCell<T>> .Сампо себеRc<T> неможетбытьразыменовансправомизменения;посколькуRc<T> позволяетделитьсяданнымииодновременнаяпопыткаизмененияданныхможетпривестикнебезопасномуповедению,мыкладём внутрьRefCell<T> , чтобы получить динамическую проверку одновременныхпопыток изменения. Теперь у нас есть разделяемые изменяемые данные, но одновременныйдоступкнимпредоставляетсятольконачтение,азаписьвсегдаисключительна.

Далее мы можем развить эту мысль и получитьRc<RefCell<Vec<T>>> илиRc<Vec<RefCell<T>>> .Это —изменяемые,разделяемыемеждупотокамивектора,ноонинеодинаковы.

В первом типеRefCell<T> оборачиваетVec<T> , поэтому изменяем весьVec<T>целиком.В тоже время, это значит, что в каждыймомент времениможет быть только однассылка наVec<T> с правом изменения. Поэтому код не может одновременно работать сразными элементами вектора, обращаясь к ним через разныеRc . Однако, мы сможемдобавлятьиудалятьэлементывекторавпроизвольныемоментывремени.Этоттиппохожна&mutVec<T> ,стемразличием,чтопроверказаимствованияделаетсявовремяисполнения.

Во втором типе заимствуются отдельные элементы, а вектор в целом неизменяем.Поэтому мы можем получить ссылки на отдельные элементы, но не можем добавлять илиудалять элементы. Это похоже на&mut [T] , но, опять-таки, проверка заимствованияпроизводитсявовремяисполнения.

ВмногопоточныхпрограммахвозникаетпохожаяситуациясArc<Mutex<T>> ,которыйобеспечиваетразделяемоевладениеиодновременноеизменение.

Когда вы читаете такой код, рассматривайте гарантии и накладные расходы каждоговложенноготипашагзашагом.

Когда вы выбираете сложный тип, поступайте наоборот: решите, какие гарантии вамнужны,ивкаком«слое»сочетанияонипонадобятся.Например,еслиувасстоитвыбормеждуVec<RefCell<T>> иRefCell<Vec<T>> , найдите компромисс путём рассуждений, какмыделаливышепотексту,ивыберитенужныйвамтип.

2

ЯзыкпрограммированияRust

145Выборгарантий

Page 146: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

1. На самом деле,Arc<UnsafeCell<T>> не скомпилируется, посколькуUnsafeCell<T> нереализуетSend илиSync ,номыможемобернутьеговтипиреализовать для негоSend /Sync вручную, чтобы получитьArc<Wrapper<T>> ,гдеWrapper  —этоstructWrapper<T>(UnsafeCell<T>) .↩

2. &[T] и&mut[T]  —этосрезы; они состоятизуказателяидлины,имогутссылатьсяначастьвектораилимассива.&mut[T] такжепозволяетизменятьсвоиэлементы,ноегодлинуизменитьнельзя.↩

ЯзыкпрограммированияRust

146Выборгарантий

Page 147: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Интерфейс внешних функций (foreign functioninterface)

ВведениеВданномруководствевкачествепримерамыбудемиспользоватьsnappy,библиотекудля

сжатия/распаковки данных. Мы реализуем Rust-интерфейс к этой библиотеке через вызоввнешних функций. Rust в настоящее время не в состоянии делать вызовы напрямую вбиблиотекиC++,ноsnappyвключаетвсебяинтерфейсC(документированвsnappy-c.h ).

Ниже приведен минимальный пример вызова внешней функции, который будетскомпилированприусловии,чтобиблиотекаsnappyустановлена:

Блокextern содержит список сигнатур функций из внешней библиотеки, в данномслучае для C ABI (application binary interface; двоичный интерфейс приложений) даннойплатформы. Чтобы указать, что программу нужно компоновать с библиотекой snappy,используетсяатрибут#[link(...)] .Благодаряэтому,символыбудутуспешноразрешены.

Предполагается,чтовнешниефункциимогутбытьнебезопасными,поэтомуихвызовыдолжныбытьобёрнутывблокunsafe{} какобещаниекомпилятору,чтовсевнутриэтогоблокавдействительностибезопасно.БиблиотекиCчастопредоставляютинтерфейсы,которыене являются потоко-безопасными. И почти любая функция, которая принимает в качествеаргументауказатель,неможетприниматьлюбоевходноезначений,посколькууказательможетбытьвисячим;сырыеуказателивыходятзапределыбезопасноймоделипамятивRust.

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

Блокextern можетбытьраспространённавесьAPIsnappy:

externcratelibc;uselibc::size_t;

#[link(name="snappy")]extern{fnsnappy_max_compressed_length(source_length:size_t)->size_t;}

fnmain(){letx=unsafe{snappy_max_compressed_length(100)};println!("максимальныйразмерсжатогобуферадлиной100байт:{}",x);}

ЯзыкпрограммированияRust

147Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 148: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СозданиебезопасногоинтерфейсаСырой C API (application programming interface; интерфейс программирования

приложений)необходимообернуть,чтобыобеспечитьбезопасностьпамяти.Тогдамысможемиспользовать концепции более высокого уровня, такие как векторы. Библиотека можетвыборочно открывать только безопасный, высокоуровневый интерфейс и скрыватьнебезопасныевнутренниедетали.

Оборачивание функций, которые принимают в качестве входных параметров буферы,включает в себя использование модуляslice::raw для управления векторами Rust какуказателяминапамять.ВекторыRustпредставляютсобойгарантированнонепрерывныйблокпамяти.Длина —этоколичествоэлементов,котороевнастоящеевремясодержитсяввекторе,а ёмкость  — общее количество выделенной памяти в элементах. Длина меньше или равнаёмкости.

Обёрткаvalidate_compressed_buffer использует блокunsafe , но этогарантирует,чтоеёвызовбудетбезопасендлявсехвходныхданных,посколькумодификаторunsafe отсутствуетв сигнатурефункции.Т.е.небезопасностьскрытавнутрифункциииневиднавызывающему.

Функцииsnappy_compress иsnappy_uncompress являютсяболеесложными,таккакдолженбытьвыделенбуфердляхранениявыходныхданных.

externcratelibc;uselibc::{c_int,size_t};

#[link(name="snappy")]extern{fnsnappy_compress(input:*constu8,input_length:size_t,compressed:*mutu8,compressed_length:*mutsize_t)->c_int;fnsnappy_uncompress(compressed:*constu8,compressed_length:size_t,uncompressed:*mutu8,uncompressed_length:*mutsize_t)->c_int;fnsnappy_max_compressed_length(source_length:size_t)->size_t;fnsnappy_uncompressed_length(compressed:*constu8,compressed_length:size_t,result:*mutsize_t)->c_int;fnsnappy_validate_compressed_buffer(compressed:*constu8,compressed_length:size_t)->c_int;}

pubfnvalidate_compressed_buffer(src:&[u8])->bool{unsafe{snappy_validate_compressed_buffer(src.as_ptr(),src.len()assize_t)==0}}

ЯзыкпрограммированияRust

148Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 149: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Функцияsnappy_max_compressed_length может быть использована длявыделениявекторамаксимальнойёмкости,требуемойдляхранениясжатыхвыходныхданных.Затемэтотвекторможетбытьпереданвфункциюsnappy_compress вкачествевыходногопараметра.Ещёодинпараметрпередается,чтобыполучитьнастоящуюдлинупослесжатияиустановитьсоответствующуюдлинувектора.

Распаковка аналогична, потому что snappy хранит размер несжатых данных как частьформата сжатия, иsnappy_uncompressed_length будет возвращать точный размернеобходимогобуфера.

Длясправки,примеры,используемыездесь,такжедоступнывбиблиотекенаGitHub.

ДеструкторыВнешниебиблиотекичастопередаютвладениересурсамиввызывающийкод.Когдаэто

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

pubfncompress(src:&[u8])->Vec<u8>{unsafe{letsrclen=src.len()assize_t;letpsrc=src.as_ptr();

letmutdstlen=snappy_max_compressed_length(srclen);letmutdst=Vec::with_capacity(dstlenasusize);letpdst=dst.as_mut_ptr();

snappy_compress(psrc,srclen,pdst,&mutdstlen);dst.set_len(dstlenasusize);dst}}

pubfnuncompress(src:&[u8])->Option<Vec<u8>>{unsafe{letsrclen=src.len()assize_t;letpsrc=src.as_ptr();

letmutdstlen:size_t=0;snappy_uncompressed_length(psrc,srclen,&mutdstlen);

letmutdst=Vec::with_capacity(dstlenasusize);letpdst=dst.as_mut_ptr();

ifsnappy_uncompress(psrc,srclen,pdst,&mutdstlen)==0{dst.set_len(dstlenasusize);Some(dst)}else{None//SNAPPY_INVALID_INPUT}}}

ЯзыкпрограммированияRust

149Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 150: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Чтобыполучитьболееподробнуюинформациюодеструкторах,смотритетипажDrop.

Обратные вызовы функций Rust кодом на C(CallbacksfromCcodetoRust

functions)Некоторыевнешниебиблиотекитребуютиспользованиеобратныхвызововдляпередачи

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

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

Простойпример:

КоднаRust:

КоднаC:

typedefvoid(*rust_callback)(int32_t);rust_callbackcb;

int32_tregister_callback(rust_callbackcallback){cb=callback;return1;}

voidtrigger_callback(){cb(7);//Вызоветcallback(7)вRust}

externfncallback(a:i32){println!("МенявызываютизCсозначением{0}",a);}

#[link(name="extlib")]extern{fnregister_callback(cb:externfn(i32))->i32;fntrigger_callback();}

fnmain(){unsafe{register_callback(callback);trigger_callback();//Активацияфункцииобратноговызова}}

ЯзыкпрограммированияRust

150Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 151: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вэтомпримерефункцияmain() вRust вызоветфункциюtrigger_callback() вC,которая,всвоюочередь,выполнитобратныйвызовфункцииcallback() вRust.

Обратные вызовы, адресованные объектам Rust(TargetingcallbackstoRust

objects)Предыдущий пример показал, как глобальнаяфункцияможет быть вызвана изC кода.

Однако зачастую желательно, чтобы обратный вызов был адресован конкретному объекту вRust. Это может быть объект, который представляет собой обертку для соответствующегообъектаC.

Такое поведение может быть достигнуто путем передачи небезопасного указателя наобъектвбиблиотекуC.ПослечегобиблиотекаCсможетпередаватьуказательнаобъектRustприобратномвызове.ЭтопозволитполучитьнебезопасныйдоступкобъектуRust,накоторойсослалисьвобратномвызове.

КоднаRust:

#[repr(C)]structRustObject{a:i32,//другиеполя}

extern"C"fncallback(target:*mutRustObject,a:i32){println!("МенявызываютизCсозначением{0}",a);unsafe{//МеняемзначениевRustObjectназначение,полученноечерезфункциюобратноговызова(*target).a=a;}}

#[link(name="extlib")]extern{fnregister_callback(target:*mutRustObject,cb:externfn(*mutRustObject,i32))->i32;fntrigger_callback();}

fnmain(){//Создаёмобъект,накоторыйбудемссылатьсявфункцииобратноговызоваletmutrust_object=Box::new(RustObject{a:5});

unsafe{register_callback(&mut*rust_object,callback);trigger_callback();}}

ЯзыкпрограммированияRust

151Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 152: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

КоднаC:

typedefvoid(*rust_callback)(void*,int32_t);void*cb_target;rust_callbackcb;

int32_tregister_callback(void*callback_target,rust_callbackcallback){cb_target=callback_target;cb=callback;return1;}

voidtrigger_callback(){cb(cb_target,7);//Вызоветcallback(&rustObject,7)вRust}

АсинхронныеобратныевызовыВприведённыхпримерахобратныевызовывыполняютсякакнепосредственнаяреакция

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

Болеесложнаяситуация —этокогдавнешняябиблиотекапорождаетсвоисобственныепотокииосуществляетобратныевызовыизних.ВэтихслучаяхдоступкструктурамданныхRustизобратныхвызововособенноопасен,ипоэтомунужноиспользоватьсоответствующиемеханизмы синхронизации. Помимо классических механизмов синхронизации, таких какмьютексы, в Rust есть еще одна возможность: использовать каналы(std::sync::mpsc::channel ),чтобынаправитьданныеизпотокаC,которыйвыполнялобратныйвызов,впотокRust.

Если асинхронный обратный вызов адресован конкретному объекту в адресномпространстве Rust, то необходимо, чтобы обратные вызовы не выполнялись библиотекой Cпосле уничтожения этого объекта Rust. Для этого следует, во-первых, проектироватьбиблиотекутакимобразом,чтобыотменарегистрацииобратноговызовагарантировала,чтоонбольше не будет выполняться. Во-вторых, нужно отменить регистрацию обратного вызова вдеструктореобъектаRust,которомуадресованобратныйвызов.

КомпоновкаАтрибутlink для блоковextern предоставляетrustc основные инструкции

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

#[link(name="foo")]#[link(name="foo",kind="bar")]

ЯзыкпрограммированияRust

152Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 153: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вобоихэтихслучаяхfoo  —этоимянативнойбиблиотеки,скотороймыкомпонуемся.Вовторомслучаеbar  —этотипнативнойбиблиотеки,скоторойпроисходиткомпоновка.Внастоящеевремяrustc известнытритипанативныхбиблиотек:

Динамические —#[link(name="readline")]Статические  —#[link(name = "my_build_dependency", kind ="static")]Фреймворки  —#[link(name = "CoreFoundation", kind ="framework")]

Обратитевнимание,чтофреймворкидоступнытолькодляOSX.

Различные значенияkind нужны, чтобы определить, как компоновать нативнуюбиблиотеку. С точки зрения компоновки, компилятор Rust создает две разновидностиартефактов: промежуточный (rlib/статическая библиотека) и конечный (динамическаябиблиотека/исполняемый файл). (Прим. переводчика: rlib  — это формат статическойбиблиотеки с метаданными в формате Rust) Зависимости от нативных динамическихбиблиотекифреймворковраспространяютсядальше,поканедойдутдоконечногоартефакта,аотстатическихбиблиотек —нет.

Вотнесколькопримеровтого,какэтамодельможетбытьиспользована:

Нативнаязависимостьприсборке.ИногданаписанныйнаRustкоднеобходимосостыковатьснекоторымкодомнаC/C++,нораспространениеC/C++кодавформатебиблиотекивызываетдополнительныетрудности.Вэтомслучае,кодбудутупакованвlibfoo.a ,азатемконтейнерRustдолженбудетобъявитьзависимостьспомощью#[link(name="foo",kind="static")] .

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

Обычнаядинамическаязависимость.Общиесистемныебиблиотеки(такие,какreadline ) доступны на большом количестве систем, и статическую копию этихбиблиотекчастосложнонайти.КогдатакаязависимостьвключенавконтейнерRust,промежуточныеартефакты(например,rlib'ы)небудуткомпоноватьсясбиблиотекой,но когда rlib включается в состав конечного артефакта (например, исполняемыйфайл),нативнаябиблиотекабудетприкомпонована.

НаOSX,фреймворкиведутсебятакже,какидинамическиебиблиотеки.

НебезопасныеблокиНекоторые операции, такие как разыменование небезопасных указателей или вызов

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

ЯзыкпрограммированияRust

153Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 154: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Небезопасные функции же, наоборот, показывают свою опасность всем. Небезопаснаяфункциязаписываетсяввиде:

Эта функция может быть вызвана только из блокаunsafe или из другойunsafeфункции.

ДоступквнешнимглобальнымпеременнымВнешниеAPIдовольночастоэкспортируютглобальныепеременные,которыемогутбыть

использованы,например,дляотслеживанияглобальногосостояния.Длятого,чтобыполучитьдоступ к этим переменным, нужно объявить их в блокеextern , используяключевое словоstatic :

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

unsafefnkaboom(ptr:*consti32)->i32{*ptr}

externcratelibc;

#[link(name="readline")]extern{staticrl_readline_version:libc::c_int;}

fnmain(){println!("Youhavereadlineversion{}installed.",rl_readline_versionasi32);}

externcratelibc;

usestd::ffi::CString;usestd::ptr;

#[link(name="readline")]extern{staticmutrl_prompt:*constlibc::c_char;}

fnmain(){letprompt=CString::new("[my-awesome-shell]$").unwrap();unsafe{rl_prompt=prompt.as_ptr();

println!("{:?}",rl_prompt);

rl_prompt=ptr::null();}}

ЯзыкпрограммированияRust

154Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 155: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Обратите внимание, что любое взаимодействие сstatic mut небезопасно  — какчтение, так и запись. Работа с изменяемым глобальным состоянием требует значительнобольшейосторожности.

СоглашениеовызовевнешнихфункцийБольшинствовнешнегокодапредоставляетCABI.ИRustпривызовевнешнихфункций

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

Это указание относится ко всему блокуextern . Вот список поддерживаемыхограниченийдляABI:

stdcallaapcscdeclfastcallRustrust-intrinsicsystemCwin64

Большинство ABI в этом списке не требуют пояснений, но ABIsystem можетпоказатьсянемногостранным.ОнвыбираеттакоеABI,котороеподходитдлявзаимодействияснативнымибиблиотекамиданнойплатформы.Например,наплатформеwin32сархитектуройx86, это означает, что будет использован ABIstdcall . Однако, на windows x86_64используется соглашение о вызовеC , поэтому в этом случае будет использованC ABI.Этоозначает,чтовнашемпредыдущемпримеремымоглибыиспользоватьextern"system"{...} ,чтобыопределитьблокдлявсехwindowsсистем,анетолькодляx86.

ВзаимодействиесвнешнимкодомRust гарантирует, что размещение полейstruct совместимо с представлением в C

только в том случае, если к ней применяется атрибут#[repr(C)] . Атрибут#[repr(C,packed)] может быть использован для размещения полей структуры без выравнивания.

externcratelibc;

#[cfg(all(target_os="win32",target_arch="x86"))]#[link(name="kernel32")]#[allow(non_snake_case)]extern"stdcall"{fnSetEnvironmentVariableA(n:*constu8,v:*constu8)->libc::c_int;}

ЯзыкпрограммированияRust

155Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 156: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Атрибут#[repr(C)] такжеможетбытьпримененикперечислениям.

ВладеющиеупаковкивRust(Box<T> )используютуказатели,недопускающиенулевоезначение (non-nullable), как дескрипторы содержащихся в них объектов. Тем не менее, этидескрипторы не должны создаваться вручную, так как они управляются внутреннимисредствами выделения памяти. Ссылки можно без риска считать ненулевыми указателяминепосредственонатип.Однаконарушениеправилпроверкизаимствованияилиизменяемостиможет быть небезопасным. Но компилятор не может сделать так много предположений осырых указателях. Например, он не полагается на настоящую неизменяемость данных поднеизменяемым сырым указателем. Поэтому используйте сырые указатели (* ), если вамнеобходимо намеренно нарушить правила (но так, что при этом всё работает). Это нужно,чтобы компилятор «случайно» не предположил относительно ссылок чего-то, что мысобираемсянарушать(возможно,намнужнынесколькоуказателейсправомизменения,чтонедопускаетсяобычнымиссылками).

Векторыистрокисовместноиспользуютоднуитужебазовуюcхемуразмещенияпамятии утилиты, доступные в модуляхvec иstr , для работы с C API. Однако, строки незавершаются нулевым байтом,\0 .Есливамнужнастрока, завершающаясянулевымбайтом,длясовместимостисC,выдолжныиспользоватьтипCString измодуляstd::ffi .

СтандартнаябиблиотекавключаетвсебяпсевдонимытиповиопределенияфункцийдлястандартнойбиблиотекиCвмодулеlibc ,иRustкомпонуетlibc иlibm поумолчанию.

Оптимизация указателей, допускающих нулевоезначение

(Thenullablepointeroptimization)Некоторые типы по определению не могут бытьnull . Это ссылки (&T , &mut T ),

упаковки(Box<T> ),указателинафункции(extern"abi"fn() ).Привзаимодействиижес С часто используются указатели, которые могут бытьnull . Как особый случай  —обобщенныйenum , который содержит ровно два варианта, один из которых не содержитданных, а другой содержит одно поле. Такое использование перечисления имеет право на«оптимизацию указателя, допускающего нулевое значение». Когда создан экземпляр такогоперечислениясоднимизне-обнуляемыхтипов,тоонпредставляетсобойненулевойуказательдля варианта, содержащего данные, и нулевой — для варианта без данных. Таким образом,Option<extern "C" fn(c_int) -> c_int>   — это представление указателя нафункцию,допускающегонулевоезначение,исовместимогосCABI.

ВызовкоданаRustизкоданаCВыможетескомпилироватькоднаRustтакимобразом,чтобыонмогбытьвызванизкода

наC.Этодовольнолегко,нотребуетнесколькихвещей:

ЯзыкпрограммированияRust

156Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 157: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

extern указывает, что эта функцию придерживается соглашения о вызове C, какописано выше в разделе «Соглашение о вызове внешних функций». Атрибутno_mangleвыключает изменение имён, применяемое в Rust, чтобы было легче компоноваться с этимкодом.

#[no_mangle]pubexternfnhello_rust()->*constu8{"Hello,world!\0".as_ptr()}

ЯзыкпрограммированияRust

157Интерфейсвнешнихфункций(foreignfunctioninterface)

Page 158: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Типажи`Borrow`и`AsRef`ТипажиBorrow иAsRef очень похожи, но в то же время отличаются. Ниже

приводитсянебольшаяпамяткаобэтихдвухтипажах.

ТипажBorrowТипажBorrow используется,когдавыпишетеструктуруданныхихотитеиспользовать

владениеизаимствованиетипакаксинонимы.

Например,HashMap имеетметодget ,которыйиспользуетBorrow :

Эта сигнатура является довольно сложной. ПараметрK   — это то, что нас здесьинтересует.ОнссылаетсянапараметрсамогоHashMap :

ПараметрK представляетсобойтипключа,которыйиспользуетHashMap .Взглянемнасигнатуруget() еще раз. Использоватьget() возможно, когда ключ реализуетBorrow<Q> . Таким образом, мы можем сделатьHashMap , который использует ключиString ,ноиспользовать&str ,когдамывыполняемпоиск:

Это возможно, так как стандартная библиотека содержитimpl Borrow<str> forString .

Для большинства типов, когда вы хотите получить право собственности илипозаимствовать значений, достаточно использовать просто&T . Borrow же становитсяполезен,когдаестьболееодноговидазанимаемогозначения.Этоособенновернодляссылокисрезов: у васможет быть как&T , так и&mutT .Еслимыхотимприниматьоба этих типа,Borrow какраздляэтогоподходит:

fnget<Q:?Sized>(&self,k:&Q)->Option<&V>whereK:Borrow<Q>,Q:Hash+Eq

structHashMap<K,V,S=RandomState>{

usestd::collections::HashMap;

letmutmap=HashMap::new();map.insert("Foo".to_string(),42);

assert_eq!(map.get("Foo"),Some(&42));

ЯзыкпрограммированияRust

158Типажи`Borrow`и`AsRef`

Page 159: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этовыведетaзаимствовано:5 дважды.

ТипажAsRefТипажAsRef являетсяпреобразующимтипажом.Ониспользуетсявобобщённомкоде

дляпреобразованиянекоторогозначениявссылку.Например:

Чтовкакомслучаеследуетиспользовать?Мы видим, что они вроде одинаковы: имеют дело с владением и заимствованием

значениянекотороготипа.Темнеменее,этитипажинемногоотличаются.

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

ИспользуйтеAsRef , когда вы пишете обобщённый код и хотите непосредственнопреобразоватьчто-либовссылку.

usestd::borrow::Borrow;usestd::fmt::Display;

fnfoo<T:Borrow<i32>+Display>(a:T){println!("aзаимствовано:{}",a);}

letmuti=5;

foo(&i);foo(&muti);

lets="Hello".to_string();

fnfoo<T:AsRef<str>>(s:T){letslice=s.as_ref();}

ЯзыкпрограммированияRust

159Типажи`Borrow`и`AsRef`

Page 160: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

КаналысборокПроект Rust использует концепцию под названием «каналы сборок» для управления

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

ОбзорЕстьтриканаласборокRust:

Ночной(Nightly)Бета(Beta)Стабильный(Stable)

Новые ночные сборки создаются раз в день. Каждые шесть недель последняя ночнаясборкапереводится в канал«бета».С этогомоментаонабудетполучать толькоисправлениясерьёзных ошибок. Шесть недель спустя бета сборка переводится в канал «стабильный» истановитсяочереднойстабильнойсборкой1.x .

Этотпроцесспроисходитпараллельно.Так,каждыешестьнедель,водинитотжедень,ночнаясборкапревращаетсявбетасборку,абетасборкапревращаетсявстабильнуюсборку.Этопроизойдётодновременно:стабильнаясборкаполучитверсию1.x ,бетасборкаполучитверсию1.(x+1)-beta ,аночнаясборкастанетпервойверсией1.(x+2)-nightly .

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

стабильныхсборок.Этисборкипредназначеныдляширокойаудитории.

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

Помощь экосистеме с помощью непрерывнойинтеграции

Ачтонасчётбетаканала?МыпризываемвсехпользователейRust,которыеиспользуютканал стабильных сборок, также протестировать работу с использованием бета канала в ихсистемах непрерывной интеграции. Это поможет предупредить команду в случае

ЯзыкпрограммированияRust

160Каналысборок

Page 161: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

возникновениянеожиданныхрегрессий.

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

ЯзыкпрограммированияRust

161Каналысборок

Page 162: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СинтаксисисемантикаЭта часть разбита на небольшие главы, каждая из которых описывает определённое

понятиеRust.

ЕсливыхотитеизучитьRust«отидо»,продолжайтечтениеданнойчастипопорядку-вынаверномпути!

Эти главы также являются справочником понятий, так что если при чтении другогоматериалавамбудетчто-тонепонятно,вывсегдасможетенайтиобъяснениездесь.

ЯзыкпрограммированияRust

162Синтаксисисемантика

Page 163: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СвязываниеимёнЛюбаяреальнаяпрограмманаRustпосложнее,чем«HelloWorld»,используетсвязывание

имён.Этовыглядиттак:

Все операции, производимые ниже, будут происходить в функцииmain() , так каккаждыйразвставлятьвпримерыfnmain(){ немногоутомляет.Убедитесь,чтопримеры,приведённыевэтомразделе,вывводитевфункциюmain() ,иначеможетеполучитьошибкуприкомпиляции.

Во многих языках программирования это называетсяпеременной. Но у связыванияпеременныхвRustестьпаратрюковврукаве.Влевойчастивыраженияlet располагаетсянепростоимяпеременной,а"шаблон".Этозначит,чтомыможемделатьвещивродеэтой:

Послезавершенияэтоговыраженияx будетединицей,ay  —двойкой.Шаблоныоченьмощны, и о них написана отдельнаяглава. Но на данный момент нам не нужны этивозможности,такчтомыпростобудемпомнитьонихипойдёмдальше.

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

Темнеменее,мыможемуказатьжелаемыйтип.Онследуетпоследвоеточия(: ):

Еслибымыпопросиливаспрочитатьэтовслух,выбысказали«x -этосвязываниетипаint созначениемпять».

Вэтомслучаемыуказали,чтоx унасбудет32-битнымцелымчисломсознаком.ВRustестьидругиецелочисленныетипы.Ихименаначинаютсясi дляцелыхчиселсознакомисuдляцелыхчиселбеззнака.Целыечисламогутиметьразмер8,16,32и64бита.

Вдальнейшихпримерахмыбудемуказыватьтипвкомментариях.Этобудетвыглядетьвоттак:

fnmain(){letx=5;}

let(x,y)=(1,2);

letx:i32=5;

fnmain(){letx=5;//x:i32}

ЯзыкпрограммированияRust

163Связываниеимён

Page 164: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Поумолчанию,связываниенеизменяемо.Этоткоднескомпилируется:

Ивыполучитеошибку:

error:re-assignmentofimmutablevariable`x`x=10;^~~~~~~

Если вы хотите, чтобы связывание было изменяемым, вы можете использоватьмодификаторmut :

Может показаться, что незачем делать связывание неизменяемым по умолчанию. Новспомните,начёмвпервуюочередьфокусируетсяRust: набезопасности.Есливыслучайнозабыли указатьmut иизменилисвязывание,компиляторзаметитэто,исообщитвам,чтовыпопытались изменить не то, что собирались. Если бы по умолчанию связывание былоизменяемым, то в приведённой выше ситуации компилятор не сможет вам помочь. Если вынамереныизменитьзначениепеременной,топростодобавьтеmut .

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

Вернёмся к связыванию. Связывание переменных в Rust имеет ещё одно отличие отдругихязыков:онотребуетинициализациипередиспользованием.

Давайте приступим к рассмотрению вышесказанного. Измените ваш файлsrc/main.rs так,чтобыонвыгляделследующимобразом:

Используйте командуcargo build в командной строке, чтобы собрать проект. Выдолжны получить предупреждение, но программа будет работать и будет выводить строку«Привет,мир!»:

letx=5;x=10;

letmutx=5;//mutx:i32x=10;

fnmain(){letx:i32;

println!("Helloworld!");}

ЯзыкпрограммированияRust

164Связываниеимён

Page 165: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Compilinghello_worldv0.0.1(file:///home/you/projects/hello_world)src/main.rs:2:9:2:10warning:unusedvariable:`x`,#[warn(unused_variable)]onbydefaultsrc/main.rs:2letx:i32;^

Rustпредупредитнасотом,чтомынеиспользуемсвязаннуюпеременную,нооттого,что мы её не используем, не будет никакого вреда, поэтому это не ошибка. Однако, всёизменится, еслимыпопробуемиспользоватьx .Сделаемэто.Изменитевашупрограммутак,чтобыонавыгляделаследующимобразом:

Ипопробуйтесобратьпроект.Выполучитеошибку:

$cargobuildCompilinghello_worldv0.0.1(file:///home/you/projects/hello_world)src/main.rs:4:39:4:40error:useofpossiblyuninitializedvariable:`x`src/main.rs:4println!("xимеетзначение{}",x);^note:inexpansionofformat_args!<stdmacros>:2:23:2:77note:expansionsite<stdmacros>:1:1:3:2note:inexpansionofprintln!src/main.rs:4:5:4:42note:expansionsiteerror:abortingduetopreviouserrorCouldnotcompile`hello_world`.

Rustнепозволитиспользоватьнеинициализированнуюпеременную.Далее,поговоримо{} ,которыемыдобавиливprintln! .

Если вы добавите две фигурные скобки ({} , иногда называемые «усами»...) в вашупечатаемуюстроку,Rust истолкует это какпросьбу вставкинекоторого значения.Строковаяинтерполяция —этотерминвинформатике,которыйобозначает«вставитьпосредистроки».Мыдобавилизапятую,изатемx ,чтобыуказать,чтомыхотимвставитьx встроку.Запятаяиспользуется для разделения параметров, если в функцию или макрос передаётся большеодногопараметра.

При вставке переменной в строку, Rust проверит её тип и попытается отобразитьосмысленное значение. Если вы хотите указать формат более детально, то можетеознакомиться сдоступнымиспособамиформатированиястрок(англ.).Наданныймоментмыпростоиспользуемспособпоумолчанию:печататьцелыечисланеоченьсложно.

fnmain(){letx:i32;

println!("xимеетзначение{}",x);}

ЯзыкпрограммированияRust

165Связываниеимён

Page 166: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ФункцииКаждаяпрограмманаRustимеетпокрайнеймереоднуфункцию —main :

Это простейшее объявление функции. Как мы упоминали ранее, ключевое словоfnобъявляетфункцию.Занимследуетеёимя,пустыекруглыескобки(посколькуэтафункциянепринимаетаргументов),азатемтелофункции,заключённоевфигурныескобки.Вотфункцияfoo :

Итак,чтонасчётаргументов,принимаемыхфункцией?Вотфункция,печатающаячисло:

Вотполнаяпрограмма,использующаяфункциюprint_number :

Как видите, аргументы функций похожи на операторыlet : вы можете объявить типаргументапоследвоеточия.

Вотполнаяпрограмма,котораяскладываетдвачислаипечатаетих:

Аргументыразделяютсязапятой —ипривызовефункции,иприеёобъявлении.

В отличие отlet , выдолжны объявлять типы аргументов функции. Этот код нескомпилируется:

fnmain(){}

fnfoo(){}

fnprint_number(x:i32){println!("xравен:{}",x);}

fnmain(){print_number(5);}

fnprint_number(x:i32){println!("xравен:{}",x);}

fnmain(){print_sum(5,6);}

fnprint_sum(x:i32,y:i32){println!("суммачисел:{}",x+y);}

fnprint_sum(x,y){println!("суммачисел:{}",x+y);}

ЯзыкпрограммированияRust

166Функции

Page 167: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Выувидитетакуюошибку:

expectedoneof`!`,`:`,or`@`,found`)`fnprint_number(x,y){

Это осознанное решение при проектировании языка. Бесспорно, вывод типов во всейпрограммевозможен.ОднакодажевHaskellсчитаетсяхорошимстилемявнодокументироватьтипы функций, хотя в этом языке и возможен полный вывод типов. Мы считаем, чтопринудительноеобъявление типовфункцийпри сохранениилокальноговывода типов —этохорошийкомпромисс.

Какнасчётвозвращаемогозначения?Вотфункция,котораяприбавляетодинкцелому:

Функции в Rust возвращают ровно одно значение, тип которого объявляется после«стрелки».«Стрелка»представляетсобойдефис(- ), закоторымследуетзнак«больше»(> ).Заметьте,чтовфункциивышенетточкисзапятой.Еслибымыдобавилиеё:

мыбыполучилиошибку:

error:notallcontrolpathsreturnavaluefnadd_one(x:i32)->i32{x+1;}

help:considerremovingthissemicolon:x+1;^

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

ВыраженияиоператорыRust — в первую очередь язык, ориентированныйна выражения. Есть только два типа

операторов,авсёостальноеявляетсявыражением.

Авчёмжеразница?Выражениевозвращаетзначение,втовремякакоператор-нет.Вотпочему мы получаем здесь «not all control paths return a value»: операторх + 1; невозвращаетзначение.ЕстьдватипаоператороввRust:«операторыобъявления»и«операторывыражения». Все остальное  — выражения. Давайте сначала поговорим об операторахобъявления.

fnadd_one(x:i32)->i32{x+1}

fnadd_one(x:i32)->i32{x+1;}

ЯзыкпрограммированияRust

167Функции

Page 168: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Операторобъявления — это связывание. В некоторых языках связывание переменныхможетбытьзаписанокаквыражение,анетолькокакоператор.Например,вRuby:

x=y=5

Однако,вRustиспользованиеlet длясвязываниянеявляетсявыражением.Следующийкодвызоветошибкукомпиляции:

Здесь компилятор сообщил нам, что ожидал увидеть выражение, ноlet являетсяоператором,аневыражением.

Обратите внимание, что присвоение уже связанной переменной (например:y = 5 )являетсявыражением,ноегозначениенеособеннополезно.Вотличиеотдругихязыков,гдерезультатом присваивания является присваиваемое значение (например,5 из предыдущегопримера),вRustзначениемприсваиванияявляетсяпустойкортеж() .

ВторымтипомоператороввRustявляетсяоператорвыражения.Егоцель-превратитьлюбое выражение в оператор. В практическом плане, грамматика Rust ожидает, что заоператорамибудутидтидругиеоператоры.Этоозначает,чтовыиспользуететочкусзапятойдля отделения выражений друг от друга. Rust выглядит как многие другие языки, которыетребуютиспользовать точку с запятой в конце каждой строки.Выувидите её в концепочтикаждойстрокикоданаRust.

Из-зачегомыговорим«почти»?Выэтоужевиделивэтомпримере:

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

ДосрочныйвозвратизфункцииА что насчёт досрочного возврата из функции? У нас есть для этого ключевое слово

return :

letx=(lety=5);//expectedidentifier,foundkeyword`let`

letmuty=5;

letx=(y=6);//xбудетприсвоенозначение`()`,ане`6`

fnadd_one(x:i32)->i32{x+1}

fnfoo(x:i32)->i32{returnx;

//дальнейшийкоднебудетисполнен!x+1}

ЯзыкпрограммированияRust

168Функции

Page 169: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

return можно написать в последней строке тела функции, но это считается плохимстилем:

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

РасходящиесяфункцииДля функций, которые не возвращают управление («расходящихся»), в Rust есть

специальныйсинтаксис:

panic!  —этомакрос,какиprintln!() ,которыймывстречалиранее.Вотличиеотprintln!() , panic!() вызывает остановку текущего потока исполнения с заданнымсообщением.

Поскольку эта функция вызывает остановку исполнения, она никогда не вернётуправление. Поэтому тип её возвращаемого значения обозначается знаком! и читается как«расходится».Значениерасходящейсяфункцииможетбытьиспользованокакзначениелюбоготипа:

fnfoo(x:i32)->i32{returnx+1;}

fndiverges()->!{panic!("Этафункцияневозвращаетуправление!");}

letx:i32=diverges();letx:String=diverges();

ЯзыкпрограммированияRust

169Функции

Page 170: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ПростыетипыЯзыкRust имеет несколько типов, которые считаются «простыми» («примитивными»).

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

Логическийтип(bool)Rust имеет встроенный логический тип, называемыйbool . Он может принимать два

значения,true иfalse :

Логическиетипычастоиспользуютсявконструкцииif .

Вы можете найти больше информации о логических типах (bool ) вдокументации кстандартнойбиблиотеке(англ.).

Символы(char)Типchar представляет собой одиночное скалярное значение Unicode. Вы можете

создатьchar спомощьюодинарныхкавычек:(' )

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

Выможетенайтибольшеинформацииосимволах(char )вдокументациикстандартнойбиблиотеке(англ.).

ЧисловыетипыRustимеетцелыйрядчисловыхтипов,разделённыхнанесколькокатегорий:знаковыеи

беззнаковые, фиксированного и переменного размера, числа с плавающей точкой и целыечисла.

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

Еслидлячисловоголитераланеуказантип,тоонбудетвыведенпоумолчанию:

letx=true;

lety:bool=false;

letx='x';lettwo_hearts='�';

ЯзыкпрограммированияRust

170Простыетипы

Page 171: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Нижепредставленсписокразличныхчисловыхтипов,соссылкаминаихдокументациювстандартнойбиблиотеке:

i8i16i32i64u8u16u32u64isizeusizef32f64

Давайтепройдёмсяпоихкатегориям.

ЗнаковыеибеззнаковыеЦелыетипыбываютдвухвидов:знаковыеибеззнаковые.Чтобыпонятьразницу,давайте

рассмотрим число с размером в четыре бита. Знаковые четырёхбитные числа, позволяютхранить значения от-8 до+7 . Знаковые числа используют представление «дополнение додвух» (дополнительный код). Беззнаковые четырёхбитные числа, ввиду того что не нужнохранитьотрицательныезначения,позволяютхранитьзначенияот0 до+15 .

Беззнаковыетипыиспользуютu длясвоейкатегории,азнаковыетипыиспользуютi . iозначает«integer».Так,u8 представляетсобойчислобеззнакасразмеромвосемьбит,аi8представляетсобойчислосознакомсразмеромвосемьбит.

ТипыфиксированногоразмераТипысфиксированнымразмеромсоответственноимеютфиксированноеколичествобит

в своём представлении. Допустимыми размерами являются8 , 16 , 32 , 64 . Таким образом,u32 представляетсобойцелоечислобеззнакасразмером32бита,аi64  —целоечислосознакомсразмером64бита.

ТипыпеременногоразмераRustтакжепредоставляеттипы,размеркоторыхзависитотразмерауказателянацелевой

машине. Эти типы имеют «size» в названии в качестве признака размера, и могут бытьзнаковымиилибеззнаковыми.Такимобразом,существуетдватипа:isize иusize .

letx=42;//xимееттипi32

lety=1.0;//yимееттипf64

ЯзыкпрограммированияRust

171Простыетипы

Page 172: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СплавающейточкойВRustтакжеестьдватипасплавающейточкой:f32 иf64 .ОнисоответствуютIEEE-

754числамсплавающейточкойодинарнойидвойнойточностисоответственно.

МассивыВ Rust, как и во многих других языках программирования, есть типы-

последовательности,дляпредставленияпоследовательностейнекихвещей.Самыйпростойизних — этомассив, то есть последовательность элементов одного и того же типа, имеющаяфиксированныйразмер.Массивынеизменяемыпоумолчанию.

Массивы имеют тип[T; N] . О значенииT мы поговорим позже, когда будемрассматриватьобобщённое программирование. N   — это константа времени компиляции,представляющаясобойдлинумассива.

Дляинициализациивсехэлементовмассиваоднимитемжезначениеместьспециальныйсинтаксис.Вэтомпримерекаждыйэлементa будетинициализированзначением0 :

Выможетеполучитьчислоэлементовмассиваa спомощьюметодаa.len() :

Выможетеполучитьопределённыйэлементмассиваспомощьюиндекса:

Индексы нумеруются с нуля, как и в большинстве языков программирования, поэтомумыполучаемпервоеимяспомощьюnames[0] ,авторое —спомощьюnames[1] .ПримервышепечатаетВтороеимя:Brian .Есливыпопытаетесьиспользоватьиндекс,которыйневходитвмассив,выполучитеошибку:придоступекмассивампроисходитпроверкаграницвовремяисполненияпрограммы.Такаяошибочнаяпопыткадоступа —источникмногихпроблемвдругихязыкахсистемногопрограммирования.

Вы можете найти больше информации о массивах (array ) вдокументации кстандартнойбиблиотеке(англ.).

Срезы

leta=[1,2,3];//a:[i32;3]letmutm=[1,2,3];//m:[i32;3]

leta=[0;20];//a:[i32;20]

leta=[1,2,3];

println!("Числоэлементоввa:{}",a.len());

letnames=["Graydon","Brian","Niko"];//names:[&str;3]

println!("Второеимя:{}",names[1]);

ЯзыкпрограммированияRust

172Простыетипы

Page 173: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Срезыимеюттип&[T] .ОзначенииT мыпоговоримпозже,когдабудемрассматриватьобобщённоепрограммирование.

Выможетенайтибольшеинформацииосрезах(slice ) вдокументациикстандартнойбиблиотеке(англ.).

strТипstr вRustявляетсянаиболеепростымтипомстрок.Этобезразмерныйтип,поэтому

сам по себе он не очень полезен, но он становится полезным при использовании ссылки,&str .Покапростоостановимсянаэтом.

Выможетенайтибольшеинформацииостроках (str ) вдокументациик стандартнойбиблиотеке(англ.).

КортежиКортеж —этопоследовательностьфиксированногоразмера.Вродетакой:

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

Как вы можете видеть, тип кортежа выглядит как сам кортеж, но места элементовзанимают типы. Внимательные читатели также отметят, что кортежи гетерогенны: в этомкортеже одновременно хранятся значения типовi32 и&str . В языках системногопрограммированиястрокинемногоболеесложны,чемвдругихязыках.Покавыможетечитать&str каксрезстроки.Мывскореузнаемобэтомбольше.

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

leta=[0,1,2,3,4];letmiddle=&a[1..4];//Срез`a`:толькоэлементы1,2,и3letcomplete=&a[..];//Срез,содержащийвсеэлементымассива`a`

letx=(1,"привет");

letx:(i32,&str)=(1,"привет");

ЯзыкпрограммированияRust

173Простыетипы

Page 174: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Стоитотметитьиещёодинмомент,касающийсядлиныкортежей:кортежнулевойдлины(() ; пустой кортеж) часто называют «единичным значением». Соответственно, тип такогозначения —«единичныйтип».

Доступ к полям кортежа можно получить с помощьюдеконструирующего let. Вотпример:

Помните, мыговорили, что левая часть оператораlet может больше, чем простоприсваиватьимена?Мыимеливвиду то, чтоприведеновыше.Мыможемнаписать слеваотlet шаблон,и,еслионсовпадаетсозначениемсправа,произойдётприсваиваниеимёнсразунескольким значениям. В данном случае,let «деконструирует» или «разбивает» кортеж, иприсваиваетегочаститрёмименам.

Этооченьудобныйшаблонпрограммирования,имыещёнеразувидимего.

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

ИндексациякортежейВытакжеможетеполучитьдоступкполямкортежаспомощьюиндексации:

Какивслучаеиндексациимассивов,индексыначинаютсяснуля,ноздесь,вотличиеотмассивов,используется. ,ане[] .

Вы можете найти больше информации о кортежах (tuple ) вдокументации кстандартнойбиблиотеке(англ.).

ФункцииФункциитожеимеюттип!Этовыглядитследующимобразом:

letmutx=(1,2);//x:(i32,i32)lety=(2,3);//y:(i32,i32)

x=y;

let(x,y,z)=(1,2,3);

println!("xэто{}",x);

(0,);//одноэлементныйкортеж(0);//нольвкруглыхскобках

lettuple=(1,2,3);

letx=tuple.0;lety=tuple.1;letz=tuple.2;

println!("xis{}",x);

ЯзыкпрограммированияRust

174Простыетипы

Page 175: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

В данном примереx   — это «указатель на функцию», которая принимает в качествеаргументаi32 ивозвращаетi32 .

fnfoo(x:i32)->i32{x}

letx:fn(i32)->i32=foo;

ЯзыкпрограммированияRust

175Простыетипы

Page 176: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

КомментарииТеперь, когда у нас есть несколько функций, неплохо бы узнать о комментариях.

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

ВRustестьдвавидакомментариев:строчныекомментариииdoc-комментарии.

Другоеприменениекомментария —этоdoc-комментарий.Doc-комментарийиспользует/// вместо// ,иподдерживаетMarkdown-разметкувнутри:

При написании doc-комментария очень полезно добавлять разделы для аргументов,возвращаемыхзначенийипривестинекоторыепримерыиспользования.Заметьте,чтоздесьмыиспользовалиновыймакрос:assert_eq! .Онсравниваетдвазначенияивызываетpanic! ,если они не равны.Для документации такие примеры очень полезны. Также есть и другоймакрос,assert! ,которыйвызываетpanic! когдазначениеравноfalse .

Вы можете использоватьrustdoc для генерации HTML- документации из этих doc-комментариев,атакжезапускакодаизпримеровкактестов.

//Строчныекомментарии —этовсёчтоугоднопосле'//'идоконцастроки.

letx=5;//этотожестрочныйкомментарий.

//Еслиувасдлинноеобъяснениедлячего-либо,выможетерасположитьстрочные//комментарииодинзадругим.Поместитепробелмежду'//'ивашимкомментарием,//таккакэтоболеечитаемо.

///Прибавляемединицукзаданномучислу.//////#Examples//////```///letfive=5;//////assert_eq!(6,add_one(5));///```fnadd_one(x:i32)->i32{x+1}

ЯзыкпрограммированияRust

176Комментарии

Page 177: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Конструкция`if`if в Rust не сильно сложен и больше похож наif в динамически типизированных

языках,чемнаболеетрадиционныйизсистемных.Давайтепоговоримонём,чтобывыпонялинекоторыеегонюансы.

if является одной из форм более общего понятия, именуемоговетвлением. Этоназвание произошло от ветвей деревьев: конечный результат зависит от того, какой изнесколькихвариантовбудетвыбран.

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

При изменении значенияx на какое-либо другое, эта строчка не будет выведена наэкран.Еслиподробнее,токогдаусловиебудетиметьзначениеtrue ,следующийпосленегоблоккодавыполнится.Впротивномслучае —нет.

Бываетнужночто-то выполнить, если условиене выполнится (выражение будетиметьзначениеfalse).Втакомслучаеможноиспользоватьelse :

Когданеобходимобольшеодноговыбора,можноиспользоватьelseif :

Всёэтодовольнопрозаично.Однако,вытакжеможетесделатьтакуюштуку:

letx=5;

ifx==5{println!("xравняетсяпяти!");}

letx=5;

ifx==5{println!("xравняетсяпяти!");}else{println!("xэтонепять:(");}

letx=5;

ifx==5{println!("xравняетсяпяти!");}elseifx==6{println!("xэтошесть!");}else{println!("xэтонипять,нишесть:(");}

ЯзыкпрограммированияRust

177Конструкция`if`

Page 178: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Которуюмыможем(идолжны)записатьпримерноследующимобразом:

Это работает, потому чтоif является выражением. Его значением является значениепоследнего выражения из выбранной ветви.if безelse всегда возвращает() в качествезначения.

letx=5;

lety=ifx==5{10}else{15};//y:i32

letx=5;

lety=ifx==5{10}else{15};//y:i32

ЯзыкпрограммированияRust

178Конструкция`if`

Page 179: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЦиклыНаданныймоментвRustестьтриспособаорганизоватьциклическоеисполнениекода.

Этоloop ,while иfor .Укаждогоподходасвоёприменения.

ЦиклыloopБесконечный цикл (loop )  — простейшая форма цикла в Rust. С помощью этого

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

ЦиклыwhileЦиклwhile  —этоещёодинвидконструкциициклавRust.Выглядитонтак:

Он применяется, если неизвестно, сколько раз нужно выполнить тело цикла, чтобыполучитьрезультат.Прикаждойитерациициклапроверяетсяусловие,иеслионоистинно,тозапускаетсяследующаяитерация.Иначециклwhile завершается.

Есливамнуженбесконечныйцикл,томожетесделатьусловиевсегдаистинным:

Однако,длятакогослучаявRustимеетсяключевоесловоloop :

В Rust анализатор потока управления обрабатывает конструкциюloop иначе, чемwhiletrue , хотя для нас это одно и тоже.На данном этапе изученияRust нам не важнознать в чем именно различие между этими конструкциями, но если вы хотите сделатьбесконечный цикл, то используйте конструкциюloop . Компилятор сможет транслироватьвашкодвболееэффективныйибезопасныймашинныйкод.

loop{println!("Зациклились!");}

letmutx=5;//mutx:i32letmutdone=false;//mutdone:bool

while!done{x+=x-3;

println!("{}",x);

ifx%5==0{done=true;}}

whiletrue{

loop{

ЯзыкпрограммированияRust

179Циклы

Page 180: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЦиклыforЦиклfor нужендляповторенияблокакодаопределённоеколичествораз.Циклыfor в

Rust работают немного иначе, чем в других языках программирования. Например в Си-подобномязыкециклfor выглядиттак:

for(x=0;x<10;x++){printf("%d\n",x);}

Однако,этоткодвRustбудетвыглядетьследующимобразом:

Можнопредставитьциклболееабстрактно:

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

В нашем примере,0..10   — это выражение, которое задаёт начальное и конечноезначение,ивозвращаетитератор.Обратитевнимание,чтоконечноезначениеневключаетсявнего.Внашемпримеребудутнапечатанычислаот0 до9 ,нонебудетнапечатано10 .

В Rust намеренно нет циклаfor в стиле C. Управлять каждым элементом циклавручнуюсложно,иэтоможетприводитькошибкамдажеуопытныхпрограммистовнаC.

ПеречислениеЕсли вы хотите отслеживать число прошедших итераций, используйте функцию

.enumerate() .

Синтервалами

Выводит:

forxin0..10{println!("{}",x);//x:i32}

forпеременнаяinвыражение{тело_цикла}

for(i,j)in(5..10).enumerate(){println!("i={}иj={}",i,j);}

ЯзыкпрограммированияRust

180Циклы

Page 181: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

i=0иj=5i=1иj=6i=2иj=7i=3иj=8i=4иj=9

Незабудьтенаписатьскобкивокругинтервала.

Ситераторами

Outputs:

0:привет1:мир2:hello3:world

РаннеепрерываниециклаДавайтеещёразпосмотримнациклwhile :

В этом примере в условии для выхода из цикла используется изменяемое имяdoneлогического типа. В Rust имеются два ключевых слова, которые помогают работать ситерациямицикла:break иcontinue .

Мыможемпереписатьциклспомощьюbreak ,чтобыизбавитьсяотпеременнойdone :

for(linenumber,line)inlines.enumerate(){println!("{}:{}",linenumber,line);}

letmutx=5;letmutdone=false;

while!done{x+=x-3;

println!("{}",x);

ifx%5==0{done=true;}}

letmutx=5;

loop{x+=x-3;

println!("{}",x);

ifx%5==0{break;}}

ЯзыкпрограммированияRust

181Циклы

Page 182: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Теперь мы используем бесконечный циклloop иbreak для выхода из цикла.Использованиеявногоreturn такжеостановитвыполнениецикла.

continue похож наbreak , но вместо выхода из цикла переходит к следующейитерации.Следующийпримеротобразиттольконечётныечисла:

МеткицикловКогда у вас много вложенных циклов, вы можете захотеть указать, к какому именно

циклуотноситсяbreak илиcontinue .Какивомногихдругихязыках,поумолчаниюэтиоператорыбудутотноситьсяксамомувнутреннемуциклу.Есливыхотитепрерватьвнешнийцикл,выможетеиспользоватьметку.Так,этоткодбудетпечататьнаэкранетолькокогдаиx ,иy нечётны:

forxin0..10{ifx%2==0{continue;}

println!("{}",x);}

'outer:forxin0..10{'inner:foryin0..10{ifx%2==0{continue'outer;}//продолжаетциклпоxify%2==0{continue'inner;}//продолжаетциклпоyprintln!("x:{},y:{}",x,y);}}

ЯзыкпрограммированияRust

182Циклы

Page 183: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ВладениеЭтаглаваявляетсяоднойизтрёх,описывающихсистемувладенияресурсамиRust.Эта

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

владение,еёвычитаетесейчасзаимствование,исвязаннаяснимвозможность«ссылки»времяжизни,расширениепонятиязаимствования

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

МетаПреждечемперейтикподробностям,отметимдваважныхмоментавсистемевладения.

Rust сфокусированнабезопасностии скорости.Этодостигается за счёт«абстракций снулевой стоимостью» (zero-cost abstractions). Это значит, что в Rust стоимость абстракцийдолжна быть настолько малой, насколько это возможно без ущерба для работоспособности.Система владения ресурсами — это яркий пример абстракции с нулевой стоимостью. Весьанализ,окотороммыбудемговоритьвэтомруководстве,выполняетсявовремякомпиляции.Вовремяисполнениявынеплатитезакакую-либоизвозможностейничего.

Тем не менее, эта система всё же имеет определённую стоимость: кривая обучения.Многие новые пользователиRust «борются с проверкой заимствования» — компиляторRustотказывается компилировать программу, которая по мнению автора является абсолютноправильной.Эточастопроисходитпотому,чтомысленноепредставлениепрограммистаотом,как должно работать владение, не совпадает с реальными правилами, которыми оперируетRust. Вы, наверное, поначалу также будете испытывать подобные трудности. Однакосуществуетихорошаяновость:болееопытныеразработчикинаRustговорят,чточембольшеониработаютсправиламисистемывладения,темменьшеониборютсяскомпилятором.

Имеяэтоввиду,давайтеперейдёмкизучениюсистемывладения.

ВладениеСвязанныеименаимеютоднуособенностьвRust:они«владеют»тем,счемонисвязаны.

Этоозначает, что, когдаимя выходит запределыобласти видимости, ресурс, с которымоносвязано,будетосвобождён.Например:

ЯзыкпрограммированияRust

183Владение

Page 184: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Когдаv входитвобластьвидимости,создаётсяновыйVec<T> .Вданномслучаевектортакже выделяет изкучи пространство для трёх элементов. Когдаv выходит из областивидимости в концеfoo() , Rust очищает все, связанное с вектором, даже динамическивыделеннуюпамять.Этопроисходитдетерминировано,вконцеобластивидимости.

СемантикаперемещенияХотя тут есть некоторые тонкости: Rust гарантирует, что существуетровно одно

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

Но,еслипослеэтогомыпопытаемсяиспользоватьv ,тополучимошибку:

Ошибкавыглядитследующимобразом:

error:useofmovedvalue:`v`println!("v[0]={}",v[0]);^

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

Та же самая ошибка: «use of moved value» («используется перемещённое значение»).Когдамыпередаёмправовладениякуда-тоещё,мыкакбыговорим,чтомы«перемещаем»то,на что ссылаемся. При этом не нужно указывать какую-либо специальную аннотацию, Rustделаетэтопоумолчанию.

fnfoo(){letv=vec![1,2,3];}

letv=vec![1,2,3];

letv2=v;

letv=vec![1,2,3];

letv2=v;

println!("v[0]={}",v[0]);

fntake(v:Vec<i32>){//чтобудетздесьнеоченьважно}

letv=vec![1,2,3];

take(v);

println!("v[0]={}",v[0]);

ЯзыкпрограммированияRust

184Владение

Page 185: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ПодробностиПричина, по которой мы не можем использовать значение после того, как мы его

переместили,неочевидна,нооченьважна.Когдамыпишемкодвродеэтого:

Перваястрокасоздаётнекоторыеданныедлявекторавстеке,v .Данныесамоговектора,однако,сохраняютсявкуче,ипоэтомустековыеданныесодержатуказательнаданныевкуче.Когда мы перемещаемv вv2 , то создаётся копия стековых данных дляv2 . Что будетозначать,чтодвауказателяссылаютсянарасположенныйвкучевектор.Такоеповедениемоглобыбытьпроблемой:ононарушалобыгарантиибезопасностиRust,привносягонкиподанным.ПоэтомуRustзапрещаетиспользованиеv послетого,какмывыполнилиегоперемещение.

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

Типы,реализующиетипажCopyМы установили, что как только владение передаётся другому имени, вы больше не

можете использовать исходное. Тем не менее, существуеттипаж, который изменяет такоеповедение,ионназываетсяCopy .Мыещёнеобсуждалитипажи,нопокавыможетедуматьоних как об аннотациях к конкретному типу, которые придают дополнительное поведение.Например:

Вэтомпримереv связанстипомi32 .ЭтоттипреализуеттипажCopy .Этоозначает,что когда мы присваиваем значениеv имениv2 , будет создана копия данных, как и приперемещении.Но, в отличиеотперемещения,мыможемиспользоватьv в дальнейшем.Этопроисходит потому, что вi32 нет указателей на данные в каком-либо другом месте. Притакомкопированиисоздаётсяполнаякопия.

Мыбудемобсуждать,каксделатьсвоисобственныетипы,реализующиетипажCopy вразделеТипажи.

Больше,чемвладениеКонечно, если бы нам нужно было вернуть владение обратно из функции, то мы бы

написали:

letv=vec![1,2,3];

letv2=v;

letv=1;

letv2=v;

println!("v={}",v);

ЯзыкпрограммированияRust

185Владение

Page 186: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этосильноутомляет.Функциястановитсятемхуже,чембольшеправвладенияонахочетзабратьсебе:

Брр! Возвращаемый тип, строка возврата, и вызов функции получается намногоболеесложным.

Ксчастью,Rustпредлагаеттакуювозможность,какзаимствование,котораяпомогаетнамрешитьэтупроблему.Этотемаследующегораздела!

fnfoo(v:Vec<i32>)->Vec<i32>{//делаемчто-либосv

//возвращаемвладениеv}

fnfoo(v1:Vec<i32>,v2:Vec<i32>)->(Vec<i32>,Vec<i32>,i32){//делаемчто-нибудьсv1иv2

//возвращаемвладениеирезультатнашейфункции(v1,v2,42)}

letv1=vec![1,2,3];letv2=vec![1,2,3];

let(v1,v2,answer)=foo(v1,v2);

ЯзыкпрограммированияRust

186Владение

Page 187: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СсылкиизаимствованиеЭтаглаваявляетсяоднойизтрёх,описывающихсистемувладенияресурсамиRust.Эта

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

владение,ключеваяконцепциязаимствование,еёвычитаетесейчасвремяжизни,расширениепонятиязаимствования

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

МетаПреждечемперейтикподробностям,отметимдваважныхмоментавсистемевладения.

Rust сфокусированнабезопасностии скорости.Этодостигается за счёт«абстракций снулевой стоимостью» (zero-cost abstractions). Это значит, что в Rust стоимость абстракцийдолжна быть настолько малой, насколько это возможно без ущерба для работоспособности.Система владения ресурсами — это яркий пример абстракции с нулевой стоимостью. Весьанализ,окотороммыбудемговоритьвэтомруководстве,выполняетсявовремякомпиляции.Вовремяисполнениявынеплатитезакакую-либоизвозможностейничего.

Тем не менее, эта система всё же имеет определённую стоимость: кривая обучения.Многие новые пользователиRust «борются с проверкой заимствования» — компиляторRustотказывается компилировать программу, которая по мнению автора является абсолютноправильной.Эточастопроисходитпотому,чтомысленноепредставлениепрограммистаотом,как должно работать владение, не совпадает с реальными правилами, которыми оперируетRust. Вы, наверное, поначалу также будете испытывать подобные трудности. Однакосуществуетихорошаяновость:болееопытныеразработчикинаRustговорят,чточембольшеониработаютсправиламисистемывладения,темменьшеониборютсяскомпилятором.

Имеяэтоввиду,давайтеперейдёмкизучениюсистемывладения.

ЗаимствованиеВконцеглавыВладениеунасбылаубогаяфункция,котораявыгляделатак:

ЯзыкпрограммированияRust

187Ссылкиизаимствование

Page 188: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Однако, этот код не является идиоматичным с точки зрения Rust, так как он неиспользуетзаимствование.Вотпервыйшаг:

Вместотого,чтобыприниматьVec<i32> вкачествеаргументов,мыбудемприниматьссылки:&Vec<i32> . И вместо передачиv1 иv2 напрямую, мы будем передавать&v1 и&v2 .Мыназываемтип&T «ссылка»,ивместотого,чтобызабиратьвладениересурсом,онаегозаимствует.Имена,которыезаимствуютчто-то,неосвобождаютресурс,когдаонивыходятизобластивидимости.Этоозначает,что,послевызоваfoo() ,мысноваможемиспользоватьнашиисходныеимена.

Ссылки являются неизменяемыми, как и имена. Это означает, что внутриfoo()векторынемогутбытьизменены:

выдаётошибку:

error:cannotborrowimmutableborrowedcontent`*v`asmutablev.push(5);^

fnfoo(v1:Vec<i32>,v2:Vec<i32>)->(Vec<i32>,Vec<i32>,i32){//dostuffwithv1andv2

//handbackownership,andtheresultofourfunction(v1,v2,42)}

letv1=vec![1,2,3];letv2=vec![1,2,3];

let(v1,v2,answer)=foo(v1,v2);

fnfoo(v1:&Vec<i32>,v2:&Vec<i32>)->i32{//dostuffwithv1andv2

//returntheanswer42}

letv1=vec![1,2,3];letv2=vec![1,2,3];

letanswer=foo(&v1,&v2);

//wecanusev1andv2here!

fnfoo(v:&Vec<i32>){v.push(5);}

letv=vec![];

foo(&v);

ЯзыкпрограммированияRust

188Ссылкиизаимствование

Page 189: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Добавление значения изменяет вектор, и поэтому компилятор не позволил нам этосделать.

Ссылки&mutВотвторойвидссылок:&mutT .Это«изменяемаяссылка»,котораяпозволяетизменять

ресурс,которыйвызаимствуете.Например:

Этот код напечатает6 . Мы создалиy , изменяемую ссылку наx , а затем добавилиединицу к значению, на которое указываетy . Следует отметить, чтоx также должно бытьпомеченокакmut .Еслибыэтогонебыло,томынемоглибыполучитьизменяемуюссылкунеизменяемогозначения.

Во всем остальном изменяемые ссылки (&mut ) такие же, как и неизменяемые (& ).Однако, существует большая разница между этими двумя концепциями, и тем, как онивзаимодействуют. Вы можете сказать, что в приведённом выше примере есть что-топодозрительное, потому что нам зачем-то понадобилась дополнительная область видимости,созданнаяспомощью{ и} .Еслимыуберемэтискобки,тополучимошибку:

error:cannotborrow`x`asimmutablebecauseitisalsoborrowedasmutableprintln!("{}",x);^note:previousborrowof`x`occurshere;themutableborrowpreventssubsequentmoves,borrows,ormodificationof`x`untiltheborrowendslety=&mutx;^note:previousborrowendsherefnmain(){

}^

Оказывается,естьопределённыеправиласозданияссылок.

ПравилаВотправилазаимствованиявRust.

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

однаилиболеенеизменяемыхссылок(&T )наресурс;

letmutx=5;{lety=&mutx;*y+=1;}println!("{}",x);

ЯзыкпрограммированияRust

189Ссылкиизаимствование

Page 190: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ровнооднаизменяемаяссылка(&mutT )наресурс.

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

Состояние «гонки данных» возникает, когда два или более указателейосуществляютдоступкоднойитойжеобластипамятиодновременно,покрайнеймереодинизнихпроизводитзапись,иоперациинесинхронизированы.

Чтокасаетсянеизменяемыхссылок,товыможетеиметьихстолько,сколькохотите,таккакниоднаизнихнепроизводитзапись.Еслижевыпроизводитезапись,ивамнужнодваилибольше указателей на одну и туже область памяти, то выможете иметь только одну&mutодновременно. Так Rust предотвращает возникновение состояния гонки данных во времякомпиляции:мыполучимошибкукомпиляции,еслинарушимэтиправила.

Имеяэтоввиду,давайтерассмотримнашпримерещераз.

Осмысливаемобластивидимости(Thinkinginscopes)Воткод:

Этоткодвыдаетнамтакуюошибку:

error:cannotborrow`x`asimmutablebecauseitisalsoborrowedasmutableprintln!("{}",x);^

Это потому, что мы нарушили правила: у нас есть изменяемая ссылка&mut T ,указывающая наx , и поэтому мы не можем создать какую-либо&T . Одно из двух.Примечаниеподсказываеткакследуетрассматриватьэтупроблему:

note:previousborrowendsherefnmain(){

}^

Другими словами, изменяемая ссылка сохраняется до конца нашего примера. А мыхотим, чтобы изменяемое заимствование заканчивалосьдо того, как мы пытаемся вызватьprintln! исоздатьнеизменяемоезаимствование.ВRustзаимствованиепривязанокобластивидимости, в которой оно является действительным. И эти области видимости выглядятследующимобразом:

letmutx=5;lety=&mutx;

*y+=1;

println!("{}",x);

ЯзыкпрограммированияRust

190Ссылкиизаимствование

Page 191: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Конфликтобластейвидимости:мынеможемсоздать&x дотехпор,покаy находитсявобластивидимости.

Поэтому,когдамыдобавляемфигурныескобки:

Никаких проблем нет. Наша изменяемая ссылка выходит из области видимости досоздания неизменяемой. Но область видимости является ключом к определению того, какдолгодлитсязаимствование.

Проблемы,которыепредотвращаетзаимствованиеПочему нужны эти ограничивающие правила? Ну, как мы уже отметили, эти правила

предотвращают гонки данных. Какие виды проблем могут привести к состоянию гонкиданных?Вотнекоторыеизних.

НедействительныйитераторОдним из примеров является «недействительный итератор». Такое может произойти,

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

Этоткодпечатаетчислаотодногодотрёх.Когдамыобходимвектор,мыполучаемлишьссылкинаэлементы.Исамv заимствованкакнеизменяемый,чтоозначает,чтомынеможемизменитьеговпроцессеобхода:

letmutx=5;

lety=&mutx;//-+заимствованиеxчерез&mutначинаетсяздесь//|*y+=1;//|//|println!("{}",x);//-+-пытаемсяпозаимствоватьxздесь//-+заимствованиеxчерез&mutзаканчиваетсяздесь

letmutx=5;

{lety=&mutx;//-+заимствованиечерез&mutначинаетсяздесь*y+=1;//|}//-+...изаканчиваетсяздесь

println!("{}",x);//<-пытаемсяпозаимствоватьxздесь

letmutv=vec![1,2,3];

foriin&v{println!("{}",i);}

ЯзыкпрограммированияRust

191Ссылкиизаимствование

Page 192: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вотошибка:

error:cannotborrow`v`asmutablebecauseitisalsoborrowedasimmutablev.push(34);^note:previousborrowof`v`occurshere;theimmutableborrowpreventssubsequentmovesormutableborrowsof`v`untiltheborrowendsforiin&v{^note:previousborrowendshereforiin&v{println!(“{}”,i);v.push(34);}^

Мынеможемизменитьv ,потомучтоонужезаимствованвцикле.

Использованиепослеосвобождения(useafterfree)Ссылки должны жить так же долго, как и ресурс, на который они ссылаются. Rust

проверяетобластивидимостивашихссылок,чтобыудостовериться,чтоэтоправда.

ЕслиRustнебудетпроверятьэтосвойство,томыможемслучайноиспользоватьссылку,котораябудетнедействительна.Например:

Мыполучимследующуюошибку:

letmutv=vec![1,2,3];

foriin&v{println!("{}",i);v.push(34);}

lety:&i32;{letx=5;y=&x;}

println!("{}",y);

ЯзыкпрограммированияRust

192Ссылкиизаимствование

Page 193: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

error:`x`doesnotlivelongenoughy=&x;^note:referencemustbevalidfortheblocksuffixfollowingstatement0at2:16...lety:&i32;{letx=5;y=&x;}

note:...butborrowedvalueisonlyvalidfortheblocksuffixfollowingstatement0at4:18letx=5;y=&x;}

Другимисловами,y действителентолькодлятойобластивидимости,гдесуществуетx .Как толькоx выходит из области видимости, ссылка на него становится недействительной.Такимобразом,ошибкасообщает,чтозаимствование«неживетдостаточнодолго»(«doesnotlive long enough»), потому что оно не является действительным столько времени, сколькотребуется.

Такаяжепроблема возникает, когда ссылка объявленаперед значением,накотороеонассылается. Это происходит потому что ресурсы в одном блоке освобождаются в порядке,противоположномпорядкуихобъявления:

Мыполучимтакуюошибку:

error:`x`doesnotlivelongenoughy=&x;^note:referencemustbevalidfortheblocksuffixfollowingstatement0at2:16...lety:&i32;letx=5;y=&x;

println!("{}",y);}

note:...butborrowedvalueisonlyvalidfortheblocksuffixfollowingstatement1at3:14letx=5;y=&x;

println!("{}",y);}

lety:&i32;letx=5;y=&x;

println!("{}",y);

ЯзыкпрограммированияRust

193Ссылкиизаимствование

Page 194: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Впримеревышеy объявленапередx ,т.е.живётдольшеx ,аэтозапрещено.

ЯзыкпрограммированияRust

194Ссылкиизаимствование

Page 195: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ВремяжизниЭтаглаваявляетсяоднойизтрёх,описывающихсистемувладенияресурсамиRust.Эта

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

владение,ключеваяконцепциязаимствование,исвязаннаяснимвозможность«ссылки»времяжизни,еёвычитаетесейчас

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

МетаПреждечемперейтикподробностям,отметимдваважныхмоментавсистемевладения.

Rust сфокусированнабезопасностии скорости.Этодостигается за счёт«абстракций снулевой стоимостью» (zero-cost abstractions). Это значит, что в Rust стоимость абстракцийдолжна быть настолько малой, насколько это возможно без ущерба для работоспособности.Система владения ресурсами — это яркий пример абстракции с нулевой стоимостью. Весьанализ,окотороммыбудемговоритьвэтомруководстве,выполняетсявовремякомпиляции.Вовремяисполнениявынеплатитезакакую-либоизвозможностейничего.

Тем не менее, эта система всё же имеет определённую стоимость: кривая обучения.Многие пользователи Rust занимаются тем, что мы зовём «борьбой с проверкойзаимствования»  — компилятор Rust отказывается компилировать программу, которая помнениюавтораявляетсяабсолютноправильной.Эточастопроисходитпотому,чтомысленноепредставлениепрограммистаотом,какдолжноработатьвладение,несовпадаетсреальнымиправилами, которыми оперирует Rust. Вы, наверное, поначалу также будете испытыватьподобныетрудности.Однакосуществуетихорошаяновость:болееопытныеразработчикинаRust говорят,чточембольшеониработаютсправиламисистемывладения,темменьшеониборютсяскомпилятором.

Имеяэтоввиду,давайтеперейдёмкизучениюсистемывладения.

ВремяжизниОдалживаниессылкинаресурс,которымкто-товладеет,можетбытьдовольносложным.

Например,представьтесебеследующуюпоследовательностьопераций:

ЯзыкпрограммированияRust

195Времяжизни

Page 196: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Ой-ой! Ваша ссылка указывает на недопустимый ресурс. Это называется «висячийуказатель»или«использованиепослеосвобождения»,когдаресурсомявляетсяпамять.

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

Когда унас естьфункция, котораяпринимает ссылку в качестве аргумента,мыможемявноилинеявноуказатьвремяжизниссылки:

Читается'a как «время жизни a». Технически, все ссылки имеют некоторое времяжизни,связанноесними,нокомпиляторпозволяетопускатьеговобщихслучаях.Преждечеммыперейдемкэтому,давайтеразберемпримерниже,сявнымуказаниемвременижизни:

Эта часть объявляет параметры времени жизни. Она говорит, чтоbar имеет одинпараметрвременижизни,'a .Еслибывкачествепараметровфункцииунасбылодвессылки,тоэтовыгляделобытак:

Затемвспискепараметровфункциимыиспользуемзаданныепараметрывременижизни:

Еслибымыхотели&mut ссылку,тосделалибытак:

Есливысравните&muti32 с&'amuti32 ,тоувидите,чтоониотличаютсятолькоопределением времени жизни'a , написанным между& иmuti32 . &muti32 читаетсякак «изменяемая ссылка на i32», а&'a mut i32   — как «изменяемая ссылка на i32 современемжизни'a».

Внутриstruct 'ов

//неявноfnfoo(x:&i32){}

//явноfnbar<'a>(x:&'ai32){}

fnbar<'a>(...)

fnbar<'a,'b>(...)

...(x:&'ai32)

...(x:&'amuti32)

ЯзыкпрограммированияRust

196Времяжизни

Page 197: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вытакжедолжныбудетеявноуказатьвремяжизниприработесоstruct 'ми:

Как вы можете заметить, структуры также могут иметь время жизни. Так же как ифункции,

объявляетвремяжизнии

использует его. Почему же мы должны определять время жизни здесь? Мы должныубедиться, что ссылка наFoo не может жить дольше, чем ссылка наi32 , содержащаяся внем.

БлокиimplДавайтереализуемметоддляFoo :

Каквыможетевидеть,намнужнообъявитьвремяжизнидляFoo встрокесimpl .Мыповторяем'a дважды,каквфункциях:impl<'a> определяетвремяжизни'a , иFoo<'a>используетего.

Нескольковремёнжизни(Multiplelifetimes)Если выимеете несколько ссылок, выможете использовать однои тоже времяжизни

несколькораз:

structFoo<'a>{x:&'ai32,}

fnmain(){lety=&5;//тожесамое,чтои`let_y=5;lety=&_y;`letf=Foo{x:y};

println!("{}",f.x);}

structFoo<'a>{

x:&'ai32,

structFoo<'a>{x:&'ai32,}

impl<'a>Foo<'a>{fnx(&self)->&'ai32{self.x}}

fnmain(){lety=&5;//тожесамое,чтои`let_y=5;lety=&_y;`letf=Foo{x:y};

println!("xis:{}",f.x());}

ЯзыкпрограммированияRust

197Времяжизни

Page 198: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этоткодговорит,чтоx иy находятсяводнойобластивидимостидругсдругом,ичтовозвращаемоезначениеживонапротяжениитойжеобластивидимости.Есливыхотите,чтобыx иy имелиразныевременажизни,выдолжныиспользоватьпараметрынесколькихвремёнжизни:

Вэтомпримереx иy имеютразличныеобластивидимости,новозвращаемоезначениеимееттожевремяжизни,чтоиx .

Осмысливаемобластивидимости(Thinkinginscopes)Одинизспособовпонять,чтожетакоевремяжизни —этовизуализироватьобласть, в

которойссылкаявляетсядействительной.Например:

ДобавимнашуструктуруFoo :

Нашаf живет в областивидимостиy ,поэтомувсеработает.Чтожепроизойдёт,еслиэтобудетнетак?Этоткоднебудетработать:

fnx_or_y<'a>(x:&'astr,y:&'astr)->&'astr{

fnx_or_y<'a,'b>(x:&'astr,y:&'bstr)->&'astr{

fnmain(){lety=&5;//-+yвходитвобластьвидимости//|//что-то//|//|}//-+yвыходитизобластивидимости

structFoo<'a>{x:&'ai32,}

fnmain(){lety=&5;//-+yвходитвобластьвидимостиletf=Foo{x:y};//-+fвходитвобластьвидимости//что-то//|//|}//-+fиyвыходятизобластивидимости

ЯзыкпрограммированияRust

198Времяжизни

Page 199: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Уф! Как вы можете видеть здесь, области видимостиf иy меньше, чем областьвидимостиx .Нокогдамывыполняемx=&f.x ,мыприсваиваемx ссылкуначто-то,чтовот-вотвыйдетизобластивидимости.

Присвоениеименивременижизни —этоспособзадатьимяобластивидимости.Чтобыдуматьочём-то,нужноиметьназваниедляэтого.

'staticВремяжизни с именем «static» — особенное.Оно обозначает, что что-то имеет время

жизни,равноевременижизнивсейпрограммы.БольшинствопрограммистовнаRustвпервыесталкиваютсяс'static ,когдаимеютделосостроками:

Строковые литералы имеют тип&'static str , потому что ссылка всегдадействительна:строкирасполагаютсявсегментеданныхконечногодвоичногофайла.Другойпример —глобальныепеременные:

Вэтомпримереi32 добавляетсявсегментданныхдвоичногофайла,аx ссылаетсянанего.

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

уровня не выводятся, чтобы можно было рассуждать о типах на основании одних лишьсигнатур. Из соображений удобства, введён ограниченный механизм вывода типов сигнатурфункций, называемый «опускание времени жизни» («lifetime elision»). Он выводит типы наоснованиитолькоэлементовсигнатуры —телофункцииприэтомнеучитывается.Приэтом

structFoo<'a>{x:&'ai32,}

fnmain(){letx;//-+xвходитвобластьвидимости//|{//|lety=&5;//---+yвходитвобластьвидимостиletf=Foo{x:y};//---+fвходитвобластьвидимостиx=&f.x;//||здесьошибка}//---+fиyвыходятизобластивидимости//|println!("{}",x);//|}//-+xвыходитизобластивидимости

letx:&'staticstr="Привет,мир.";

staticFOO:i32=5;letx:&'statici32=&FOO;

ЯзыкпрограммированияRust

199Времяжизни

Page 200: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

его назначение  — это вывести лишь параметры времени жизни аргументов. Для этого онреализует три простых правила. Таким образом, опускание времени жизни упрощаетнаписаниесигнатур,одновременнонескрываяреальныетипыаргументов.

Когдаречьидетонеявномвременижизни,мыиспользуемтерминывходноевремяжизни(input lifetime) ивыходное время жизни (output lifetime). Входное время жизни связано спередаваемыми в функцию параметрами, авыходное времяжизни связано с возвращаемымфункциейзначением.Например,этафункцияимеетвходноевремяжизни:

Аэтаимеетвыходноевремяжизни:

Этажеимееткаквходное,такивыходноевремяжизни:

Нижепредставленытриправила:

Каждое неявное время жизни в аргументах функции становится отдельнымвременемжизни.

Если есть ровно одно входное времяжизни, явное илинеявное, то это времяжизниназначаетсявсемнеявнымвыходнымвременамжизни.

Если есть несколько входных времён жизни, но одно из них это&self или&mutself ,товсемнеявнымвыходнымвременамжизниназначаетсявремяжизниself .

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

ПримерыВот некоторые примеры функций, представленные в двух видах: с явно и неявно

заданнымвременемжизни:

fnfoo<'a>(bar:&'astr)

fnfoo<'a>()->&'astr

fnfoo<'a>(bar:&'astr)->&'astr

ЯзыкпрограммированияRust

200Времяжизни

Page 201: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

fnprint(s:&str);//неявноfnprint<'a>(s:&'astr);//явно

fndebug(lvl:u32,s:&str);//неявноfndebug<'a>(lvl:u32,s:&'astr);//явно

//Впредыдущемпримередля`lvl`нетребуетсяуказыватьвремяжизни,потомучто//этонессылка(`&`).Толькоэлементы,связанныесссылками(например,такие//какструктура,содержащаяссылку)требуютуказаниявременижизни.

fnsubstr(s:&str,until:u32)->&str;//неявноfnsubstr<'a>(s:&'astr,until:u32)->&'astr;//явно

fnget_str()->&str;//НЕКОРРЕКТНО,нетвходныхпараметров

fnfrob(s:&str,t:&str)->&str;//НЕКОРРЕКТНО,двавходныхпараметраfnfrob<'a,'b>(s:&'astr,t:&'bstr)->&str;//Развёрнуто:Выходноевремяжизнинеясно

fnget_mut(&mutself)->&mutT;//неявноfnget_mut<'a>(&'amutself)->&'amutT;//явно

fnargs<T:ToCStr>(&mutself,args:&[T])->&mutCommand//неявноfnargs<'a,'b,T:ToCStr>(&'amutself,args:&'b[T])->&'amutCommand//явно

fnnew(buf:&mut[u8])->BufWriter;//неявноfnnew<'a>(buf:&'amut[u8])->BufWriter<'a>//явно

ЯзыкпрограммированияRust

201Времяжизни

Page 202: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Изменяемость(mutability)Изменяемость, то есть возможность изменить что-то, работает вRust несколько иначе,

чемвдругихязыках.Во-первых,поумолчаниюсвязанныеименанеизменяемы:

Изменяемостьможнодобавитьспомощьюключевогословаmut :

Это изменяемоесвязанноеимя. Когда связанное имя изменяемо, это означает, что мыможем поменять связанное с ним значение. В примере выше не то, чтобы само значениеxменялось,простоимяx связываетсясдругимзначениемтипаi32 .

Если же вы хотите изменить само связанное значение, вам понадобитсяизменяемаяссылка:

y  — это неизменяемое имя для изменяемой ссылки.Это значит, чтоy нельзя связатьещёсчем-то(y=&mutz ),номожноизменитьто,начтоуказываетсвязаннаяссылка(*y=5 ).Тонкаяразница.

Конечно,выможетеобъявитьиизменяемоеимядляизменяемойссылки:

Теперьy можносвязатьсдругимзначением,исамоэтозначениетожеможноменять.

Стоитотметить,чтоmut  —эточастьшаблона,поэтомуможноделатьтакиевещи:

Внутренняя (interior) и внешняя (exterior)изменяемость

Однако,когдамыговорим,чточто-либо«неизменяемо»вRust,этонеозначает,чтооносовсем не может измениться. Мы говорим о «внешней изменяемости». Для примерарассмотримArc<T> :

letx=5;x=6;//ошибка!

letmutx=5;

x=6;//нетпроблем!

letmutx=5;lety=&mutx;

letmutx=5;letmuty=&mutx;

let(mutx,y)=(5,6);

fnfoo(mutx:i32){

ЯзыкпрограммированияRust

202Изменяемость(mutability)

Page 203: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Когдамывызываемметодclone() ,Arc<T> должнаобновитьсчётчикссылок.Мынеиспользовали модификаторmut , а значитx  — неизменяемое имя.Мы не можем получитьссылку(&mut5 )илисделатьчто-топодобное.Ичтоже?

Для того чтобы понять это, мы должны вернуться назад к основамфилософииRust, ксохранностипамятиимеханизму,гарантирующемуэто,ксистемевладения,и,вчастности,кзаимствованию:

Одновременноувасможетбытьтолькоодиниздвухперечисленныхнижевидовзаимствования,нонеобасразу:

однаилиболеенеизменяемыхссылок(&T )наресурс,ровнооднаизменяемаяссылка(&mutT )наресурс.

Итак, что же здесь на самом деле является «неизменяемым»? Безопасно ли иметь двауказателянаодинобъект?ВслучаесArc<T> ,да:изменяемыйобъектполностьюнаходитсявнутрисамойструктуры.Поэтойпричине,методclone() возвращаетнеизменяемуюссылку(&T ). Если бы он возвращал изменяемую ссылку (&mut T ), то у нас были бы проблемы.Таким образом,let mut z = Arc::new(5); объявляет атомарный счётчик ссылок свнешнейизменяемостью.

Другие типы, например те, что определены в модулеstd::cell , напротив, имеют«внутреннююизменяемость».Например:

RefCellвозвращаетизменяемуюссылку&mut припомощиметодаborrow_mut() .Анеопаснолиэто?Что,еслимысделаемтак:

Это приведёт к панике во время исполнения. Вот что делаетRefCell : онпринудительно выполняет проверку правил заимствования во время исполнения и вызываетpanic! ,еслионибылинарушены.

usestd::sync::Arc;

letx=Arc::new(5);lety=x.clone();

usestd::cell::RefCell;

letx=RefCell::new(42);

lety=x.borrow_mut();

usestd::cell::RefCell;

letx=RefCell::new(42);

lety=x.borrow_mut();letz=x.borrow_mut();

ЯзыкпрограммированияRust

203Изменяемость(mutability)

Page 204: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Стоит отметить, что тип изменяемости  — внутренняя или внешняя  — определяетсясамим типом. Нет способа волшебно превратить значение с внутренней изменяемостью взначениесовнешней,инаоборот.

ВсёэтоподводитнаскдругимаспектамправилизменяемостиRust.Давайтепоговоримоних.

ИзменяемостьнауровнеполейИзменяемость —этосвойстволибоссылки(&mut ),либоимени(letmut ).Этозначит,

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

Изменяемостьструктурыопределяетсяприеёсвязывании:

Однако,используяCell<T> ,выможетеэмулироватьизменяемостьнауровнеполей:

Этовыведетнаэкранy:Cell{value:7} .Мыуспешноизменилизначениеy .

structPoint{x:i32,muty:i32,//нельзя}

structPoint{x:i32,y:i32,}

letmuta=Point{x:5,y:6};

a.x=10;

letb=Point{x:5,y:6};

b.x=10;//error:cannotassigntoimmutablefield`b.x`

usestd::cell::Cell;

structPoint{x:i32,y:Cell<i32>,}

letpoint=Point{x:5,y:Cell::new(6)};

point.y.set(7);

println!("y:{:?}",point.y);

ЯзыкпрограммированияRust

204Изменяемость(mutability)

Page 205: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЯзыкпрограммированияRust

205Изменяемость(mutability)

Page 206: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СтруктурыСтруктуры(struct )—этоодиниз способов созданияболее сложныхтиповданных.

Например,еслимырассчитываемчто-тосиспользованиемкоординат2Dпространства,тонампонадобятсяобазначения—x иy :

Структура позволяет нам объединить эти два значения в один тип сx иy в качествеименполей:

Этот код делает много разных вещей, поэтому давайте разберём его по порядку. Мыобъявляем структуру с помощью ключевого словаstruct , за которым следует имяобъявляемой структуры. Обычно, имена типов-структур начинаются с заглавной буквы ииспользуют чередующийся регистр букв: названиеPointInSpace выглядит привычно, аPoint_In_Space  —нет.

Каквсегда,мыможемсоздать экземплярнашейструктурыспомощьюоператораlet .Однако в данном случае мы используем синтаксис видаключ:значение для установкизначения каждого поля. Порядок инициализации полей не обязательно должен совпадать спорядкомихобъявления.

Наконец,посколькууполейестьимена,мыможемполучитькнимдоступспомощьюоперацииточка :origin.x .

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

letorigin_x=0;letorigin_y=0;

structPoint{x:i32,y:i32,}

fnmain(){letorigin=Point{x:0,y:0};//origin:Point

println!("Началокоординатнаходитсяв({},{})",origin.x,origin.y);}

ЯзыкпрограммированияRust

206Структуры

Page 207: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЭтоткоднапечатаетТочканаходитсяв(5,0) .

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

Изменяемость — это свойство имени, а не самой структуры. Если вы привыкли куправлениюизменяемостьюнауровнеполей,сначалаэтоможетпоказатьсянепривычным,нона самомделе такое решение сильноупрощает вещи.Онодажепозволяет вамделатьименаизменяемымитольконакороткоевремя:

Структуры так же могут содержать&mut ссылки, это позволяет вам производитьподобныепреобразования:

structPoint{x:i32,y:i32,}

fnmain(){letmutpoint=Point{x:0,y:0};

point.x=5;

println!("Точканаходитсяв({},{})",point.x,point.y);}

structPoint{mutx:i32,y:i32,}

structPoint{x:i32,y:i32,}

fnmain(){letmutpoint=Point{x:0,y:0};

point.x=5;

letpoint=point;//этоновоеимянеизменяемо

point.y=6;//этовызываетошибку}

ЯзыкпрограммированияRust

207Структуры

Page 208: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Синтаксисобновления(updatesyntax)Вы можете включить в описание структуры.. чтобы показать, что вы хотите

использоватьзначенияполейкакой-тодругойструктуры.Например:

Этоткодприсваиваетpoint новоеy ,нооставляетстарыеx иz .Этонеобязательнодолжнабытьтажесамаяструктура—выможетеиспользоватьэтотсинтаксискогдасоздаётеновыеструктуры,чтобыскопироватьзначениянеуказанныхполей:

КортежныеструктурыВ Rust есть ещё один тип данных, который представляет собой нечто среднее между

кортежем и структурой. Он называетсякортежной структурой. Кортежные структурыименуются,авотуихполейимённет:

structPoint{x:i32,y:i32,}

structPointRef<'a>{x:&'amuti32,y:&'amuti32,}

fnmain(){letmutpoint=Point{x:0,y:0};

{letr=PointRef{x:&mutpoint.x,y:&mutpoint.y};

*r.x=5;*r.y=6;}

assert_eq!(5,point.x);assert_eq!(6,point.y);}

structPoint3d{x:i32,y:i32,z:i32,}

letmutpoint=Point3d{x:0,y:0,z:0};point=Point3d{y:1,..point};

letorigin=Point3d{x:0,y:0,z:0};letpoint=Point3d{z:1,x:2,..origin};

ЯзыкпрограммированияRust

208Структуры

Page 209: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этидваобъектаразличны,несмотрянато,чтоуниходинаковыезначения.

Почтивсегда,вместокортежнойструктурылучшеиспользоватьобычнуюструктуру.МыбыскорееобъявилитипыColor иPoint воттак:

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

Однако,есть один случай, когда кортежные структуры очень полезны. Это кортежнаяструктурасвсегооднимэлементом.Такоеиспользованиеназываетсяновымтипом,потомучтооно позволяет создать новый тип, отличный от типа значения, содержащегося в кортежнойструктуре.Приэтомновыйтипобозначаетчто-тодругое:

Как вы можете видеть в данном примере, извлечь вложенный целый тип можно спомощьюдеконструирующегоlet .Мыобсуждалиэтовыше,вразделе«кортежи».Вданномслучае, операторlet Inches(integer_length) присваивает10 имениinteger_length .

Unit-подобныеструктурыВыможетеобъявитьструктурубезполейвообще:

structColor(i32,i32,i32);structPoint(i32,i32,i32);

letblack=Color(0,0,0);letorigin=Point(0,0,0);

structColor{red:i32,blue:i32,green:i32,}

structPoint{x:i32,y:i32,z:i32,}

structInches(i32);

letlength=Inches(10);

letInches(integer_length)=length;println!("Длинавдюймах:{}",integer_length);

structElectron;

letx=Electron;

ЯзыкпрограммированияRust

209Структуры

Page 210: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Такие структуры называют «unit-подобные» («unit-like»), потому что они похожи напустой кортеж() , иногда называемый «unit». Как и кортежные структуры, их называютновымтипом.

Самипосебеониредкобываютполезны(хотяиногдаихиспользуютвкачествеметок),но в сочетании с другими возможностями их использование имеет смысл. Например, дляиспользования библиотеки может быть необходимо создать структуру, которая реализуетопределенныйтипаж для обработки событий. Если у вас нет данных, которые нужнопоместитьвструктуру,томожнопростосоздатьunit-подобнуюструктуру.

ЯзыкпрограммированияRust

210Структуры

Page 211: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ПеречисленияВ Rustперечисление (enum )— это тип данных, который представляет собой один из

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

Синтаксисдляобъявлениявариантовсхожссинтаксисомдляобъявленияструктур:увасмогут быть варианты без данных (как unit-подобные структуры), варианты с именованнымиданными и варианты с безымянными данными (подобно кортежным структурам). Вариантыперечисленияимеютодини тотже тип, и в отличииот структурне являются определениемотдельныхтипов.Значениеперечисленияможетсоответствоватьлюбомуизвариантов.Из-заэтого перечисления иногда называюттип-сумма (sum-type): множество возможных значенийперечисления—этосуммамножестввозможныхзначенийкаждоговарианта.

Мыиспользуемсинтаксис:: чтобыиспользоватьимякаждогоизвариантов.Ихобластьвидимостиограниченаименемсамогоперечисления.Этопозволяетиспользоватьобавариантаизпримеранижесовместно:

ОбавариантаимеютодинаковоеимяMove ,нопосколькуобластьвидимостикаждогоизних ограничена именем соответствующего перечисления, они могут быть использованы безконфликтов.

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

enumMessage{Quit,ChangeColor(i32,i32,i32),Move{x:i32,y:i32},Write(String),}

letx:Message=Message::Move{x:3,y:4};

enumBoardGameTurn{Move{squares:i32},Pass,}

lety:BoardGameTurn=BoardGameTurn::Move{squares:1};

fnprocess_color_change(msg:Message){letMessage::ChangeColor(r,g,b)=msg;//ошибкавременикомпиляции}

ЯзыкпрограммированияRust

211Перечисления

Page 212: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

То, что пользовательские типы по умолчанию не поддерживают операции, можетпоказатьсядовольноограниченным.Ноэтоограничение,котороемывсегдаможемпреодолеть.Естьдваспособа:реализоватьоперациюсамостоятельно,иливоспользоватьсясопоставлениемс образцом с помощьюmatch , о котором вы узнаете в следующем разделе. Пока мы ещенедостаточнознаемRust,чтобыреализовыватьоперации,номынаучимсяделатьэтовразделеtraits .

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

Например:

тожесамое,чтои

Наданныймоментэтонетакужиполезнодлянас,нокогдамыперейдемкзамыканиям,мыпоговоримопередачефункцийвролиаргументадругойфункции.Например,спомощьюитераторов мы можем преобразовывать вектор строк в вектор состоящий изMessage::Write :

letm=Message::Write("Hello,world".to_string());

fnfoo(x:String)->Message{Message::Write(x)}

letx=foo("Hello,world".to_string());

letv=vec!["Hello".to_string(),"World".to_string()];

letv1:Vec<Message>=v.into_iter().map(Message::Write).collect();

ЯзыкпрограммированияRust

212Перечисления

Page 213: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Конструкция`match`Простогоif /else частонедостаточно,потомучтонужнопроверитьбольше, чемдва

возможныхварианта.Даиктомужеусловиявelse частостановятсяоченьсложными.Какжерешитьэтупроблему?

В Rust есть ключевое словоmatch , позволяющее заменить группы операторовif /else чем-тоболееудобным.Смотрите:

match принимает выражение и выбирает одну из ветвей исполнения согласно егозначению. Каждаяветвь имеет формузначение => выражение . Выражение ветвивычисляется, когда значение данной ветви совпадает со значением, принятым операторомmatch (в данном случае,x ). Эта конструкция называетсяmatch (сопоставление), потомучтоонавыполняетсопоставлениезначениянеким«шаблонам».Глава«Шаблоны»описываетвсешаблоны,которыеможноиспользоватьвmatch .

Так в чём же преимущества данной конструкции? Их несколько. Во-первых, ветвиmatch проверяются на полноту. Видите последнюю ветвь, со знаком подчёркивания (_ )?Еслимыудалимеё,Rustвыдастошибку:

error:non-exhaustivepatterns:`_`notcovered

Другими словами, компилятор сообщает нам, что мы забыли сопоставить какие-тозначения. Посколькуx   — это целое число, оно может принимать разные значения  —например,6 .Однако,еслимыубираемветвь_ ,ниоднаветвьнесовпадёт,поэтомутакойкодне скомпилируется._  —это«совпадениеслюбымзначением».Еслиниоднадругаяветвьнесовпала, совпадёт ветвь с_ . Поскольку в примере выше есть ветвь с_ , мы покрываем всёмножествозначенийx ,инашапрограммаскомпилируется.

match также является выражением. Это значит, что мы можем использовать его вправойчастиоператораlet илинепосредственнокаквыражение:

letx=5;

matchx{1=>println!("один"),2=>println!("два"),3=>println!("три"),4=>println!("четыре"),5=>println!("пять"),_=>println!("что-тоещё"),}

ЯзыкпрограммированияRust

213Конструкция`match`

Page 214: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Иногдаспомощьюmatch можноудобнопреобразоватьзначенияодноготипавдругой.

СопоставлениесобразцомдляперечисленийДругой полезный способ использованияmatch   — обработка возможных вариантов

перечисления:

Какобычно,компиляторRustпроверяетполноту,поэтомувmatch должнабытьветвьдля каждого варианта перечисления. Если какой-то вариант отсутствует, программа нескомпилируетсяивампридётсяиспользовать_ .

Здесь мы не можем использовать обычныйif вместоmatch , в отличие от кода,который мы видели раньше. Но мы могли бы использоватьif let   — его можновосприниматькаксокращённуюформузаписиmatch .

letx=5;

letnumer=matchx{1=>"one",2=>"two",3=>"three",4=>"four",5=>"five",_=>"somethingelse",};

enumMessage{Quit,ChangeColor(i32,i32,i32),Move{x:i32,y:i32},Write(String),}

fnquit(){/*...*/}fnchange_color(r:i32,g:i32,b:i32){/*...*/}fnmove_cursor(x:i32,y:i32){/*...*/}

fnprocess_message(msg:Message){matchmsg{Message::Quit=>quit(),Message::ChangeColor(r,g,b)=>change_color(r,g,b),Message::Move{x:x,y:y}=>move_cursor(x,y),Message::Write(s)=>println!("{}",s),};}

ЯзыкпрограммированияRust

214Конструкция`match`

Page 215: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Шаблонысопоставления`match`Шаблоны достаточно часто используются в Rust. Мы уже использовали их в разделе

Связываниепеременных,вразделеКонструкцияmatch ,атакжевнекоторыхдругихместах.Давайтекороткопробежимсяповсемвозможностям,которыеможнореализоватьспомощьюшаблонов!

Быстро освежимвпамяти: сопоставлять сшаблономлитералыможнолибонапрямую,либосиспользованиемсимвола_ ,которыйозначаетлюбойслучай:

Этоткоднапечатаетодин .

СопоставлениеснесколькимишаблонамиВыможетесопоставлятьснесколькимишаблонами,используя| :

Этоткоднапечатаетодинилидва .

ДеструктуризацияЕсливыработаетессоставнымтипомданных,вродеstruct ,выможетеразобратьего

начасти(«деструктурировать»)внутришаблона:

letx=1;

matchx{1=>println!("один"),2=>println!("два"),3=>println!("три"),_=>println!("чтоугодно"),}

letx=1;

matchx{1|2=>println!("одинилидва"),3=>println!("три"),_=>println!("чтоугодно"),}

structPoint{x:i32,y:i32,}

letorigin=Point{x:0,y:0};

matchorigin{Point{x,y}=>println!("({},{})",x,y),}

ЯзыкпрограммированияRust

215Шаблонысопоставления`match`

Page 216: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мыможемиспользовать: ,чтобыпривязатьзначениекновомуимени.

Если нас интересуют только некоторые значения, мы можем не давать имена всемсоставляющим:

Этоткоднапечатаетxравен0 .

Вы можете использовать это в любом сопоставлении: не обязательно игнорироватьименнопервыйэлемент:

Этоткоднапечатаетyравен0 .

Можно произвести деструктуризацию любого составного типа данных  — например,кортежейиперечислений.

ИгнорированиесвязыванияВы можете использовать в шаблоне_ , чтобы проигнорировать соответствующее

значение.Например,вотсопоставлениеResult<T,E> :

structPoint{x:i32,y:i32,}

letorigin=Point{x:0,y:0};

matchorigin{Point{x:x1,y:y1}=>println!("({},{})",x1,y1),}

structPoint{x:i32,y:i32,}

letorigin=Point{x:0,y:0};

matchorigin{Point{x,..}=>println!("xравен{}",x),}

structPoint{x:i32,y:i32,}

letorigin=Point{x:0,y:0};

matchorigin{Point{y,..}=>println!("yравен{}",y),}

ЯзыкпрограммированияRust

216Шаблонысопоставления`match`

Page 217: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

В первой ветви мы привязываем значение вариантаOk к имениvalue . А в ветвиобработки вариантаErr мы используем_ , чтобы проигнорировать конкретную ошибку, ипростопечатаемобщеесообщение.

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

Здесь мы связываем первый и последний элемент кортежа с именамиx иzсоответственно,авторойэлементигнорируем.

Похожимобразом,вшаблонеможноиспользовать.. ,чтобыпроигнорироватьнесколькозначений.

ЭтоткодпечатаетПолучиликортеж! .

refиrefmutЕсливыхотитеполучитьссылку,тоиспользуйтеключевоесловоref :

ЭтоткоднапечатаетПолучилиссылкуна5 .

Здесьr внутриmatch имеет тип&i32 . Другими словами, ключевое словоrefсоздает ссылку, для использования вшаблоне. Если вам нужна изменяемая ссылка, тоrefmut будетработатьаналогичнымобразом:

matchsome_value{Ok(value)=>println!("получилизначение:{}",value),Err(_)=>println!("произошлаошибка"),}

fncoordinate()->(i32,i32,i32){//создаёмивозвращаемкакой-токортежизтрёхэлементов}

let(x,_,z)=coordinate();

enumOptionalTuple{Value(i32,i32,i32),Missing,}

letx=OptionalTuple::Value(5,-2,3);

matchx{OptionalTuple::Value(..)=>println!("Получиликортеж!"),OptionalTuple::Missing=>println!("Вотнеудача."),}

letx=5;

matchx{refr=>println!("Получилиссылкуна{}",r),}

ЯзыкпрограммированияRust

217Шаблонысопоставления`match`

Page 218: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СопоставлениесдиапазономВыможетесопоставлятьсдиапазономзначений,используя... :

Этоткоднапечатаетотодногодопяти .

Диапазонывосновномиспользуютсясчисламиилиодиночнымисимволами(char ).

Этоткоднапечатаетчто-тоещё .

СвязываниеВыможетесвязатьзначениесименемспомощьюсимвола@ :

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

letmutx=5;

matchx{refmutmr=>println!("Получилиизменяемуюссылкуна{}",mr),}

letx=1;

matchx{1...5=>println!("отодногодопяти"),_=>println!("чтоугодно"),}

letx='�';

matchx{'а'...'и'=>println!("ранняябуква"),'к'...'я'=>println!("поздняябуква"),_=>println!("что-тоещё"),}

letx=1;

matchx{[email protected]=>println!("получилиэлементдиапазона{}",e),_=>println!("чтоугодно"),}

ЯзыкпрограммированияRust

218Шаблонысопоставления`match`

Page 219: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЭтоткоднапечатаетSome("Steve") :мысвязаливнутреннююname сa .

Есливыиспользуете@ совместно с| ,товыдолжныубедиться,чтоимясвязываетсявкаждойизчастейшаблона:

ОграничителишаблоновВыможетеввестиограничителишаблонов(matchguards)спомощьюif :

ЭтоткоднапечатаетПолучилицелое! .

Есливыиспользуетеif снесколькимишаблонами,онприменяетсякобоимчастям:

Этоткодпечатаетнет ,потомучтоif применяетсяковсему4|5 , анетолькок5 .Другимисловами,приоритетif выглядиттак:

#[derive(Debug)]structPerson{name:Option<String>,}

letname="Steve".to_string();letmutx:Option<Person>=Some(Person{name:Some(name)});matchx{Some(Person{name:refa@Some(_),..})=>println!("{:?}",a),_=>{}}

letx=5;

matchx{[email protected]|[email protected]=>println!("получилиэлементдиапазона{}",e),_=>println!("чтоугодно"),}

enumOptionalInt{Value(i32),Missing,}

letx=OptionalInt::Value(5);

matchx{OptionalInt::Value(i)ifi>5=>println!("Получилицелоебольшепяти!"),OptionalInt::Value(..)=>println!("Получилицелое!"),OptionalInt::Missing=>println!("Неудача."),}

letx=4;lety=false;

matchx{4|5ify=>println!("да"),_=>println!("нет"),}

ЯзыкпрограммированияRust

219Шаблонысопоставления`match`

Page 220: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

(4|5)ify=>...

анетак:

4|(5ify)=>...

ЗаключениеВоттак!Существуетмногоразныхспособовиспользованияконструкциисопоставления

сшаблоном, и все они могут быть смешаны и состыкованы, в зависимости от того, что выхотитесделать:

Шаблоны —этооченьмощныйинструмент.Используйтеих.

matchx{Foo{x:Some(refname),y:None}=>...}

ЯзыкпрограммированияRust

220Шаблонысопоставления`match`

Page 221: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СинтаксисметодовФункции  — это хорошо, но если вы хотите вызвать несколько связных функций для

каких-либоданных,тоэтоможетбытьнеудобно.Рассмотримэтоткод:

Читать данную строку кода следует слева направо, поэтому мы наблюдаем такойпорядок:«bazbarfoo».Ноонпротивоположенпорядку,вкоторомфункциибудутвызываться:«foobarbaz».Былобыкласснозаписатьвызовывтомпорядке,вкоторомонипроисходят,нетакли?

К счастью, как вы уже наверно догадались, это возможно! Rust предоставляетвозможность использовать такойсинтаксис вызова метода с помощью ключевого словаimpl .

ВызовметодовВоткакэтоработает:

Этоткоднапечатает12.566371 .

Мысоздалиструктуру,котораяпредставляетсобойкруг.Затеммынаписалиблокimplиопределилиметодarea внутринего.

Методы принимают специальный первый параметр,&self . Есть три возможныхварианта:self , &self и&mutself .Выможетедуматьобэтомспециальномпараметрекак оx вx.foo() . Три варианта соответствуют трем возможным видам элементаx :self  —еслиэтопростозначениевстеке,&self  —еслиэтоссылкаи&mutself  —если

baz(bar(foo)));

foo.bar().baz();

structCircle{x:f64,y:f64,radius:f64,}

implCircle{fnarea(&self)->f64{std::f64::consts::PI*(self.radius*self.radius)}}

fnmain(){letc=Circle{x:0.0,y:0.0,radius:2.0};println!("{}",c.area());}

ЯзыкпрограммированияRust

221Синтаксисметодов

Page 222: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

это изменяемая ссылка. Мы передаем параметр&self в методarea , поэтому мы можемиспользоватьеготакже,какилюбойдругойпараметр.Таккакмызнаем,чтоэтоCircle ,мыможемполучитьдоступкполюradius также,какеслибыэтобылалюбаядругаяструктура.

По умолчанию следует использовать&self , также как следует предпочитатьзаимствование владению, а неизменные ссылки изменяемым. Вот пример, включающий всетриварианта:

ЦепочкавызововметодовИтак,теперьмызнаем,каквызватьметод,напримерfoo.bar() .Ночтонасчетнашего

первоначального примера,foo.bar().baz()? Это называется «цепочка вызовов», и мыможемсделатьэто,вернувself .

structCircle{x:f64,y:f64,radius:f64,}

implCircle{fnreference(&self){println!("принимаемselfпоссылке!");}

fnmutable_reference(&mutself){println!("принимаемselfпоизменяемойссылке!");}

fntakes_ownership(self){println!("принимаемвладениеself!");}}

ЯзыкпрограммированияRust

222Синтаксисметодов

Page 223: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Проверьтетипвозвращаемогозначения:

Мыпростоуказываем,чтовозвращаетсяCircle .Спомощьюэтогометодамыможемсоздатьновыйкруг,площадькоторогобудетв100разбольше,чемустарого.

СтатическиеметодыВы также можете определить методы, которые не принимают параметрself . Вот

шаблонпрограммирования,которыйоченьраспространенвкоденаRust:

structCircle{x:f64,y:f64,radius:f64,}

implCircle{fnarea(&self)->f64{std::f64::consts::PI*(self.radius*self.radius)}

fngrow(&self,increment:f64)->Circle{Circle{x:self.x,y:self.y,radius:self.radius+increment}}}

fnmain(){letc=Circle{x:0.0,y:0.0,radius:2.0};println!("{}",c.area());

letd=c.grow(2.0).area();println!("{}",d);}

fngrow(&self)->Circle{

ЯзыкпрограммированияRust

223Синтаксисметодов

Page 224: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этотстатический метод, который создает новыйCircle . Обратите внимание, чтостатические методы вызываются с помощью синтаксиса:Struct::method() , а неref.method() .

Шаблон«строитель»(BuilderPattern)Давайтепредположим,чтонамнужно,чтобынашипользователимоглисоздаватькругии

чтобыунихбылавозможностьзадаватьтолькотесвойства,которыеимнужны.Впротивномслучае, атрибутыx иy будут0.0 , аradius будет1.0 .Rustнеподдерживаетперегрузкуметодов, именованные аргументы или переменное количество аргументов. Вместо этого мыиспользуемшаблон«строитель».Онвыглядитследующимобразом:

structCircle{x:f64,y:f64,radius:f64,}

implCircle{fnnew(x:f64,y:f64,radius:f64)->Circle{Circle{x:x,y:y,radius:radius,}}}

fnmain(){letc=Circle::new(0.0,0.0,2.0);}

ЯзыкпрограммированияRust

224Синтаксисметодов

Page 225: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

structCircle{x:f64,y:f64,radius:f64,}

implCircle{fnarea(&self)->f64{std::f64::consts::PI*(self.radius*self.radius)}}

structCircleBuilder{x:f64,y:f64,radius:f64,}

implCircleBuilder{fnnew()->CircleBuilder{CircleBuilder{x:0.0,y:0.0,radius:0.0,}}

fnx(&mutself,coordinate:f64)->&mutCircleBuilder{self.x=coordinate;self}

fny(&mutself,coordinate:f64)->&mutCircleBuilder{self.y=coordinate;self}

fnradius(&mutself,radius:f64)->&mutCircleBuilder{self.radius=radius;self}

fnfinalize(&self)->Circle{Circle{x:self.x,y:self.y,radius:self.radius}}}

fnmain(){letc=CircleBuilder::new().x(1.0).y(2.0).radius(2.0).finalize();

println!("площадь:{}",c.area());println!("x:{}",c.x);println!("y:{}",c.y);}

ЯзыкпрограммированияRust

225Синтаксисметодов

Page 226: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Всё,чтомысделализдесь —этосоздалиещёоднуструктуру,CircleBuilder .Внеймыопределилиметодыстроителя.Такжемыопределилиметодarea() вCircle .Мытакжесделали еще один метод вCircleBuilder : finalize() . Этот метод создаёт нашокончательныйCircle из строителя. Таким образом, мы можем использовать методыCircleBuilder чтобыуточнитьсозданиеCircle .

ЯзыкпрограммированияRust

226Синтаксисметодов

Page 227: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вектора«Вектор» — это динамический или, по-другому, «растущий» массив, реализованный в

виде стандартного библиотечного типаVec<T> (где<T> являетсяобобщённым типом).Векторавсегдаразмещаютданныевкуче.Выможетесоздаватьихспомощьюмакросаvec! :

(Заметьте, что, в отличие от макросаprintln! , который мы использовали ранее, сvec! используются квадратные скобки[] . Rust разрешает использование и круглых, иквадратныхскобоквобеихситуациях —этопростостилистическоесоглашение.)

Длясозданиявектораизповторяющихсязначенийестьдругаяформаvec! :

ДоступкэлементамЧтобыполучитьзначениепоопределенномуиндексуввекторе,мыиспользуем[] :

Индексыотсчитываютсяот0 ,такчтотретьимэлементомявляетсяv[2] .

ОбходВыможетеобойтиэлементывектораспомощьюfor .Естьтриварианта:

У векторов есть много других полезных методов, о которых вы можете прочитать вдокументацииAPI.

letv=vec![1,2,3,4,5];//v:Vec<i32>

letv=vec![0;10];//десятьнулей

letv=vec![1,2,3,4,5];

println!("Третийэлементвектораvравен{}",v[2]);

letmutv=vec![1,2,3,4,5];

foriin&v{println!("Ссылка{}",i);}

foriin&mutv{println!("Изменяемаяссылка{}",i);}

foriinv{println!("Владениевекторомиегоэлементами{}",i);}

ЯзыкпрограммированияRust

227Вектора

Page 228: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЯзыкпрограммированияRust

228Вектора

Page 229: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СтрокиСтроки —важное понятие для любого программиста.Система обработки строк вRust

немного отличается от других языков, потому что это язык системного программирования.Работатьсоструктурамиданныхспеременнымразмеромдовольносложно,истроки —какразтакая структура данных. Кроме того, работа со строками в Rust также отличается и отнекоторыхсистемныхязыков,такихкакC.

Давайте разбираться в деталях.string  — это последовательность скалярных значенийюникод,закодированныхввидепотокабайтUTF-8.ВсестрокидолжныбытьгарантированновалиднымиUTF-8последовательностями.Крометого,строкинеоканчиваютсянулёмимогутсодержатьнулевыебайты.

ВRust естьдваосновныхтипа строк:&str иString . Сперва поговорим о&str  —это «строковый срез». Строковые срезы имеют фиксированный размер и не могут бытьизменены.ОнипредставляютсобойссылкунапоследовательностьбайтUTF-8:

"Всемпривет." —этостроковыйлитерал,еготип—&'staticstr .Строковыелитералы являются статически размещенными строковыми срезами. Это означает, что онисохраняются внутри нашей скомпилированной программы и существуют в течение всегопериода ее выполнения. Имяgreeting представляет собой ссылку на эту статическиразмещенную строку. Любая функция, ожидающая строковый срез, может также принять вкачествеаргументастроковыйлитерал.

Строковые литералы могут состоять из нескольких строк. Такие литералы можнозаписыватьвдвухразныхформах.Перваябудетвключатьвсебяпереводнановуюстрокуиведущиепробелы:

Втораяформа,включающаявсебя\ ,вырезаетпробелыипереводнановуюстроку:

НовRustестьнетолько&str .ТипString представляетсобойстроку,размещеннуювкуче.Этастрокарасширяема,ионатакжегарантированноявляетсяпоследовательностьюUTF-8 .String обычно создаётся путем преобразования изстрокового среза с использованиемметодаto_string .

letgreeting="Всемпривет.";//greeting:&'staticstr

lets="foobar";

assert_eq!("foo\nbar",s);

lets="foo\bar";

assert_eq!("foobar",s);

ЯзыкпрограммированияRust

229Строки

Page 230: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

String преобразуютсяв&str спомощью& :

Это преобразование не происходит в случае функций, которые принимают какой-тотипаж&str , а не сам&str . Например, у методаTcpStream::connect есть параметртипаToSocketAddrs .Сюдаможнопередать&str , ноString нужноявнопреобразоватьспомощью&* .

ПредставлениеString как&str   — дешёвая операция, но преобразование&str вString предполагаетвыделениепамяти.Нестоитделатьэтобезнеобходимости!

ИндексацияПоскольку строки являются валидными UTF-8 последовательностями, то они не

поддерживаютиндексацию:

Как правило, доступ к вектору с помощью[] является очень быстрой операцией.Нопоскольку каждый символ в строке, закодированной UTF-8, может быть представленнесколькимибайтами,топрипоискевыдолжныперебратьn-оеколичестволитервстроке.Этозначительно более дорогая операция, а мы не хотим вводить в заблуждение. Кроме того,«литера» —этонесовсемто,чтоопределеновUnicode.Мыможемвыбратькакрассматриватьстроку:какотдельныебайтыиликаккодовыеединицы(codepoints):

letmuts="Привет".to_string();//muts:Stringprintln!("{}",s);

s.push_str(",мир.");println!("{}",s);

fntakes_slice(slice:&str){println!("Получили:{}",slice);}

fnmain(){lets="Привет".to_string();takes_slice(&s);}

usestd::net::TcpStream;

TcpStream::connect("192.168.0.1:3000");//параметр&str

letaddr_string="192.168.0.1:3000".to_string();TcpStream::connect(&*addr_string);//преобразуемaddr_stringв&str

lets="привет";

println!("Перваябукваs —{}",s[0]);//ОШИБКА!!!

ЯзыкпрограммированияRust

230Строки

Page 231: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этоткоднапечатает:

229,191,160,231,138,172,227,131,143,227,131,129,229,133,172,�,�,�,�,�,

Каквыможетевидеть,количествобайтбольше,чемколичествосимволов(char ).

Выможетеполучитьчто-тонаподобиеиндекса,какпоказанониже:

Этоподчеркивает,чтомыдолжныпройтипоспискуchars отегоначала.

СрезыВыможетеполучитьсрезстрокиспомощьюсинтаксисасрезов:

Нозаметьте,чтоэтоиндексыбайтов,анесимволов.Поэтомуэтоткодзапаникует:

стакойошибкой:

thread'<main>'panickedat'index0and/or2in`�����`donotlieoncharacterboundary'

КонкатенацияЕслиувасестьString ,товыможетеприсоединитькнемувконец&str :

НоеслиувасестьдвеString ,тонеобходимоиспользовать& :

lethachiko="�����";

forbinhachiko.as_bytes(){print!("{},",b);}

println!("");

forcinhachiko.chars(){print!("{},",c);}

println!("");

letdog=hachiko.chars().nth(1);//что-товродеhachiko[1]

letdog="hachiko";lethachi=&dog[0..5];

letdog="�����";lethachi=&dog[0..2];

lethello="Hello".to_string();letworld="world!";

lethello_world=hello+world;

ЯзыкпрограммированияRust

231Строки

Page 232: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Это потому, что&String может быть автоматически приведен к&str . Этавозможностьназывается«Приведениеприразыменовании».

lethello="Hello".to_string();letworld="world!".to_string();

lethello_world=hello+&world;

ЯзыкпрограммированияRust

232Строки

Page 233: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

работалидлянесколькихтиповаргументов.Ксчастью,уRustестьвозможность,котораядаётнам лучший способ реализовать это: обобщённое программирование. Обобщённоепрограммирование называется «параметрическим полиморфизмом» в теории типов. Этоозначает,чтотипыилифункцииимеютнесколькоформ(poly —кратно,morph —форма)поданномупараметру(«параметрический»).

Влюбомслучае,хватитотеориитипов;давайтерассмотримкакой-нибудьобобщённыйкод. Стандартная библиотека Rust предоставляет типOption<T> , который являетсяобобщённымтипом:

Часть<T> , которую вы раньше уже видели несколько раз, указывает, что этообобщённый тип данных. Внутри перечисления, везде, где мы видимT , мы подставляемвместо этого абстрактного типа тот, который используется в обобщении. Вот примериспользованияOption<T> снекоторымидополнительнымианнотациямитипов:

ВопределениитипамыиспользуемOption<i32> .Обратитевнимание,чтоэтооченьпохоже наOption<T> .С тойлишьразницей, что, в данномконкретномOption , T имеетзначениеi32 .ВправойстороневыражениямыиспользуемSome(T) ,гдеT равно5 .Таккак5 является представителем типаi32 , то типы по обе стороны совпадают, поэтомукомпиляторсчастлив.Еслижеонинесовпадают,томыполучимошибку:

Но это не значит, что мы не можем сделатьOption<T> , который содержитf64 !Простотипыдолжнысовпадать:

Этопростопрекрасно.Одноопределение —многостороннееиспользование.

Обобщатьможноболее,чемпоодномупараметру.РассмотримдругойобобщённыйтипизстандартнойбиблиотекиRust —Result<T,E> :

enumOption<T>{Some(T),None,}

letx:Option<i32>=Some(5);

letx:Option<f64>=Some(5);//error:mismatchedtypes:expected`core::option::Option<f64>`,//found`core::option::Option<_>`(expectedf64butfoundintegralvariable)

letx:Option<i32>=Some(5);lety:Option<f64>=Some(5.0f64);

ЯзыкпрограммированияRust

233Обобщённоепрограммирование

Page 234: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этот тип является обобщённым сразу длядвух типов:T иE .Кстати, заглавныебуквымогутбытьлюбыми.МымоглибыопределитьResult<T,E> как:

если бы захотели. Соглашение гласит, что первый обобщённый параметр для 'типа'долженбытьT ,ичтодля'ошибки'используетсяE .НоRustнепроверяетэтого.

ТипResult<T,E> предназначендлятого,чтобывозвращатьрезультатвычисления,иимеетвозможностьвернутьошибку,еслипроизойдёткакой-либосбой.

ОбобщённыефункцииМы можем задавать функции, которые принимают обобщённые типы, с помощью

аналогичногосинтаксиса:

Синтаксис состоит из двух частей:<T> говорит о том, что «эта функция являетсяобобщённойпоодномутипу,T»,аx:T говоритотом,что«химееттипT».

Несколькоаргументовмогутиметьодинитотжеобобщённыйтип:

Мыможемнаписатьверсию,котораяпринимаетнесколькотипов:

Обобщённые функции наиболее полезны в связке с «ограничениями по типажам», окоторыхмырасскажемвглавеТипажи.

ОбобщённыеструктурыВытакжеможетезадатьобобщённыйтипдляstruct :

enumResult<T,E>{Ok(T),Err(E),}

enumResult<A,Z>{Ok(A),Err(Z),}

fntakes_anything<T>(x:T){//делаемчто-тосx}

fntakes_two_of_the_same_things<T>(x:T,y:T){//...}

fntakes_two_things<T,U>(x:T,y:U){//...}

ЯзыкпрограммированияRust

234Обобщённоепрограммирование

Page 235: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Аналогично функциям, мы также объявляем обобщённые параметры в<T> , а затемиспользуемихвобъявлениитипаx:T .

structPoint<T>{x:T,y:T,}

letint_origin=Point{x:0,y:0};letfloat_origin=Point{x:0.0,y:0.0};

ЯзыкпрограммированияRust

235Обобщённоепрограммирование

Page 236: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ТипажиТипаж --- это возможность объяснить компилятору, что данный тип должен

предоставлятьопределённуюфункциональность.

Выпомнитеключевое словоimpl ,используемоедлявызовафункциичерезсинтаксисметода?

Типажи схожи, за исключением того, что мы определяем типаж, содержащий лишьсигнатуру метода, а затем реализуем этот типаж для нужной структуры. Например, какпоказанониже:

Каквыможетевидеть,блокtrait оченьпохожнаблокimpl .Различиесостоитлишьв том, что тело метода не определяется, а определяется только его сигнатура. Когда мыреализуемтипаж,мыиспользуемimplTraitforItem ,анепростоimplItem .

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

Rustвыводит:

structCircle{x:f64,y:f64,radius:f64,}

implCircle{fnarea(&self)->f64{std::f64::consts::PI*(self.radius*self.radius)}}

structCircle{x:f64,y:f64,radius:f64,}

traitHasArea{fnarea(&self)->f64;}

implHasAreaforCircle{fnarea(&self)->f64{std::f64::consts::PI*(self.radius*self.radius)}}

fnprint_area<T>(shape:T){println!("Thisshapehasanareaof{}",shape.area());}

ЯзыкпрограммированияRust

236Типажи

Page 237: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

error:type`T`doesnotimplementanymethodinscopenamed`area`

ПосколькуT может быть любого типа, мы не можем быть уверены, что он реализуетметодarea .Номыможемдобавить«ограничениепотипажу»кнашемуобобщённомутипуT ,гарантируя,чтоонбудетсоответствоватьтребованиям:

Синтаксис<T:HasArea> означает«любойтип,реализующийтипажHasArea».Таккактипажиопределяютсигнатурытиповфункций,мыможембытьуверены,чтолюбойтип,которыйреализуетHasArea ,будетиметьметод.area() .

Вотрасширенныйпримертого,какэтоработает:

fnprint_area<T:HasArea>(shape:T){println!("Thisshapehasanareaof{}",shape.area());}

ЯзыкпрограммированияRust

237Типажи

Page 238: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Нижепоказанвыводпрограммы:

Площадьэтойфигурыравна3.141593Площадьэтойфигурыравна1

traitHasArea{fnarea(&self)->f64;}

structCircle{x:f64,y:f64,radius:f64,}

implHasAreaforCircle{fnarea(&self)->f64{std::f64::consts::PI*(self.radius*self.radius)}}

structSquare{x:f64,y:f64,side:f64,}

implHasAreaforSquare{fnarea(&self)->f64{self.side*self.side}}

fnprint_area<T:HasArea>(shape:T){println!("Площадьэтойфигурыравна{}",shape.area());}

fnmain(){letc=Circle{x:0.0f64,y:0.0f64,radius:1.0f64,};

lets=Square{x:0.0f64,y:0.0f64,side:1.0f64,};

print_area(c);print_area(s);}

ЯзыкпрограммированияRust

238Типажи

Page 239: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Каквыможетевидеть,теперьprint_area нетолькоявляетсяобобщённойфункцией,но и гарантирует, что будет получен корректный тип. Еслиже мы передадим некорректныйтип:

Мыполучимошибкувременикомпиляции:

error:thetrait`HasArea`isnotimplementedforthetype`_`[E0277]

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

Хотя технически это возможно, реализацияметодов для примитивных типов считаетсяплохимстилемпрограммирования.

Может показаться, что такой подход легко приводит к бардаку в коде, однако есть дваограничения,связанныесреализациейтипажей,которыемешаюткодувыйтииз-подконтроля.Во-первых, если типаж не определён в нашей области видимости, он не применяется.Например, стандартная библиотека предоставляет типажWrite , который добавляет типуFile функциональностьввода-вывода.ПоумолчаниюуFile небудетэтихметодов:

Вотошибка:

error:type`std::fs::File`doesnotimplementanymethodinscopenamed`write`letresult=f.write(buf);^~~~~~~~~~

Сначаламыдолжнысделатьuse длятипажаWrite :

Этоскомпилируетсябезошибки.

print_area(5);

traitHasArea{fnarea(&self)->f64;}

implHasAreafori32{fnarea(&self)->f64{println!("этонелепо");

*selfasf64}}

5.area();

letmutf=std::fs::File::open("foo.txt").ok().expect("Немогуоткрытьfoo.txt");letbuf=b"whatever";//литералстрокибайт.buf:&[u8;8]letresult=f.write(buf);

usestd::io::Write;

letmutf=std::fs::File::open("foo.txt").ok().expect("Немогуоткрытьfoo.txt");letbuf=b"whatever";letresult=f.write(buf);

ЯзыкпрограммированияRust

239Типажи

Page 240: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Благодарятакойлогикеработы,дажеесликто-тосделаетчто-тострашное —например,добавитметодыi32 ,этонекоснётсявас,покавынеимпортируететипаж.

Второеограничениереализациитипажей---этото,чтоилитипаж,илитип,длякотороговыреализуететипаж,долженбытьреализованвами.МымоглибыопределитьHasArea дляi32 ,потомучтоHasArea  —этонашкод.Ноеслибымыпопробовалиреализоватьдляi32ToString  —типаж,предоставляемыйRust —мыбынесмоглисделатьэто,потомучтонитипаж,нитипнереализованнами.

Последнее, что нужно сказать о типажах: обобщённые функции с ограничением потипажам используютмономорфизацию (mono: один,morph: форма), поэтому онидиспетчеризуются статически. Что это значит? Посмотрите главуТипажи-объекты, чтобыполучитьбольшеинформации.

МножественныеограниченияпотипажамВы уже видели, как можно ограничить обобщённый параметр типа определённым

типажом:

Есливамнужнобольшеодногоограничения,выможетеиспользовать+ :

ТеперьтипT долженреализоваватькактипажClone ,такитипажDebug .

УтверждениеwhereНаписание функций с несколькими обобщёнными типами и небольшим количеством

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

Имя функции находится слева, а список параметров  — далеко справа. Ограничениязагромождаютместо.

fnfoo<T:Clone>(x:T){x.clone();}

usestd::fmt::Debug;

fnfoo<T:Clone+Debug>(x:T){x.clone();println!("{:?}",x);}

usestd::fmt::Debug;

fnfoo<T:Clone,K:Clone+Debug>(x:T,y:K){x.clone();y.clone();println!("{:?}",y);}

ЯзыкпрограммированияRust

240Типажи

Page 241: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Естьрешениеидляэтойпроблемы,иононазывается«утверждениеwhere»:

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

Такаягибкостьможетдобавитьясностивсложныхситуациях.

На самом делеwhere не толькоупрощаетнаписание, этоболеемощнаявозможность.Например:

usestd::fmt::Debug;

fnfoo<T:Clone,K:Clone+Debug>(x:T,y:K){x.clone();y.clone();println!("{:?}",y);}

fnbar<T,K>(x:T,y:K)whereT:Clone,K:Clone+Debug{x.clone();y.clone();println!("{:?}",y);}

fnmain(){foo("Привет","мир");bar("Привет","мир");}

usestd::fmt::Debug;

fnbar<T,K>(x:T,y:K)whereT:Clone,K:Clone+Debug{

x.clone();y.clone();println!("{:?}",y);}

ЯзыкпрограммированияRust

241Типажи

Page 242: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этот код демонстрирует дополнительные преимущества использования утвержденияwhere :онопозволяетзадаватьограничение,гдеслевойсторонырасполагаетсяпроизвольныйтип(вданномслучаеi32 ),анетолькопростойпараметртипа(вродеT ).

МетодыпоумолчаниюЕстьещеоднаособенностьтипажей,окоторойстоитпоговорить:методыпоумолчанию.

Прощевсегопоказатьэтонапримере:

В типах, реализующих типажFoo , нужно реализовать методis_valid() , аis_invalid() будетреализованпо-умолчанию.Егоповедениеможнопереопределить:

traitConvertTo<Output>{fnconvert(&self)->Output;}

implConvertTo<i64>fori32{fnconvert(&self)->i64{*selfasi64}}

//можетбытьвызвансT==i32fnnormal<T:ConvertTo<i64>>(x:&T)->i64{x.convert()}

//можетбытьвызвансT==i64fninverse<T>()->T//используетConvertToкакеслибыэтобыло«ConvertFrom<i32>»wherei32:ConvertTo<T>{1i32.convert()}

traitFoo{fnis_valid(&self)->bool;

fnis_invalid(&self)->bool{!self.is_valid()}}

ЯзыкпрограммированияRust

242Типажи

Page 243: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

НаследованиеИногда чтобы реализовать один типаж, нужно реализовать типажи, от которых он

зависит:

Типы,реализующиеFooBar ,должныреализовыватьFoo :

structUseDefault;

implFooforUseDefault{fnis_valid(&self)->bool{println!("ВызванUseDefault.is_valid.");true}}

structOverrideDefault;

implFooforOverrideDefault{fnis_valid(&self)->bool{println!("ВызванOverrideDefault.is_valid.");true}

fnis_invalid(&self)->bool{println!("ВызванOverrideDefault.is_invalid!");true//этареализацияпротиворечитсамасебе!}}

letdefault=UseDefault;assert!(!default.is_invalid());//печатает«ВызванUseDefault.is_valid.»

letover=OverrideDefault;assert!(over.is_invalid());//печатает«ВызванOverrideDefault.is_invalid!»

traitFoo{fnfoo(&self);}

traitFooBar:Foo{fnfoobar(&self);}

structBaz;

implFooforBaz{fnfoo(&self){println!("foo");}}

implFooBarforBaz{fnfoobar(&self){println!("foobar");}}

ЯзыкпрограммированияRust

243Типажи

Page 244: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЕслимызабудемреализоватьFoo ,компиляторскажетнамобэтом:

error:thetrait`main::Foo`isnotimplementedforthetype`main::Baz`[E0277]

ЯзыкпрограммированияRust

244Типажи

Page 245: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Типаж`Drop`(сброс)Мыобсудилитипажи.Теперьдавайтепоговоримоконкретномтипаже,предоставляемом

стандартной библиотекой Rust. Этот типаж  —Drop (сброс)  — позволяет выполнитьнекоторыйкод,когдазначениевыходитизобластивидимости.Например:

Когдаx выходит из области видимости в концеmain() , исполнится код реализациитипажаDrop . У него один метод, который тоже называетсяdrop() . Он принимаетизменяемуюссылкунасебя(self ).

Вот и всё! РаботаDrop достаточно проста, но есть несколько тонкостей. Например,значениясбрасываютсявпорядке,обратномпорядкуихобъявления.Вотещёпример:

Этоткодвыведетследующее:

БАБАХсилой100!!!БАБАХсилой1!!!

Сначала взрывается тринитротолуоловаябомба (tnt ), потому что она была объявленапоследней.Занейвзрываетсяшутиха(firecracker ).Первымвошёл,последнимвышел.

structHasDrop;

implDropforHasDrop{fndrop(&mutself){println!("Сбрасываем!");}}

fnmain(){letx=HasDrop;

//сделаемчто-то

}//тутxвыходитизобластивидимости

structFirework{strength:i32,}

implDropforFirework{fndrop(&mutself){println!("БАБАХсилой{}!!!",self.strength);}}

fnmain(){letfirecracker=Firework{strength:1};lettnt=Firework{strength:100};}

ЯзыкпрограммированияRust

245Типаж`Drop`(сброс)

Page 246: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Так зачем нуженDrop? ЧастоDrop используют, чтобы освободить ресурсы,представленныеструктурой(struct ).Например,счётчикссылокArc<T> уменьшаетчислоактивныхссылоквdrop() ,икогдаонодостигаетнуля,освобождаетхранимоезначение.

ЯзыкпрограммированияRust

246Типаж`Drop`(сброс)

Page 247: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Конструкция`iflet`Иногдахочетсясделатьопределённыевещименеенеуклюже.Например,скомбинировать

if иlet чтобыболееудобносделатьсопоставлениесобразцом.Дляэтогоестьiflet .

В качестве примера рассмотримOption<T> . Если этоSome<T> , мы хотим вызватьфункциюнаэтомзначении,аеслиэтоNone  —неделатьничего.Вродетакого:

Здесьнеобязательноиспользоватьmatch .if тожеподойдёт:

Нообаэтихвариантавыглядятстранно.Мыможемисправитьэтоспомощьюiflet :

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

Есливыхотитеделатьчто-тоещёпринесовпадениисобразцом,используйтеelse :

whileletПохожим образом,whilelet можно использовать для перебора значений, пока они

соответствуютобразцу.Кодвродетакого:

Превращаетсявтакой:

matchoption{Some(x)=>{foo(x)},None=>{},}

ifoption.is_some(){letx=option.unwrap();foo(x);}

ifletSome(x)=option{foo(x);}

ifletSome(x)=option{foo(x);}else{bar();}

loop{matchoption{Some(x)=>println!("{}",x),_=>break,}}

ЯзыкпрограммированияRust

247Конструкция`iflet`

Page 248: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

whileletSome(x)=option{println!("{}",x);}

ЯзыкпрограммированияRust

248Конструкция`iflet`

Page 249: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Типажи-объектыКогдакодвключаетвсебяполиморфизм,тодолженбытьмеханизм,чтобыопределить,

какаяконкретнаяверсиябудетфактическивызвана.Этоназывается'диспетчеризация.'Естьдвеосновные формы диспетчеризации: статическая и динамическая. Хотя Rust и отдаетпредпочтение статической диспетчеризации, он также поддерживает динамическуюдиспетчеризациючерезмеханизм,называемый'типажи-объекты.'

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

Давайте создадим простой типажFoo . Он содержит один метод, который возвращаетString .

Такжемыреализуемэтоттипаждляu8 иString :

СтатическаядиспетчеризацияМы можем использовать этот типаж для выполнения статической диспетчеризации с

помощьюограничениятипажом:

Здесь Rust использует 'мономорфизацию' для статической диспетчеризации. Этоозначает,чтоRustсоздастспециальнуюверсиюdo_something() длякаждогоизтипов:u8иString ,азатемзаменитвсеместавызововнавызовыэтихспециализированныхфункций.Другимисловами,Rustсгенерируетнечтовродеэтого:

traitFoo{fnmethod(&self)->String;}

implFooforu8{fnmethod(&self)->String{format!("u8:{}",*self)}}

implFooforString{fnmethod(&self)->String{format!("string:{}",*self)}}

fndo_something<T:Foo>(x:T){x.method();}

fnmain(){letx=5u8;lety="Hello".to_string();

do_something(x);do_something(y);}

ЯзыкпрограммированияRust

249Типажи-объекты

Page 250: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Кроме того, компиляторы не совершенны и могут «оптимизировать» код так, что онстанет медленнее. Например, встроенные функции будут слишком охотно раздувать кэшкоманд(правилакэшированиявсевокругнас).Этооднаизпричин,покоторой#[inline] и#[inline(always)] следует использовать осторожно, и почему использованиединамическойдиспетчеризациииногдаболееэффективно.

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

ДинамическаядиспетчеризацияRust обеспечивает динамическую диспетчеризацию через механизм под названием

'типажи-объекты'. Типажи-объекты, такие как&Foo илиBox<Foo> , это обычныепеременные, хранящие значениялюбого типа,реализующегоданныйтипаж.Конкретныйтиптипажа-объектаможетбытьопределентольконаэтапевыполнения.

Типаж-объектможетбытьполученизуказателянаконкретныйтип,которыйреализуетэтот типаж, путем егоявного приведения (например,&x as &Foo ) илинеявногоприведения (например, используя&x в качестве аргумента функции, которая принимает&Foo ).

fndo_something_u8(x:u8){x.method();}

fndo_something_string(x:String){x.method();}

fnmain(){letx=5u8;lety="Hello".to_string();

do_something_u8(x);do_something_string(y);}

ЯзыкпрограммированияRust

250Типажи-объекты

Page 251: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Явное и неявное приведение типажа-объекта также работает для таких указателей, как&mut T в&mut Foo иBox<T> вBox<Foo> , но это все на данный момент. Явное инеявноеприведениеидентичны.

Этаоперацияможетрассматриватьсякак«затирание»знаниякомпилятораоконкретномтипеуказателя,поэтомутипажи-объектыиногданазывают«затираниемтипов».

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

илинеявногоприведениятипа:

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

Почемууказатели?Вотличиеотмногихуправляемыхязыков,Rustпоумолчаниюнеразмещаетзначенияпо

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

ДляFoo допускаетсяиметьзначение,котороеможетбытьлибоString (24байт),либоu8 (1 байт), либо любой другой тип, для которого в соответствующих крейтах может бытьреализованFoo (возможноабсолютнолюбоечислобайт).Таккакэтотдругойтипможетбытьсколь угодно большими, то нет никакого способа, гарантирующего, что последний вариантбудетработать,еслизначениясохраняютсябезуказателя.

fndo_something(x:&Foo){x.method();}

fnmain(){letx=5u8;do_something(&xas&Foo);}

fndo_something(x:&Foo){x.method();}

fnmain(){letx="Hello".to_string();do_something(&x);}

ЯзыкпрограммированияRust

251Типажи-объекты

Page 252: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

ПредставлениеМетоды типажа можно вызвать для типажа-объекта с помощью специальной записи

указателейнафункции, традиционноназываемой 'виртуальнаятаблица' ('vtable') (создаетсяиуправляетсякомпилятором).

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

Давайте начнем с простого, с рантайм представления типажа-объекта. Модульstd::raw содержит структуры с макетами, которые являются такими же, как и сложныевстроенныетипы,втомчислетипажи-объекты:

Тоестьтипаж-объект,такойкак&Foo ,состоитизуказателяна«данные»иуказателяна«виртуальнуютаблицу».

Указательdata адресует данные (какого-то неизвестного типаT ), которые храниттипаж-объект,ауказательvtable указываетнавиртуальнуютаблицу(«таблицавиртуальныхметодов»),котораясоответствуетреализацииFoo дляT .

По существу, виртуальная таблица  — это структура указателей на функции,указывающихнаконкретныйкусокмашинногокодадлякаждогометодавреализации.Вызовметода наподобиеtrait_object.method() возвращает правильный указатель извиртуальнойтаблицы,азатемдинамическивызываетметодпоэтомууказателю.Например:

pubstructTraitObject{pubdata:*mut(),pubvtable:*mut(),}

ЯзыкпрограммированияRust

252Типажи-объекты

Page 253: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Полеdestructor вкаждойвиртуальнойтаблицеуказываетнафункцию,котораябудеточищать любые ресурсы типа этой виртуальной таблицы, дляu8 она тривиальна, но дляString она будет освобождать память. Это необходимо для владельцев типажей-объектов,такихкакBox<Foo> ,длякоторыхнеобходимоочищатьвыделеннуюпамятькакдляBox ,такидлявнутреннеготипа,когдаонивыходятизобластивидимости.Поляsize иalign хранят

structFooVtable{destructor:fn(*mut()),size:usize,align:usize,method:fn(*const())->String,}

//u8:

fncall_method_on_u8(x:*const())->String{//компиляторгарантирует,чтоэтафункциявызываетсятолько//с`x`,указывающимнаu8letbyte:&u8=unsafe{&*(xas*constu8)};

byte.method()}

staticFoo_for_u8_vtable:FooVtable=FooVtable{destructor:/*магиякомпилятора*/,size:1,align:1,

//преобразованиевуказательнафункциюmethod:call_method_on_u8asfn(*const())->String,};

//String:

fncall_method_on_String(x:*const())->String{//компиляторгарантирует,чтоэтафункциявызываетсятолько//с`x`,указывающимнаStringletstring:&String=unsafe{&*(xas*constString)};

string.method()}

staticFoo_for_String_vtable:FooVtable=FooVtable{destructor:/*магиякомпилятора*/,//значениядля64-битногокомпьютера,для32-битногоонив2разаменьшеsize:24,align:8,

method:call_method_on_Stringasfn(*const())->String,};

ЯзыкпрограммированияRust

253Типажи-объекты

Page 254: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

размерзатёртоготипа,иеготребованияквыравниванию;посуществу,онинеиспользовалсявмомент, так как информация встроенного в деструктор, но будет использоваться в будущем,таккакобъектыотличительнымпризнакампостепенностановитсяболеегибким.

Предположим,унасестьнесколькозначений,которыереализуютFoo ,тогдаявныйвидсоздания и использования типажей-объектовFoo может выглядеть примерно как(игнорируютсянесоответствиятипов:влюбомслучае,онивсеголишьуказатели):

leta:String="foo".to_string();letx:u8=1;

//letb:&Foo=&a;letb=TraitObject{//storethedatadata:&a,//storethemethodsvtable:&Foo_for_String_vtable};

//lety:&Foo=x;lety=TraitObject{//storethedatadata:&x,//storethemethodsvtable:&Foo_for_u8_vtable};

//b.method();(b.vtable.method)(b.data);

//y.method();(y.vtable.method)(y.data);

ЯзыкпрограммированияRust

254Типажи-объекты

Page 255: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЗамыканияПомимо именованных функций Rust предоставляет еще и анонимные функции.

Анонимныефункции,которыеимеютсвязанноеокружение,называются 'замыкания'.Онитакназываются потому что они замыкают свое окружение. Как мы увидим далее, Rust имеетреальнокрутуюреализациюзамыканий.

СинтаксисЗамыканиявыглядятследующимобразом:

Мы создаем связывание,plus_one , и присваиваем ему замыкание. Аргументызамыканиярасполагаютсямеждудвумясимволами| ,ателомзамыканияявляетсявыражение,в данном случае:x + 1 . Помните, что{ } также является выражением, поэтому телозамыканияможетсодержатьмногострок:

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

Но мы не должны. Почему так? В основном, это было сделано из эргономическихсоображений (соображений удобства). В то время как для именованных функций явноеуказание типа является полезным для таких аспектов как документация и вывод типа, типызамыканий редко документируют, поскольку они анонимны. К тому же, они не вызывают«ошибокнарасстоянии» (error-at-a-distance),которыемогутвызыватьименованныефункции.Такие ошибки могут возникать, когда локальное изменение (например, в теле одной изфункций) вызывает изменение вывода типов. Компилятор пытается подобрать типы в

letplus_one=|x:i32|x+1;

assert_eq!(2,plus_one(1));

letplus_two=|x|{letmutresult:i32=x;

result+=1;result+=1;

result};

assert_eq!(4,plus_two(2));

letplus_one=|x:i32|->i32{x+1};

assert_eq!(2,plus_one(1));

ЯзыкпрограммированияRust

255Замыкания

Page 256: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

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

Естьнебольшиеразличия,нопринципаналогичен.

ЗамыканияиихокружениеЗамыкания называются так потому, что они 'замыкают свое окружение.' Это выглядит

следующимобразом:

Это замыкание,plus_num , ссылается на связанную с помощью оператораletпеременнуюnum ,расположеннуювсвоейобластивидимости.Еслиговоритьболееконкретно,тоонозаимствуетсвязывание.Еслимысделаемчто-то,чтопротиворечилобысвязыванию,тополучимошибку.Напримерэтоткод:

Которыйвыдастследующиеошибки:

error:cannotborrow`num`asmutablebecauseitisalsoborrowedasimmutablelety=&mutnum;^~~note:previousborrowof`num`occurshereduetouseinclosure;theimmutableborrowpreventssubsequentmovesormutableborrowsof`num`untiltheborrowendsletplus_num=|x|x+num;^~~~~~~~~~~note:previousborrowendsherefnmain(){letmutnum=5;letplus_num=|x|x+num;lety=&mutnum;}^

fnplus_one_v1(x:i32)->i32{x+1}letplus_one_v2=|x:i32|->i32{x+1};letplus_one_v3=|x:i32|x+1;

letnum=5;letplus_num=|x:i32|x+num;

assert_eq!(10,plus_num(5));

letmutnum=5;letplus_num=|x:i32|x+num;

lety=&mutnum;

ЯзыкпрограммированияRust

256Замыкания

Page 257: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Подробное и к тому же полезное сообщение об ошибке! Как говорится в этомсообщении,мынеможемполучитьизменяемыйзаемпеременнойnum потомучтозамыканиеуже заимствует его. Если же мы обеспечим выход замыкания из области видимости, то мысможем:

Однако,Rustтакжеможетзабиратьправовладенияиперемещатьсвоеокружение,еслиэтоготребуетзамыкание:

Этоткодвыдаст:

note:`nums`movedintoclosureenvironmentherebecauseithastype`[closure(())->collections::vec::Vec<i32>]`,whichisnon-copyablelettakes_nums=||nums;^~~~~~~

Vec<T> обладаетправомвладениянасвоесодержимое,ипоэтому,когдамыссылаемсянанеговнашемзамыкании,мыдолжнызабратьправовладениянаnums .Этотожесамое,какеслибымыпередавалиnums вфункцию,котораязабиралабыправовладениянанего.

Перемещающиезамыкания(move closures)Мы можем заставить наше замыкание забирать право владения на свое окружение с

помощьюключевогословаmove :

Теперь, когда указано ключевое словоmove , переменные следуют нормальнойсемантике перемещения. В данном примере5 реализуетCopy , поэтомуowns_numстановитсявладельцемкопииnum .Таквчемжеразница?

letmutnum=5;{letplus_num=|x:i32|x+num;

}//plus_numgoesoutofscope,borrowofnumends

lety=&mutnum;

letnums=vec![1,2,3];

lettakes_nums=||nums;

println!("{:?}",nums);

letnum=5;

letowns_num=move|x:i32|x+num;

ЯзыкпрограммированияRust

257Замыкания

Page 258: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Итак, в этом примере наше замыкание принимает изменяемую ссылку наnum . Затем,когда мы вызываем замыканиеadd_num , то, как мы и ожидали, оно изменяет значениевнутри. Нам также необходимо объявитьadd_num какmut , потому что оно изменяет своеокружение.

Еслижемыбудемиспользоватьmove замыкание,тополучимследующиеотличия:

Мывсеголишьполучаем5 .Вместотого,чтобыполучатьизменяемыйзаемнаnum ,мыполучаемправовладениянакопию.

Вот еще один способ думать оmove замыканиях: они предоставляют замыкание сосвоим собственным фреймом стека. Безmove замыкание может быть связано с фреймомстека,которыйегосоздал,втовремякакmove замыканиесодержитсвойсобственныйфреймстека.Этоозначает,например,чтовынеможетевернутьнеmove замыканиеизфункции.

Нопрежде чем говорить о получении в качестве аргумента и возвращении замыкания,мы должны поговорить о том, как реализуются замыкания. Как системный языкпрограммирования,Rustдаетвамкучуконтролянадтем,чтоделаетвашкод,изамыканиянеявляютсяисключением.

РеализациязамыканийРеализация замыканий в Rust немного отличается от других языков. Фактически, она

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

Изучили?Хорошо.

letmutnum=5;

{letmutadd_num=|x:i32|num+=x;

add_num(5);}

assert_eq!(10,num);

letmutnum=5;

{letmutadd_num=move|x:i32|num+=x;

add_num(5);}

assert_eq!(5,num);

ЯзыкпрограммированияRust

258Замыкания

Page 259: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Выможетезаметитьнекоторыеразличиямеждуэтимитипажами,ноестьодноглавноеразличие  —self : Fn принимает&self , FnMut принимает&mut self , FnOnceпринимаетself .Этопокрываетвсетривидаself спомощьюобычногосинтаксисавызоваметодов. Мы разделили их на три типажа, вместо того, чтобы иметь один. Это дает намбольшееколичествоконтролянадтем,какоговидазамыканиямыможемпринять.

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

ПередачазамыканийвкачествеаргументовТеперь, когда мы знаем, что замыкания являются типажами, получается, что мы уже

знаем,какприниматьивозвращатьзамыкания:какилюбойдругойтипаж!

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

pubtraitFn<Args>:FnMut<Args>{extern"rust-call"fncall(&self,args:Args)->Self::Output;}

pubtraitFnMut<Args>:FnOnce<Args>{extern"rust-call"fncall_mut(&mutself,args:Args)->Self::Output;}

pubtraitFnOnce<Args>{typeOutput;

extern"rust-call"fncall_once(self,args:Args)->Self::Output;}

fncall_with_one<F>(some_closure:F)->i32whereF:Fn(i32)->i32{

some_closure(1)}

letanswer=call_with_one(|x|x+2);

assert_eq!(3,answer);

ЯзыкпрограммированияRust

259Замыкания

Page 260: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мы передаем наше замыкание|x| x + 2 , в функциюcall_with_one . Она жеделает то, о чем говорит ее название: вызывает замыкание, передавая ему1 в качествеаргумента.

Давайтерассмотримсигнатуруфункцииcall_with_one болееподробно:

Мыпринимаемодинпараметр,которыйимееттипF .Мытакжевозвращаемi32 .Этачастьнеинтересна.Следующимважныммоментомявляется:

Так какFn является типажом,мыможемсвязать снимнашобобщенныйпараметр.Вэтом примере, замыкание принимаетi32 в качестве аргумента и возвращаетi32 , поэтомусвязывание,котороемыиспользуем,выглядиттак:Fn(i32)->i32 .

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

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

Теперь наша функция в качетве аргумента принимает типаж-объект&Fn .Поэтомумыдолжнысоздатьссылкуназамыканиеазатемпередатьеевфункциюcall_with_one , дляэтогомыиспользуем&|| .

ВозвратзамыканийЧто очень характерно для кода в функциональном стиле  — возвращать замыкания в

различных ситуациях. Если вы попытаетесь вернуть замыкание, то можете столкнуться сошибкой. Сперва это может показаться странным, но мы с этим разберемся. Вот как вы,наверное,попытаетесьвернутьзамыканиеизфункции:

fncall_with_one<F>(some_closure:F)->i32

whereF:Fn(i32)->i32{

fncall_with_one(some_closure:&Fn(i32)->i32)->i32{some_closure(1)}

letanswer=call_with_one(&|x|x+2);

assert_eq!(3,answer);

ЯзыкпрограммированияRust

260Замыкания

Page 261: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этовыдастследующиедлинные,взаимосвязанныеошибки:

error:thetrait`core::marker::Sized`isnotimplementedforthetype`core::ops::Fn(i32)->i32`[E0277]fnfactory()->(Fn(i32)->i32){^~~~~~~~~~~~~~~~note:`core::ops::Fn(i32)->i32`doesnothaveaconstantsizeknownatcompile-timefnfactory()->(Fn(i32)->i32){^~~~~~~~~~~~~~~~error:thetrait`core::marker::Sized`isnotimplementedforthetype`core::ops::Fn(i32)->i32`[E0277]letf=factory();^note:`core::ops::Fn(i32)->i32`doesnothaveaconstantsizeknownatcompile-timeletf=factory();^

Длятогочтобывернутьчто-тоизфункции,Rustдолжензнать,какойразмеримееттипвозвращаемогозначения.НотаккакFn являетсятипажом,товкачественегомогутвыступатьсовершенноразныеобъекты,сразнымиразмерами:многоразличныхтиповмогутреализоватьFn .Самыйпростой способпередатьчто-тонеопределенногоразмера —передать ссылкунаэто что-то, так как ссылки имеют известный размер. Таким образом, следовало бынаписатьтак:

Нотогдамыполучимдругуюошибку:

error:missinglifetimespecifier[E0106]fnfactory()->&(Fn(i32)->i32){^~~~~~~~~~~~~~~~~

fnfactory()->(Fn(i32)->i32){letnum=5;

|x|x+num}

letf=factory();

letanswer=f(1);assert_eq!(6,answer);

fnfactory()->&(Fn(i32)->i32){letnum=5;

|x|x+num}

letf=factory();

letanswer=f(1);assert_eq!(6,answer);

ЯзыкпрограммированияRust

261Замыкания

Page 262: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Верно. Так как у нас используется ссылка, то мы должны задать ее времяжизни. Такнашафункцияfactory() непринимаетникакихаргументов,тоэлизия(сокрытие)здесьнеуместна.Какоевремяжизнимыдолжнывыбрать?'static :

Номыполучимещеошибку:

error:mismatchedtypes:expected`&'staticcore::ops::Fn(i32)->i32`,found`[closure<anon>:7:9:7:20]`(expected&-ptr,foundclosure)[E0308]|x|x+num^~~~~~~~~~~

Эта ошибка сообщает нам, что ожидается использование&'static Fn(i32) ->i32 ,аиспользуется[closure<anon>:7:9:7:20] .Подождите,что?

Посколькукаждоезамыкание(виндивидуальномпорядке)генерируетсвоюсобственнуюstruct дляокруженияиреализуетFn икомпанию,тоэтитипыявляютсяанонимными.Онисуществуютисключительнодляэтогозамыкания.ПоэтомуRustпоказываетихкакclosure<anon> ,аневвидекакого-тоавтоматическисгенерированногоимени.

Нопочемуженашезамыканиенереализует&'staticFn?Какмыобсуждалиранее,замыкание заимствуетсвоеокружение.Ив этомслучаенашеокружениепредставляет собойвыделенуювстекепамять,содержащуюзначениесвязаннойпеременнойnum -5 .Из-заэтогозаем имеет срок жизни фрейма стека. Так что, когда мы вернем это замыкание, то вызовфункции будет завершен, а фрейм стека уйдет, и наше замыкание захватит окружение,содержащеевпамятимусор!

Такчтожеделать?Этоткодпочтиработает:

fnfactory()->&'static(Fn(i32)->i32){letnum=5;

|x|x+num}

letf=factory();

letanswer=f(1);assert_eq!(6,answer);

fnfactory()->Box<Fn(i32)->i32>{letnum=5;

Box::new(|x|x+num)}letf=factory();

letanswer=f(1);assert_eq!(6,answer);

ЯзыкпрограммированияRust

262Замыкания

Page 263: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мыиспользуемтипаж-объект,полученныйврезультатеупаковки (Box ) типажаFn .Иостаётсятолькоодна,последняяпроблема:

error:closuremayoutlivethecurrentfunction,butitborrows`num`,whichisownedbythecurrentfunction[E0373]Box::new(|x|x+num)^~~~~~~~~~~

Мывсе ещепо-прежнему ссылаемся на родительскийфрейм стека.С этимпоследнимисправлениеммысможемнаконецвыполнитьнашузадачу:

Благодаря изменению внутреннего замыкания наmove Fn будет создаваться новыйфрейм стека для нашего замыкания. А благодаря упаковке (Box ) замыкания, получаетсяизвестныйразмервозвращаемогозначения,ипозволяетемуизбежать(бытьнезависимымот)нашегофреймастека.

fnfactory()->Box<Fn(i32)->i32>{letnum=5;

Box::new(move|x|x+num)}letf=factory();

letanswer=f(1);assert_eq!(6,answer);

ЯзыкпрограммированияRust

263Замыкания

Page 264: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Универсальный синтаксис вызова функций(universalfunctioncallsyntax)

Иногда,функциимогутиметьодинаковыеимена.Рассмотримэтоткод:

Еслимыпопытаемсявызватьb.f() ,тополучимошибку:

error:multipleapplicablemethodsinscope[E0034]b.f();^~~note:candidate#1isdefinedinanimplofthetrait`main::Foo`forthetype`main::Baz`fnf(&self){println!("Baz’simplofFoo");}^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~note:candidate#2isdefinedinanimplofthetrait`main::Bar`forthetype`main::Baz`fnf(&self){println!("Baz’simplofBar");}^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Нам нужен способ указать, какой конкретно метод нужен, чтобы устранитьнеоднозначность.Этавозможностьназывается«универсальныйсинтаксисвызовафункций»,ивыглядитэтотак:

Давайтеразберемся.

traitFoo{fnf(&self);}

traitBar{fnf(&self);}

structBaz;

implFooforBaz{fnf(&self){println!("Baz’simplofFoo");}}

implBarforBaz{fnf(&self){println!("Baz’simplofBar");}}

letb=Baz;

Foo::f(&b);Bar::f(&b);

Foo::Bar::

ЯзыкпрограммированияRust

264Универсальныйсинтаксисвызовафункций(universalfunctioncallsyntax)

Page 265: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этичастивызовазадаютодиниздвухвидовтипажей:Foo иBar .Этото,чтонасамомделеустраняетнеоднозначностьмеждудвумяметодами:Rustвызываетметодтоготипажа,имякотороговыиспользуете.

Когда мы вызываем метод, используясинтаксис вызова метода, как напримерb.f() ,Rustавтоматическизаимствуетb , еслиf() принимаетвкачествеаргумента&self .Вэтомжеслучае,Rustнебудетиспользовать автоматическое заимствование,ипоэтомумыдолжныявнопередать&b .

ФормасугловымискобкамиФормаUFCS,окотороймытолькочтоговорили:

Этосокращеннаяформазаписи.Нижепредставленарасширеннаяформазаписи,котораятребуетсявнекоторыхситуациях:

Синтаксис<>:: являетсясредствомпредоставленияподсказкитипа.Типрасполагаетсявнутри<> . В этом случае типом являетсяTypeasTrait , указывающий, что мы хотимздесь вызватьTrait версиюметода.ЧастьasTrait являетсянеобязательной,есливызовнеявляетсянеоднозначным.Тожесамоечтосугловымискобками,отсюдаикороткаяформа.

Вотпримериспользованиядлиннойформызаписи.

Этоткодвызываетметодclone() типажаClone ,анетипажаFoo .

f(&b)

Trait::method(args);

<TypeasTrait>::method(args);

traitFoo{fnclone(&self);}

#[derive(Clone)]structBar;

implFooforBar{fnclone(&self){println!("MakingacloneofBar");

<BarasClone>::clone(self);}}

ЯзыкпрограммированияRust

265Универсальныйсинтаксисвызовафункций(universalfunctioncallsyntax)

Page 266: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Контейнеры(crates)имодули(modules)Когда проект начинает разрастаться, то хорошей практикой разработки программного

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

Основныетермины:контейнерыимодулиRustимеетдваразличныхтермина,которыеотносятсякмодульнойсистеме:контейнери

модуль.Контейнер —этосинонимбиблиотекиилипакетанадругихязыках.ИменнопоэтомуинструментуправленияпакетамивRustназываетсяCargo:выпересылаетевашиконтейнерыдругимспомощьюCargo.Контейнерымогутпроизводитьисполняемыйфайлилибиблиотеку,взависимостиотпроекта.

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

Вкачествепримера,давайтесделаемконтейнерphrases,которыйвыдаетнамразличныефразынаразныхязыках.Чтобынеусложнятьпример,мыбудемиспользоватьдвавидафраз:«greetings»и«farewells»,идваязыкадляэтихфраз:английскийияпонский(���).Мыбудемиспользоватьследующийшаблонмодуля:

+-----------++---|greetings||+-----------++---------+|+---|english|---+|+---------+|+-----------+|+---|farewells|+---------+|+-----------+|phrases|---++---------+|+-----------+|+---|greetings||+----------+|+-----------++---|japanese|--++----------+||+-----------++---|farewells|+-----------+

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

ЯзыкпрограммированияRust

266Контейнеры(crates)имодули(modules)

Page 267: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Теперь, когдаунас есть схема, давайтеопределиммодулив коде.Дляначала создайтеновыйконтейнерспомощьюCargo:

$cargonewphrases$cdphrases

Есливыпомните,тоэтакомандасоздаетпростойпроект:

$tree..├──Cargo.toml└──src└──lib.rs

1directory,2files

src/lib.rs   — корень нашего контейнера, соответствующийphrases в нашейдиаграммевыше.

ОбъявлениемодулейДля объявления каждого из наших модулей, мы используем ключевое словоmod .

Давайтесделаем,чтобынашsrc/lib.rs выгляделследующимобразом:

После ключевого словаmod , вы задаете имя модуля. Имена модулей следуютсоглашениям,какидругиеидентификаторыRust:lower_snake_case .Содержаниекаждогомодуляобрамляетсявфигурныескобки({} ).

Внутриmod вы можете объявить суб-mod . Мы можем обращаться к суб-модулям спомощью нотации (:: ). Так выглядят обращения к нашим четырем вложенным модулям:english::greetings , english::farewells , japanese::greetings иjapanese::farewells . Так как суб-модули располагаются в пространстве имен своихродительскихмодулей, то суб-модулиenglish::greetings иjapanese::greetingsнеконфликтуют,несмотрянато,чтоониимеютодинаковыеимена,greetings .

modenglish{modgreetings{}

modfarewells{}}

modjapanese{modgreetings{}

modfarewells{}}

ЯзыкпрограммированияRust

267Контейнеры(crates)имодули(modules)

Page 268: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Так как в этом контейнере нет функцииmain() , и называется онlib.rs , Cargoсоберетэтотконтейнерввидебиблиотеки:

$cargobuildCompilingphrasesv0.0.1(file:///home/you/projects/phrases)$lstarget/debugbuilddepsexampleslibphrases-a7448e02a0468eaa.rlibnative

libphrase-hash.rlib   — это скомпилированный контейнер. Прежде чем мырассмотрим, как его можно использовать из другого контейнера, давайте разобьем его нанесколькофайлов.

КонтейнерыснесколькимифайламиЕслибыкаждыйконтейнермогсостоятьтолькоизодногофайла,тогдаэтотфайлбылбы

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

Вместообъявлениямодулянаподобие:

Мыможемобъявитьнашмодульввиде:

Еслимыэтосделаем,тоRustбудетожидать,чтонайдетлибофайлenglish.rs ,либофайлenglish/mod.rs ссодержимымнашегомодуля.

Обратите внимание, что в этихфайлах вам не требуется заново объявлятьмодуль: этоужесделаноприизначальномобъявленииmod .

Спомощьюэтихдвухприемовмыможемразбитьнашконтейнернадведиректорииисемьфайлов:

modenglish{//contentsofourmodulegohere}

modenglish;

ЯзыкпрограммированияRust

268Контейнеры(crates)имодули(modules)

Page 269: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$tree..├──Cargo.lock├──Cargo.toml├──src│├──english││├──farewells.rs││├──greetings.rs││└──mod.rs│├──japanese││├──farewells.rs││├──greetings.rs││└──mod.rs│└──lib.rs└──target└──debug├──build├──deps├──examples├──libphrases-a7448e02a0468eaa.rlib└──native

src/lib.rs  —кореньнашегоконтейнера,ивыглядитонследующимобразом:

Эти два объявления информируют Rust, что следует искать:src/english.rs илиsrc/english/mod.rs , src/japanese.rs илиsrc/japanese/mod.rs , взависимостиотнашейструктуры.Вданномпримеремывыбраливторойвариантиз-за того,что наши модули содержат суб-модули. Иsrc/english/mod.rs иsrc/japanese/mod.rs выглядятследующимобразом:

В свою очередь, эти объявления информируют Rust, что следует искать:src/english/greetings.rs , src/japanese/greetings.rs ,src/english/farewells.rs , src/japanese/farewells.rs илиsrc/english/greetings/mod.rs , src/japanese/greetings/mod.rs ,src/english/farewells/mod.rs , src/japanese/farewells/mod.rs .Таккакэтисуб-модули не содержат свои собственные суб-модули, то мы выбралиsrc/english/greetings.rs иsrc/japanese/farewells.rs .Воттак!

Содержаниеsrc/english/greetings.rs иsrc/japanese/farewells.rsявляютсяпустыминаданныймомент.Давайтедобавимнесколькофункций.

Поместитеследующийкодвsrc/english/greetings.rs :

modenglish;modjapanese;

modgreetings;modfarewells;

fnhello()->String{"Hello!".to_string()}

ЯзыкпрограммированияRust

269Контейнеры(crates)имодули(modules)

Page 270: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Следующийкодвsrc/english/farewells.rs :

Следующийкодвsrc/japanese/greetings.rs :

Конечно, вы можете скопировать и вставить этот код с этой страницы, или простонапечатать что-нибудь еще. Вам совершенно не обязательно знать, что на японском языкенаписано«Konnichiwa»,чтобыпонятькакработаетмодульнаясистема.

Поместитеследующийкодвsrc/japanese/farewells.rs :

(Это«Sayonara»,есливаминтересно.)

Теперьунасестьнекотораяфункциональностьвнашемконтейнере,давайтепопробуемиспользоватьегоиздругогоконтейнера.

ИмпортвнешнихконтейнеровУнасестьбиблиотечныйконтейнер.Давайтесоздадимисполняемыйконтейнер,который

импортируетииспользуетнашубиблиотеку.

Создайте файлsrc/main.rs и положите в него следующее: (при этом он не будеткомпилироваться)

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

fngoodbye()->String{"Goodbye.".to_string()}

fnhello()->String{"�����".to_string()}

fngoodbye()->String{"�����".to_string()}

externcratephrases;

fnmain(){println!("HelloinEnglish:{}",phrases::english::greetings::hello());println!("GoodbyeinEnglish:{}",phrases::english::farewells::goodbye());

println!("HelloinJapanese:{}",phrases::japanese::greetings::hello());println!("GoodbyeinJapanese:{}",phrases::japanese::farewells::goodbye());}

ЯзыкпрограммированияRust

270Контейнеры(crates)имодули(modules)

Page 271: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Кроме того, Cargo предполагает, чтоsrc/main.rs   — это корень бинарного, а небиблиотечного контейнера. Теперь наш пакет содержит два контейнера:src/lib.rs иsrc/main.rs . Этот шаблон является довольно распространенным для исполняемыхконтейнеров: основная функциональность сосредоточена в библиотечном контейнере, аисполняемыйконтейнериспользуетэтубиблиотеку.Такимобразом,другиепрограммытакжемогутиспользоватьбиблиотечныйконтейнер,ктомужетакойподходобеспечиваетотделениеинтереса(разделениефункциональности).

Хотя этот код все еще не работает. Мы получаем четыре ошибки, которые выглядятпримернотак:

$cargobuildCompilingphrasesv0.0.1(file:///home/you/projects/phrases)src/main.rs:4:38:4:72error:function`hello`isprivatesrc/main.rs:4println!("HelloinEnglish:{}",phrases::english::greetings::hello());^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~note:inexpansionofformat_args!<stdmacros>:2:25:2:58note:expansionsite<stdmacros>:1:1:2:62note:inexpansionofprint!<stdmacros>:3:1:3:54note:expansionsite<stdmacros>:1:1:3:58note:inexpansionofprintln!phrases/src/main.rs:4:5:4:76note:expansionsite

Поумолчаниювсе элементывRust являютсяприватными.Давайтепоговоримоб этомболееподробно.

ЭкспортпубличныхинтерфейсовRust позволяет точно контролировать, какие элементы вашего интерфейса являются

публичными, и поэтому по умолчанию все элементы являются приватными. Чтобы сделатьэлементыпубличными,выиспользуетеключевоесловоpub .Давайтесначаласосредоточимсянамодулеenglish ,длячегосократимфайлsrc/main.rs доэтого:

В файлеsrc/lib.rs в объявлении модуляenglish давайтедобавиммодификаторpub :

Вфайлеsrc/english/mod.rs давайтесделаемобамодулясмодификаторомpub :

externcratephrases;

fnmain(){println!("HelloinEnglish:{}",phrases::english::greetings::hello());println!("GoodbyeinEnglish:{}",phrases::english::farewells::goodbye());}

pubmodenglish;modjapanese;

pubmodgreetings;pubmodfarewells;

ЯзыкпрограммированияRust

271Контейнеры(crates)имодули(modules)

Page 272: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

В файлеsrc/english/greetings.rs давайте добавим модификаторpub кобъявлениюнашейфункцииfn :

Атакжевфайлеsrc/english/farewells.rs :

Теперь наши контейнеры компилируются, хотя и с предупреждениями о том, чтофункциивмодулеjapanese неиспользуются:

$cargorunCompilingphrasesv0.0.1(file:///home/you/projects/phrases)src/japanese/greetings.rs:1:1:3:2warning:functionisneverused:`hello`,#[warn(dead_code)]onbydefaultsrc/japanese/greetings.rs:1fnhello()->String{src/japanese/greetings.rs:2"�����".to_string()src/japanese/greetings.rs:3}src/japanese/farewells.rs:1:1:3:2warning:functionisneverused:`goodbye`,#[warn(dead_code)]onbydefaultsrc/japanese/farewells.rs:1fngoodbye()->String{src/japanese/farewells.rs:2"�����".to_string()src/japanese/farewells.rs:3}Running`target/debug/phrases`HelloinEnglish:Hello!GoodbyeinEnglish:Goodbye.

Теперь,когдафункцииявляютсяпубличными,мыможемихиспользовать.Отлично!Темне менее, написаниеphrases::english::greetings::hello() является оченьдлинным и неудобным. Rust предоставляет другое ключевое слово, для импорта имен втекущую область, чтобы для обращенияможно было использовать короткие имена. Давайтепоговоримобэтомключевомслове,use .

ИмпортмодулейспомощьюuseRust предоставляет ключевое словоuse , которое позволяет импортировать имена в

нашу локальную область видимости. Давайте изменим файлsrc/main.rs , чтобы онвыгляделследующимобразом:

pubfnhello()->String{"Hello!".to_string()}

pubfngoodbye()->String{"Goodbye.".to_string()}

externcratephrases;

usephrases::english::greetings;usephrases::english::farewells;

fnmain(){println!("HelloinEnglish:{}",greetings::hello());println!("GoodbyeinEnglish:{}",farewells::goodbye());}

ЯзыкпрограммированияRust

272Контейнеры(crates)имодули(modules)

Page 273: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

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

Rustвыдастнамсообщениеобошибкевовремякомпиляции:

Compilingphrasesv0.0.1(file:///home/you/projects/phrases)src/main.rs:4:5:4:40error:avaluenamed`hello`hasalreadybeenimportedinthismodule[E0252]src/main.rs:4usephrases::japanese::greetings::hello;^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~error:abortingduetopreviouserrorCouldnotcompile`phrases`.

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

Выможетеиспользоватьсокращение:

Реэкспортспомощьюpubuse

externcratephrases;

usephrases::english::greetings::hello;usephrases::english::farewells::goodbye;

fnmain(){println!("HelloinEnglish:{}",hello());println!("GoodbyeinEnglish:{}",goodbye());}

externcratephrases;

usephrases::english::greetings::hello;usephrases::japanese::greetings::hello;

fnmain(){println!("HelloinEnglish:{}",hello());println!("HelloinJapanese:{}",hello());}

usephrases::english::greetings;usephrases::english::farewells;

usephrases::english::{greetings,farewells};

ЯзыкпрограммированияRust

273Контейнеры(crates)имодули(modules)

Page 274: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Давайтепосмотримнапримере.Изменитефайлsrc/main.rs следующимобразом:

Затемизменитефайлsrc/lib.rs ,чтобысделатьмодульjapanese спубличным:

Далее, убедитесь, что обе функции публичные, сперва вsrc/japanese/greetings.rs :

Азатемвsrc/japanese/farewells.rs :

Наконец,изменитефайлsrc/japanese/mod.rs воттак:

Объявлениеpubuse привносит указаннуюфункцию в эту часть области видимостинашей модулной иерархии. Так как мы использовалиpub use внутри нашего модуляjapanese , то теперь мы можем вызывать функциюphrases::japanese::hello() ифункциюphrases::japanese::goodbye() , хотя код для них расположен вphrases::japanese::greetings::hello() иphrases::japanese::farewells::goodbye() соответственно. Наша внутренняяорганизациянеопределяетнашвнешнийинтерфейс.

externcratephrases;

usephrases::english::{greetings,farewells};usephrases::japanese;

fnmain(){println!("HelloinEnglish:{}",greetings::hello());println!("GoodbyeinEnglish:{}",farewells::goodbye());

println!("HelloinJapanese:{}",japanese::hello());println!("GoodbyeinJapanese:{}",japanese::goodbye());}

pubmodenglish;pubmodjapanese;

pubfnhello()->String{"�����".to_string()}

pubfngoodbye()->String{"�����".to_string()}

pubuseself::greetings::hello;pubuseself::farewells::goodbye;

modgreetings;modfarewells;

ЯзыкпрограммированияRust

274Контейнеры(crates)имодули(modules)

Page 275: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вэтомпримеремыиспользуемpubuse отдельнодлякаждойфункции,которуюхотимпривнести в областьjapanese . В качестве альтернативы, мы могли бы использоватьшаблонныйсинтаксис,чтобывключатьвсебявсеэлементыизмодуляgreetings втекущуюобласть:pubuseself::greetings::* .

Что можно сказать оself? По умолчанию объявленияuse используют абсолютныепути,начинающиесяскорняконтейнера.self ,напротив,формируетэтипутиотносительнотекущегоместавиерархии.Уuse естьещеоднаособаяформа:выможетеиспользоватьusesuper:: , чтобы подняться по дереву на один уровень вверх от вашего текущегоместоположения.Некоторыепредпочитаютдуматьоself како. ,аоsuper како.. ,чтодля многих командных оболочек является представлением для текущей директории и дляродительскойдиректориисоответственно.

В н еuse , пути относительны:foo::bar() ссылаться на функцию внутриfooотносительнотого,гдемынаходимся.Еслижеиспользуетсяпрефикс:: , то::foo::bar()будетссылатьсянадругойfoo ,абсолютныйпутьотносительнокорняконтейнера.

Крометого,обратитевнимание,чтомыиспользовалиpubuse прежде, чемобъявилинашимодулиспомощьюmod .Rustтребует,чтобыобъявленияuse шливпервуюочередь.

Следующийкодсобираетсяиработает:

$cargorunCompilingphrasesv0.0.1(file:///home/you/projects/phrases)Running`target/debug/phrases`HelloinEnglish:Hello!GoodbyeinEnglish:Goodbye.HelloinJapanese:�����GoodbyeinJapanese:�����

ЯзыкпрограммированияRust

275Контейнеры(crates)имодули(modules)

Page 276: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

`const`и`static`ВRustможноопределитьпостояннуюспомощьюключевогословаconst :

В отличие от обычных имён, объявляемых с помощьюlet , тип постоянной надоуказыватьвсегда.

Постоянныеживутвтечениевсеговремениработыпрограммы.Аименно,унихвообщенетопределённогоадресавпамяти.Этопотому,чтоонивстраиваются(inline)вкаждоеместо,гдеестьихиспользование.Поэтойпричинессылкинаоднуитужепостояннуюнеобязаныуказыватьнаодинитотжеадресвпамяти.

staticВ Rust также можно объявить что-то вроде «глобальной переменной», используя

статическиезначения.Онипохожинапостоянные,ностатическиезначенияневстраиваютсявместо их использования. Это значит, что каждое значение существует в единственномэкземпляре,иунегоестьопределённыйадрес.

Вотпример:

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

Статическиезначенияживутвтечениевсеговремениработыпрограммы,илюбаяссылканапостояннуюимеетстатическоевремяжизни(static lifetime):

ИзменяемостьВыможетесделатьстатическоезначениеизменяемымспомощьюключевогословаmut :

ПосколькуN изменяемо, один поток может изменить его во время того, как другойчитает его значение. Это ситуация «гонки» по данным, и она считается небезопаснымповедением в Rust. Поэтому и чтение, и изменение статического изменяемого значения(staticmut ) являетсянебезопасным (unsafe), и обе эти операции должны выполняться внебезопасныхблоках(unsafe block):

constN:i32=5;

staticN:i32=5;

staticNAME:&'staticstr="Steve";

staticmutN:i32=5;

ЯзыкпрограммированияRust

276`const`и`static`

Page 277: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Более того, любой тип, хранимый в статической переменной, должен быть ограниченSync инеможетиметьреализацииDrop .

ИнициализацияИ постоянные, и статические значения имеют определённые требования к тому, что

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

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

место впамятии соответствующий ему адрес довольноредка.Аиспользованиепостоянныхпозволяет компилятору провести оптимизации вроде распространения постоянных (constantpropagation)нетольковвашемконтейнере,ноивтех,которыезависятотнего.

unsafe{N+=1;

println!("N:{}",N);}

ЯзыкпрограммированияRust

277`const`и`static`

Page 278: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

АтрибутыВRustобъявлениямогутбытьаннотированыспомощью«атрибутов».Онивыглядяттак:

илитак:

Разница между ними состоит в символе! , который изменяет его поведение,определяющееккакомуэлементуприменяетсяатрибут:

Атрибут#[foo] относится к следующему за ним элементу, который являетсяобъявлениемstruct .Атрибут#![bar] относитсякэлементуохватывающемуего,которыйявляется объявлениемmod . В остальном они одинаковы. Оба каким-то образом изменяютзначениеэлемента,ккоторомуониприкреплены.

Например,рассмотримтакуюфункцию:

Функция помечена как#[test] .Этоозначает, чтоонаособенная: этафункциябудетвыполнятьсяпризапускетестов.Прикомпиляции,какправило,онанебудетвключена.Теперьэтафункцияявляетсяфункциейтестирования.

Атрибутытакжемогутиметьдополнительныеданные:

Илидажеключиизначения:

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

#[test]

#![test]

#[foo]structFoo;

modbar{#![bar]}

#[test]fncheck(){assert_eq!(2,1+1);}

#[inline(always)]fnsuper_fast_fn(){

#[cfg(target_os="macos")]modmacos_only{

ЯзыкпрограммированияRust

278Атрибуты

Page 279: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ЯзыкпрограммированияRust

279Атрибуты

Page 280: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ПсевдонимытиповКлючевоесловоtype позволяетобъявитьпсевдонимдругоготипа:

Затемвыможетеиспользоватьэтотпсевдонимвместореальноготипа:

Однако, обратите внимание на то чтопсевдоним не объявляет новый тип. Rust строготипизированныйязык,напримеруваснеполучитсясравнитьзначениядвухразличныхтипов:

Выполучитеошибкуприкомпиляции:

error:mismatchedtypes:expected`i32`,found`i64`(expectedi32,foundi64)[E0308]ifx==y{^

Ноеслимыиспользуемпсевдоним:

Тоэтотпримерскомпилируетсябезошибок.ЗначениятипаNum всегдабудуттакиежекакиутипаi32 .

Вытакжеможетеиспользоватьпсевдонимытиповсобобщённымкодом:

typeName=String;

typeName=String;

letx:Name="Hello".to_string();

letx:i32=5;lety:i64=5;

ifx==y{//...}

typeNum=i32;

letx:i32=5;lety:Num=5;

ifx==y{//...}

ЯзыкпрограммированияRust

280Псевдонимытипов

Page 281: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

В этом примере мы создаем свою версию типаResult , который всегда будетиспользоватьперечислениеConcreteError вResult<T,E> вместотипаE .ПсевдонимытиповчастоиспользуютсявмодуляхстандартнойбиблиотекидлясозданиясвоихпсевдонимовдляResult<T,E> .Например,io::Result.

usestd::result;

enumConcreteError{Foo,Bar,}

typeResult<T>=result::Result<T,ConcreteError>;

ЯзыкпрограммированияRust

281Псевдонимытипов

Page 282: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ПриведениетиповRust, со своим акцентом на безопасность, обеспечивает два различных способа

преобразования различных типовмежду собой.Первый —as , для безопасного приведения.Второй —transmute , в отличие от первого, позволяет произвольное приведение типов иявляетсяоднойизсамыхопасныхвозможностейRust!

asКлючевоесловоas выполняетобычноеприведениетипов:

Онодопускаеттолькоопределенныевидыприведениятипов:

Этоприведеткошибке:

error:non-scalarcast:`[u8;4]`as`u32`letb=aasu32;//foureightsmakes32^~~~~~~~

Это «нескалярное преобразование», потому что у нас здесь преобразуютсямножественныезначения:четыреэлементамассива.Такиевидыпреобразованийоченьопасны,потомучтоониделаютпредположенияотом,какреализованымножественныенижележащиеструктуры.Поэтомунамнужночто-тоболееопасное.

transmuteФункцияtransmute предоставляетсявнутреннимисредствамикомпилятора,ито,что

она делает, является очень простым, но в то же время очень опасным. Она сообщает Rust,чтобы он воспринимал значение одного типа, как будто это значение другого типа. Этоделаетсянезависимоотсистемыпроверкитипов,ипоэтомуполностьюнавашстрахириск.

В предыдущем примере, мы знаем, что массив из четырехu8 отображается в массивu32 должным образом, и поэтому мы хотим выполнить приведение. Если вместоasиспользоватьtransmute ,тоRustпозволитэтосделать:

letx:i32=5;

lety=xasi64;

leta=[0u8,0u8,0u8,0u8];

letb=aasu32;//foureightsmakes32

ЯзыкпрограммированияRust

282Приведениетипов

Page 283: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Для того чтобы компиляция прошла успешно, мы должны обернуть эту операцию вunsafe блок. Технически, только вызовmem::transmute должен быть выполнен внебезопасном блоке, но в данном случае хорошо было бы поместить в этот блок всенеобходимое, связаное с этим вызовом, чтобы было удобнее искать. В данном примересвязаной необходимой переменной являетсяa , ипоэтомуонанаходится вблоке.Кодможетбытьвлюбомстиле,иногдаконтекстрасположенслишкомдалеко,итогдаупаковкавсегокодавunsafe небудеттакойужхорошейидеей.

Хотя при использованииtransmute и выполняется очень мало проверок, но какминимум будет проверяться, что типы имеют одинаковый размер. Нижеприведенный кодзавершитсяошибкой:

соследующимописанием:

error:transmutecalledontypeswithdifferentsizes:[u8;4](32bits)tou64(64bits)

Все,кромеэтойоднойпроверки,навашстрахириск!

usestd::mem;

unsafe{leta=[0u8,0u8,0u8,0u8];

letb=mem::transmute::<[u8;4],u32>(a);}

usestd::mem;

unsafe{leta=[0u8,0u8,0u8,0u8];

letb=mem::transmute::<[u8;4],u64>(a);}

ЯзыкпрограммированияRust

283Приведениетипов

Page 284: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

АссоциированныетипыАссоциированные (связанные) типы  — это мощная часть системы типов в Rust. Они

связанысидеей'семействатипа',другимисловами,группировкиразличныхтиповвместе.Этоописаниенемногоабстрактно,такчтодавайтеразберемнапримере.ЕсливыхотитенаписатьтипажGraph , тонужныдва обобщенныхпараметра типа: типузели типребро.Исходяизэтого,выможетенаписатьтипажGraph<N,E> ,которыйвыглядитследующимобразом:

Такое решение вроде бы достигает своей цели, но, в конечном счете, являетсянеудобным. Например, любая функция, которая принимаетGraph в качестве параметра,такжедолжнабытьобобщённойспараметрамиN иE :

НашафункциярасчетарасстоянияработаетнезависимооттипаEdge ,поэтомупараметрE вэтойсигнатуреявляетсялишнимитолькоотвлекает.

Что действительно нужно заявить, это чтобы сформировать какого-либо видаGraph ,нужнысоответствующие типыE иN ,собранныевместе.Мыможемсделатьэтоспомощьюассоциированныхтипов:

ТеперьнашиклиентымогутабстрагироватьсяотопределенногоGraph :

БольшенетнеобходимостииметьделостипомE !

Давайтепоговоримобовсемэтомболееподробно.

ОпределениеассоциированныхтиповДавайтепостроимнаштипажGraph .Вотегоопределение:

traitGraph<N,E>{fnhas_edge(&self,&N,&N)->bool;fnedges(&self,&N)->Vec<E>;//etc}

fndistance<N,E,G:Graph<N,E>>(graph:&G,start:&N,end:&N)->u32{...}

traitGraph{typeN;typeE;

fnhas_edge(&self,&Self::N,&Self::N)->bool;fnedges(&self,&Self::N)->Vec<Self::E>;//etc}

fndistance<G:Graph>(graph:&G,start:&G::N,end:&G::N)->u32{...}

ЯзыкпрограммированияRust

284Ассоциированныетипы

Page 285: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Эти объявленияtype могут иметь все то же самое, как и при работе с функциями.Например,еслибымыхотели,чтобытипN реализовывалDisplay ,чтобыбылавозможностьпечататьузлы,мымоглибысделатьследующее:

РеализацияассоциированныхтиповТипаж, который включает ассоциированные типы, как и любой другой типаж, для

реализациииспользуетключевоесловоimpl .ВотпростаяреализацияGraph :

Этоглупаяреализация,котораявсегдавозвращаетtrue ипустойVec<Edge> ,ноонадает вам общее представление о том, как реализуются такие вещи. Для начала нужны триstruct , одна для графа, одна для узла и одна для ребра.В этой реализации используются

traitGraph{typeN;typeE;

fnhas_edge(&self,&Self::N,&Self::N)->bool;fnedges(&self,&Self::N)->Vec<Self::E>;}

usestd::fmt;

traitGraph{typeN:fmt::Display;typeE;

fnhas_edge(&self,&Self::N,&Self::N)->bool;fnedges(&self,&Self::N)->Vec<Self::E>;}

structNode;

structEdge;

structMyGraph;

implGraphforMyGraph{typeN=Node;typeE=Edge;

fnhas_edge(&self,n1:&Node,n2:&Node)->bool{true}

fnedges(&self,n:&Node)->Vec<Edge>{Vec::new()}}

ЯзыкпрограммированияRust

285Ассоциированныетипы

Page 286: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Затем идет строка сimpl , которая является такой же, как и при реализации любогодругоготипажа.

Далее мы используем знак= , чтобы определить наши ассоциированные типы. Имятипажа идет слева от знака= , а конкретный тип, для которого мыimpl этот типаж, идетсправа.Наконец,мыиспользуемконкретныетипыприобъявлениифункций.

Типажи-объектыиассоциированныетипыВот еще немного синтаксиса, о котором следует упомянуть: типажи-объекты. Если вы

попытаетесьсоздатьтипаж-объектизассоциированноготипа,каквэтомпримере:

Выполучитедвеошибки:

error:thevalueoftheassociatedtype`E`(fromthetrait`main::Graph`)mustbespecified[E0191]letobj=Box::new(graph)asBox<Graph>;^~~~~~~~~~~~~~~~~~~~~~~~~~~~~24:44error:thevalueoftheassociatedtype`N`(fromthetrait`main::Graph`)mustbespecified[E0191]letobj=Box::new(graph)asBox<Graph>;^~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Мы не сможем создать типаж-объект, подобный этому, потому что у него нетинформацииобассоциированныхтипах.Вместоэтого,мыможемнаписатьтак:

СинтаксисN=Node позволяетнампредоставлятьконкретныйтип,Node ,дляпараметратипаN . То же самое и дляE=Edge . Если бымы не предоставляли это ограничение, то немоглибызнатьнаверняка,какаяimpl соответствуетэтомутипажу-объекту.

letgraph=MyGraph;letobj=Box::new(graph)asBox<Graph>;

letgraph=MyGraph;letobj=Box::new(graph)asBox<Graph<N=Node,E=Edge>>;

ЯзыкпрограммированияRust

286Ассоциированныетипы

Page 287: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

БезразмерныетипыБольшинствотиповимеютопределённыйразмервбайтах.Этотразмеробычноизвестен

во время компиляции. Например,i32   — это 32 бита, или 4 байта. Однако, существуютнекоторые полезные типы, которые не имеют определённого размера. Они называются«безразмерными»или«типамидинамическогоразмера».Одинизпримеровтакихтипов —это[T] .ЭтоттиппредставляетсобойпоследовательностьизопределённогочислаэлементовT .Номынезнаем,какмногоэтихэлементов,поэтомуразмернеизвестен.

Rustпонимаетнесколькотакихтипов,ноихиспользованиенесколькоограничено.Естьтриограничения:

1. Мыможемработатьсэкземпляромбезразмерноготипатолькоспомощьюуказателя.&[T] будетработать,а[T]  —нет.

2. Переменныеиаргументынемогутиметьтипдинамическогоразмера.3. Только последнее поле структуры может быть безразмерного типа; другие  — нет.

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

Азачемэтовсё?Посколькумыможемиспользовать[T] толькочерезуказатель,еслибыязыкнеподдерживалбезразмерныетипы,мыбынесмоглинаписатьтакойкод:

или

Вместоэтого,вамбыпришлосьнаписать:

Такимобразом,даннаяреализацияработалабытолькодляссылок,инеподдерживалабыдругие типы указателей. А реализацию для безразмерного типа смогут использовать любыеуказатели, включая определённые пользователем умные указатели (позже, когда будутисправленынекоторыеошибки).

?SizedЕсли вы пишете функцию, принимающую тип динамического размера, вы можете

использоватьспециальноеограничение?Sized :

implFooforstr{

impl<T>Foofor[T]{

implFoofor&str{

structFoo<T:?Sized>{f:T,}

ЯзыкпрограммированияRust

287Безразмерныетипы

Page 288: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Этот? читается как «Т может быть размерным (Sized )». Он означает, что этоограничениеособенное:оноразрешаетиспользованиенекоторыхтипов,которыенемоглибыбытьиспользованыв егоотсутствие.Такимобразом, онорасширяетмножествоподходящихтипов, а не сужает его. Это можно представить себе как если бы все типыT неявно былиразмерными(T:Sized ),а? отменялэтоограничениепо-умолчанию.

ЯзыкпрограммированияRust

288Безразмерныетипы

Page 289: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

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

Например,операция+ можетбытьперегруженаспомощьютипажаAdd :

Вmain мыможемиспользоватьоперацию+ длядвухPoint ,таккакмыреализовалитипажAdd<Output=Point> дляPoint .

Есть целый ряд операций, которые могут быть перегружены таким образом, и всесвязанные с этим типажи расположены в модулеstd::ops . Проверьте эту частьдокументациидляполученияполногосписка.

Реализация этих типажей следует паттерну. Давайте посмотрим на типажAdd болеедетально:

usestd::ops::Add;

#[derive(Debug)]structPoint{x:i32,y:i32,}

implAddforPoint{typeOutput=Point;

fnadd(self,other:Point)->Point{Point{x:self.x+other.x,y:self.y+other.y}}}

fnmain(){letp1=Point{x:1,y:0};letp2=Point{x:2,y:3};

letp3=p1+p2;

println!("{:?}",p3);}

pubtraitAdd<RHS=Self>{typeOutput;

fnadd(self,rhs:RHS)->Self::Output;}

ЯзыкпрограммированияRust

289Перегрузкаопераций

Page 290: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

В общей сложности здесь присутствуют три типа: типimpl Add , который мыреализуем,типRHS ,которыйпоумолчаниюравенSelf итипOutput .Длявыраженияletz=x+y :x  —этотипSelf ,y  —этотипRHS ,аz -этотипSelf::Output .

позволитвамсделатьследующее:

Использованиетипажейоперацийвобобщённыхструктурах

Теперь,когдамызнаем,какреализованытипажиопераций,мыможемреализоватьнаштипажHasArea иструктуруSquare изглавыотипажахболееобщимобразом:

implAdd<i32>forPoint{typeOutput=f64;

fnadd(self,rhs:i32)->f64{//addani32toaPointandgetanf64}}

letp:Point=//...letx:f64=p+2i32;

usestd::ops::Mul;

traitHasArea<T>{fnarea(&self)->T;}

structSquare<T>{x:T,y:T,side:T,}

impl<T>HasArea<T>forSquare<T>whereT:Mul<Output=T>+Copy{fnarea(&self)->T{self.side*self.side}}

fnmain(){lets=Square{x:0.0f64,y:0.0f64,side:12.0f64,};

println!("Площадьs:{}",s.area());}

ЯзыкпрограммированияRust

290Перегрузкаопераций

Page 291: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мы просто объявляем тип-параметрT и используем его вместоf64 в определенииHasArea иSquare .Вреализациинужносделатьболеехитрыеизменения:

Чтобыреализоватьarea ,мыдолжнымочьумножитьоперандыдругнадруга,поэтомумыобъявляемT какреализующийstd::ops::Mul .КакиAdd , Mul принимаетпараметрOutput :т.к.мызнаем,чточисланеменяютсвоеготипа,когдаихумножают,Output такжеобъявлен какT . T также должен поддерживать копирование, чтобы Rust не пыталсяпереместитьself.side ввозвращаемоезначение.

impl<T>HasArea<T>forSquare<T>whereT:Mul<Output=T>+Copy{...}

ЯзыкпрограммированияRust

291Перегрузкаопераций

Page 292: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Преобразования при разыменовании (derefcoercions)

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

Это полезно при написании своих указательных типов. Однако, в языке естьвозможность,связаннаясDeref :преобразованияприразыменовании.Вотправило:еслиестьтипU ,ионреализуетDeref<Target=T> ,значения&U будутавтоматическипреобразованыв&T ,когдаэтонеобходимо.Вотпример:

Амперсандпередзначениемозначает,чтомыберёмссылкунанего.Поэтомуowned -э т оString , а&owned   —&String . Поскольку у нас есть реализация типажаimplDeref<Target=str> for String , &String разыменуется в&str , что устраиваетfoo() .

Вот и всё. Это правило  — одно из немногих мест в Rust, где типы преобразуютсяавтоматически. Оно позволяет писать гораздо более гибкий код. Например, типRc<T>реализуетDeref<Target=T> ,поэтомутакойкодработает:

usestd::ops::Deref;

structDerefExample<T>{value:T,}

impl<T>DerefforDerefExample<T>{typeTarget=T;

fnderef(&self)->&T{&self.value}}

fnmain(){letx=DerefExample{value:'a'};assert_eq!('a',*x);}

fnfoo(s:&str){//позаимствуемстрокунасекунду}

//StringреализуетDeref<Target=str>letowned="Hello".to_string();

//Поэтому,такойкодработает:foo(&owned);

ЯзыкпрограммированияRust

292Преобразованияприразыменовании(derefcoercions)

Page 293: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мы всего лишь обернули нашString вRc<T> . Но теперь мы можем передатьRc<String> везде, куда мы могли передатьString . Сигнатураfoo не поменялась, иработаеткаксодним,такисдругимтипом.Этотпримерделаетдвапреобразования:сначалаRc<String преобразуется вString , а потомString в&str . Rust сделает столькопреобразований,скольковозможно,покатипынесовпадут.

Другая известная реализация, предоставляемая стандартной библиотекой, этоimplDeref<Target=[T]>forVec<T> :

Векторамогутразыменовыватьсявсрезы.

РазыменованиеивызовметодовDeref такжебудетработатьпривызовеметода.Другимисловами,возможентакойкод:

Несмотряна то, чтоf  —этонессылка,аfoo принимает&self , этобудетработать.Болеетого,всепримерынижеделаютодноитоже:

usestd::rc::Rc;

fnfoo(s:&str){//позаимствуемстрокунасекунду}

//StringреализуетDeref<Target=str>letowned="Hello".to_string();letcounted=Rc::new(owned);

//Поэтому,такойкодработает:foo(&counted);

fnfoo(s:&[i32]){//позаимствуемсрезнасекунду}

//Vec<T>реализуетDeref<Target=[T]>letowned=vec![1,2,3];

foo(&owned);

structFoo;

implFoo{fnfoo(&self){println!("Foo");}}

letf=Foo;

f.foo();

f.foo();(&f).foo();(&&f).foo();(&&&&&&&&f).foo();

ЯзыкпрограммированияRust

293Преобразованияприразыменовании(derefcoercions)

Page 294: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

ЯзыкпрограммированияRust

294Преобразованияприразыменовании(derefcoercions)

Page 295: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

МакросыК этому моменту вы узнали о многих инструментах Rust, которые нацелены на

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

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

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

Недостатком является то, что код, основанный на макросах, может быть трудным дляпонимания, потому что к нему применяется меньше встроенных правил. Подобно обычнойфункции,качественныймакросможетбытьиспользованбезпониманияегореализации.Темнеменее,можетбытьтрудноразработатькачественныймакрос!Крометого,ошибкикомпиляторавмакрокодесложнееинтерпретировать,потомучтоониописываютпроблемыврасширеннойформекода,аневисходнойсокращеннойформекода,которуюиспользуютразработчики.

Этинедостаткиделаютмакросычем-товроде«возможностипоследнейинстанции».Этонеозначает,чтомакросыэтоплохо;ониявляютсячастьюRust,потомучтоиногдаонивсеженужны для по-настоящему краткой записи хорошо абстрагированной части кода. Простоимейтеэтоткомпромиссввиду.

Определениемакросов(Макроопределения)Вы,возможно,виделимакросvec! ,которыйиспользуетсядляинициализациивекторас

произвольнымколичествомэлементов.

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

letx:Vec<u32>=vec![1,2,3];

ЯзыкпрограммированияRust

295Макросы

Page 296: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Мыможемреализоватьэтосокращение,используямакрос:

Ого,тутмногоновогосинтаксиса!Давайтеразберемего.

Тутмыопределяеммакроссименемvec ,аналогичнотому,какfnvec определялобыфункциюсименемvec .Привызовемынеформальнопишемимямакросасвосклицательнымзнаком,например,vec! .Восклицательныйзнакявляетсячастьюсинтаксисавызоваислужитдлятого,чтобыотличатьмакросотобычнойфункции.

Сопоставление (Matching) (Синтаксис вызовамакрокоманды)

Макрос определяется с помощью рядаправил, которые представляют собой вариантысопоставлениясобразцом.Вышеунасбыло

Это очень похоже на конструкциюmatch , но сопоставление происходит на уровнесинтаксическихдеревьевRust,наэтапекомпиляции.Точкасзапятойнеявляетсяобязательнойдля последнего (только здесь) варианта. «Образец» слева от=> известен какшаблонсовпадений (образец) (обнаружитель совпадений) (matcher). Он имеетсвою собственнуюграмматикуврамкахязыка.

Образец$x:expr будетсоответствоватьлюбомувыражениюRust,связываяегодеревосинтаксиса сметапеременной $x . Идентификаторexpr являетсяспецификаторомфрагмента; полные возможности перечислены далее в этой главе. Образец, окруженный$(...),* ,будетсоответствоватьнулюилиболеевыражениям,разделеннымзапятыми.

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

letx:Vec<u32>={letmuttemp_vec=Vec::new();temp_vec.push(1);temp_vec.push(2);temp_vec.push(3);temp_vec};

1

macro_rules!vec{($($x:expr),*)=>{{letmuttemp_vec=Vec::new();$(temp_vec.push($x);)*temp_vec}};}

macro_rules!vec{...}

($($x:expr),*)=>{...};

ЯзыкпрограммированияRust

296Макросы

Page 297: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

выведет

modeY:3

Ас

мыполучимошибкукомпиляции

error:norulesexpectedthetoken`z`

Развертывание (Expansion) (Синтаксис преобразованиямакрокоманды)

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

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

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

Еще одна деталь: макросvec! имеетдве парыфигурных скобках правой части. Оничастосочетаютсятакимобразом:

Внешние скобки являются частью синтаксисаmacro_rules! . На самом деле, выможете использовать() или[] вместо них. Они просто разграничивают правую часть вцелом.

macro_rules!foo{(x=>$e:expr)=>(println!("modeX:{}",$e));(y=>$e:expr)=>(println!("modeY:{}",$e));}

fnmain(){foo!(y=>3);}

foo!(z=>3);

$(temp_vec.push($x);)*

macro_rules!foo{()=>{{...}}}

ЯзыкпрограммированияRust

297Макросы

Page 298: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Внутренние скобки являются частью расширенного синтаксиса. Помните, что макросvec! используется в контексте выражения. Мы используем блок, для записи выражения смножественными операторами, в том числе включающееlet привязки. Если ваш макросраскрываетсяводноединственноевыражение,тодополнительнойслойскобокненужен.

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

Повторение(Repetition)(Многовариантность)Операцииповторавсегдасопутствуютдваосновныхправила:

1. $(...)* проходит через один «слой» повторений, для всех$name , которые онсодержит,вногу,и

2. каждое$name должнобытьпод,покрайнеймере,столькимколичеством$(...)* ,сколько было использовано при сопоставлении. Если оно под большим числом$(...)* ,$name будетдублироваться,принеобходимости.

Этотпричудливыймакросиллюстрируетдублированияпеременныхизвнешнихуровнейповторения.

Это наибольшая синтаксиса совпадений. Эти примеры используют конструкцию$(...)* , которая означает «ноль или более» совпадений. Также вы можете написать$(...)+ , что будет означать «одно или более» совпадений. Обе формы записи включаютнеобязательныйразделитель,располагающийсясразузазакрывающейскобкой,которыйможетбытьлюбымсимволом,заисключением+ или* .

Этасистемаповторенийоснованана«Macro-by-Example»(PDFссылка).

macro_rules!o_O{($($x:expr;[$($y:expr),*]);*)=>{&[$($($x+$y),*),*]}}

fnmain(){leta:&[i32]=o_O!(10;[1,2,3];20;[4,5,6]);

assert_eq!(a,[11,12,13,24,25,26]);}

ЯзыкпрограммированияRust

298Макросы

Page 299: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Гигиена(Hygiene)Некоторые языки реализуют макросы с помощью простой текстовой замены, что

приводит к различнымпроблемам.Например, нижеприведеннаяC программа напечатает13вместоожидаемого25 .

#defineFIVE_TIMES(x)5*x

intmain(){printf("%d\n",FIVE_TIMES(2+3));return0;}

После развертывания мы получаем5 * 2 + 3 , но умножение имеет большийприоритет чем сложение. Если вы часто использовали C макросы, вы, наверное, знаетестандартныеидиомыдляустраненияэтойпроблемы,атакжепятьилишестьдругихпроблем.ВRustмыможемнебеспокоитьсяобэтом.

Метапеременная$x обрабатываетсякакединыйузелвыражения,исохраняетсвоеместовдеревесинтаксисадажепослезамены.

Другой распространенной проблемой в системе макросов являетсязахват переменной(variablecapture).ВотCмакрос,использующийGNUCрасширение,которыйэмулируетблокивыражениийвRust.

#defineLOG(msg)({\intstate=get_log_state();\if(state>0){\printf("log(%d):%s\n",state,msg);\}\})

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

constchar*state="reticulatingsplines";LOG(state)

Онраскрываетсяв

constchar*state="reticulatingsplines";intstate=get_log_state();if(state>0){printf("log(%d):%s\n",state,state);}

macro_rules!five_times{($x:expr)=>(5*$x);}

fnmain(){assert_eq!(25,five_times!(2+3));}

ЯзыкпрограммированияRust

299Макросы

Page 300: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Вторая переменная с именемstate затеняет первую. Это проблема, потому чтокомандепечатитребуетсяобращатьсякнимобоим.

ЭквивалентныймакросвRustобладаеттребуемымповедением.

Это работает, потому что Rust имеетсистему макросов с соблюдением гигиены.Раскрытие каждого макроса происходит в отдельномконтексте синтаксиса, и каждаяпеременная обладает меткой контекста синтаксиса, где она была введена. Это как если быпеременнаяstate внутриmain былабыокрашенавдругой«цвет»вотличаеотпеременнойstate внутримакроса,из-зачегоонибынеконфликтовали.

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

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

Этосправедливодляlet привязокиметокloop,нонедляэлементов.Код,приведенныйниже,компилируется:

macro_rules!log{($msg:expr)=>{{letstate:i32=get_log_state();ifstate>0{println!("log({}):{}",state,$msg);}}};}

fnmain(){letstate:&str="reticulatingsplines";log!(state);}

macro_rules!foo{()=>(letx=3);}

fnmain(){foo!();println!("{}",x);}

macro_rules!foo{($v:ident)=>(let$v=3);}

fnmain(){foo!(x);println!("{}",x);}

ЯзыкпрограммированияRust

300Макросы

Page 301: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

РекурсиямакросовРаскрытиемакросатакжеможетвключатьвсебявызовымакросов,втомчислевызовы

тогомакроса,которыйраскрывается.Этирекурсивныемакросымогутбытьиспользованыдляобработкидревовидноговвода,какпоказанонаэтом(упрощенном)HTMLсокращение:

ОтладкамакросовЧтобы увидеть результаты расширения макросов, выполните командуrustc --

prettyexpanded .Выводпредставляетсобойцелыйконтейнер,такчтовыможетеподатьего обратно вrustc , что иногда выдает лучшие сообщения об ошибках, чем при обычнойкомпиляции. Обратите внимание, что вывод--pretty expanded может иметь разное

macro_rules!foo{()=>(fnx(){});}

fnmain(){foo!();x();}

macro_rules!write_html{($w:expr,)=>(());

($w:expr,$e:tt)=>(write!($w,"{}",$e));

($w:expr,$tag:ident[$($inner:tt)*]$($rest:tt)*)=>{{write!($w,"<{}>",stringify!($tag));write_html!($w,$($inner)*);write!($w,"</{}>",stringify!($tag));write_html!($w,$($rest)*);}};}

fnmain(){usestd::fmt::Write;letmutout=String::new();

write_html!(&mutout,html[head[title["Macrosguide"]]body[h1["Macrosarethebest!"]]]);

assert_eq!(out,"<html><head><title>Macrosguide</title></head>\<body><h1>Macrosarethebest!</h1></body></html>");}

ЯзыкпрограммированияRust

301Макросы

Page 302: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

значение, если несколько переменных, имеющих одно и то же имя (но разные контекстысинтаксиса), находятся в той же области видимости. В этом случае--prettyexpanded,hygiene расскажетвамоконтекстахсинтаксиса.

rustc , поддерживает два синтаксических расширения, которые помогают с отладкоймакросов.Внастоящеевремя,онинеустойчивыитребуютfeaturegates.

log_syntax!(...) будетпечататьсвоиаргументывстандартныйвыводвовремякомпиляции,и«развертываться»вничто.

trace_macros!(true) будетвыдаватьсообщениекомпиляторакаждыйраз,когда макрос развертывается. Используйтеtrace_macros!(false) в концеразвертывания,чтобывыключитьего.

ТребованиясинтаксисаКод на Rust может быть разобран всинтаксическое дерево, даже когда он содержит

неразвёрнутыемакросы.Этосвойствооченьполезнодляредакторовидругихинструментов,обрабатывающихисходныйкод.ОнотакжевлияетнавидсистемымакросовRust.

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

нольилибольшеэлементов;нольилибольшеметодов;выражение;оператор;образец.

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

ограничиватьсяфигурнымискобками,т.е.foo!{...} ;завершатьсяточкойсзапятой,т.е.foo!(...); .

Другое следствие разбора перед раскрытием макросов  — это то, что вызов макросадолжен состоять из допустимых лексем. Более того, скобки всех видов должны бытьсбалансированывместевызова.Например,foo!([) неявляетсяразрешённымкодом.Такоеповедениепозволяеткомпиляторупониматьгдезаканчиваетсявызовмакроса.

Говоря более формально, тело вызова макроса должно представлять собойпоследовательностьдеревьев лексем.Дереволексемопределяетсярекурсивноипредставляетсобойлибо:

последовательность деревьев лексем, окружённую согласованными круглыми,

ЯзыкпрограммированияRust

302Макросы

Page 303: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

квадратнымиилифигурнымискобками(() ,[] ,{} );любуюдругуюодиночнуюлексему.

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

ident :идентификатор.Например:x ;foo .path :квалифицированноеимя.Например:T::SpecialA .expr : выражение. Например:2+2 ; iftruethen{1}else{2} ;f(42) .ty :тип.Например:i32 ;Vec<(char,String)> ;&T .pat :образец.Например:Some(t) ;(17,'a') ;_ .stmt :единственныйоператор.Например:letx=3 .block : последовательность операторов, ограниченная фигурными скобками.Например:{log(error,"hi");return12;} .item :элемент.Например:fnfoo(){} ;structBar; .meta : «мета-элемент», как в атрибутах. Например:cfg(target_os ="windows") .tt :единственноедереволексем.

Естьдополнительныеправилаотносительнолексем,следующихзаметапеременной:

заexpr должнобытьчто-тоизэтого:=>,; ;заty иpath должнобытьчто-тоизэтого:=>,:=>as ;заpat должнобытьчто-тоизэтого:=>,= ;задругимилексемамимогутследоватьлюбыесимволы.

ПриведённыеправилаобеспечиваютразвитиесинтаксисаRustбезнеобходимостименятьсуществующиемакросы.

И ещё: система макросов никак не обрабатывет неоднозначность разбора. Например,грамматика$($t:ty)* $e:expr всегда будет выдавать ошибку, потому чтосинтаксическому анализатору пришлось бы выбирать между разбором$t и разбором$e .Можно изменить синтаксис вызова так, чтобы грамматика отличалась в начале. В данномслучаеможнонаписать$(T$t:ty)*E$e:exp .

Областивидимости,импортиэкспортмакросовМакросы разворачиваются на ранней стадии компиляции, перед разрешением имён.

Одинизнедостатковтакогоподходавтом,чтоправилавидимостидлямакросовотличныотправилдлядругихконструкцийязыка.

Компилятор определяет и разворачивает макросы при обходе графа исходного кодаконтейнера в глубину. При этом определения макросов включаются в граф в порядке ихвстречи компилятором. Поэтому макрос, определённый на уровне модуля, виден во всём

ЯзыкпрограммированияRust

303Макросы

Page 304: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

последующемкодемодуля,включаятелавсехвложенныхмодулей(mod ).

Макрос, определённый в теле функции, или где-то ещё не на уровне модуля, видентольковнутриэтогоэлемента(например,внутриоднойфункции).

Если модуль имеет атрибутmacro_use , то его макросы также видны в егородительскоммодулепослеэлементаmod данногомодуля.Еслиродительтожеимеетатрибутmacro_use ,макросытакжебудут виднывмодуле-родителеродителя,после элементаmodродителя.Этораспространяетсяналюбоечислоуровней.

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

Еслиатрибутзаписанпростокак#[macro_use] ,будутзагруженывсемакросы.Еслиатрибута нет, никакиемакросыне будут загружены. Загруженымогут быть толькомакросы,объявленныесатрибутом#[macro_export] .

Чтобы загрузить макросы из контейнерабез компоновки контейнера в выходнойартефакт,можноиспользоватьатрибут#[no_link] .

Например:

#[macro_use(foo,bar)]externcratebaz;

ЯзыкпрограммированияRust

304Макросы

Page 305: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Когдаэтабиблиотеказагружаетсяспомощью#[macro_use]externcrate ,видентолькомакросm2 .

Атрибуты,относящиесякмакросам,перечисленывсправочникеRust.

Переменная$crateЕсли макрос используется в нескольких контейнерах, всё становится ещё сложнее.

Допустим,mylib определяет

macro_rules!m1{()=>(())}

//здесьвидны:m1

modfoo{//здесьвидны:m1

#[macro_export]macro_rules!m2{()=>(())}

//здесьвидны:m1,m2}

//здесьвидны:m1

macro_rules!m3{()=>(())}

//здесьвидны:m1,m3

#[macro_use]modbar{//здесьвидны:m1,m3

macro_rules!m4{()=>(())}

//здесьвидны:m1,m3,m4}

//здесьвидны:m1,m3,m4

pubfnincrement(x:u32)->u32{x+1}

#[macro_export]macro_rules!inc_a{($x:expr)=>(::increment($x))}

#[macro_export]macro_rules!inc_b{($x:expr)=>(::mylib::increment($x))}

ЯзыкпрограммированияRust

305Макросы

Page 306: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

inc_a работаеттольковнутриmylib ,аinc_b  —толькоснаружи.Болеетого,inc_bсломается,еслипользовательимпортируетmylib поддругимименем.

ВRustпоканетгигиеничныхссылокнаконтейнеры,ноестьпростойспособобойтиэтупроблему. Особая макро-переменная$crate раскроется в::foo внутри макроса,импортированного из контейнераfoo .А когдамакрос определён и используется в одном итомжеконтейнере,$crate станетпустой.Этоозначает,чтомыможемнаписать

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

Чтобы эта система работала просто и правильно,#[macro_use] extern crate... может быть написано только в корне вашего контейнера, но не внутриmod . Этообеспечивает,что$crate раскроетсявединственныйидентификатор.

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

Рекурсивныемакросыполезныещёпооднойпричине: каждыйрекурсивныйвызовдаётнамещёоднувозможностьсопоставитьсобразцомаргументымакроса.

Приведём такой радикальныйпримериспользования данной возможности.Спомощьюрекурсивных макросов можно реализовать конечный автомат типаBitwise CyclicTag. Стоитзаметить, что мы не рекомендуем такой подход, а просто иллюстрируем возможностимакросов.

#[macro_export]macro_rules!inc{($x:expr)=>($crate::increment($x))}

ЯзыкпрограммированияRust

306Макросы

Page 307: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

РаспространённыемакросыВотнекоторыераспространённыемакросы,которыевыувидитевкоденаRust.

panic!Этот макрос вызывает панику текущего потока. Вы можете указать сообщение, с

которымпотокзавершится:

vec!Макросvec! используется по всей книге, поэтому вы наверняка уже видели его. Он

упрощаетсозданиеVec<T> :

Он также позволяет вам создавать векторы с повторяющимися значениями. Например,вотстонолей:

assert!andassert_eq!

macro_rules!bct{//cmd0:d...=>...(0,$($ps:tt),*;$_d:tt)=>(bct!($($ps),*,0;));(0,$($ps:tt),*;$_d:tt,$($ds:tt),*)=>(bct!($($ps),*,0;$($ds),*));

//cmd1p:1...=>1...p(1,$p:tt,$($ps:tt),*;1)=>(bct!($($ps),*,1,$p;1,$p));(1,$p:tt,$($ps:tt),*;1,$($ds:tt),*)=>(bct!($($ps),*,1,$p;1,$($ds),*,$p));

//cmd1p:0...=>0...(1,$p:tt,$($ps:tt),*;$($ds:tt),*)=>(bct!($($ps),*,1,$p;$($ds),*));

//haltonemptydatastring($($ps:tt),*;)=>(());}

panic!("онет!");

letv=vec![1,2,3,4,5];

letv=vec![0;100];

ЯзыкпрограммированияRust

307Макросы

Page 308: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Эти два макроса используются в тестах.assert! принимает логическое значение.assert_eq! принимаетдвазначенияипроверяет,чтоониравны.true засчитываетсякакуспех,аfalse вызываетпаникуипроваливаеттест.Воттак:

try!try! используется для обработки ошибок. Он принимает нечто возвращающее

Result<T,E> и возвращаетT еслибыловозвращеноOk<T> ;иначеонделаетвозвратизфункциисозначениемErr(E) .Вродетакого:

Такойкодчитаетсялегче,чемэтот:

unreachable!Этотмакрос применяется, когда вы хотите пометить какой-то код, который никогда не

долженисполняться:

//Работает!

assert!(true);assert_eq!(5,3+2);

//аэтонет:(

assert!(5<3);assert_eq!(5,3);

usestd::fs::File;

fnfoo()->std::io::Result<()>{letf=try!(File::create("foo.txt"));

Ok(())}

usestd::fs::File;

fnfoo()->std::io::Result<()>{letf=File::create("foo.txt");

letf=matchf{Ok(t)=>t,Err(e)=>returnErr(e),};

Ok(())}

iffalse{unreachable!();}

ЯзыкпрограммированияRust

308Макросы

Page 309: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

unimplemented!Макросunimplemented! можно использовать, когда вы хотите, чтобы ваш код

прошёл проверку типов, но пока не хотите реализовывать его настоящую логику. Один изпримеров  — это реализация типажа с несколькими требуемыми методами. Возможно, выхотите разбираться с типажом постепенно  — по одному методу за раз. В таком случае,определитеостальныеметодыкакunimplemented! ,поканезахотитенаконецреализоватьих.

ПроцедурныемакросыЕсли система макросов не может сделать того, что вам нужно, вы можете написать

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

1. Фактическоеопределениеvec! вlibcollectionsотличаетсяотпредставленногоздесьпосоображениямэффективностииповторногоиспользования.↩

letx:Option<i32>=None;

matchx{Some(_)=>unreachable!(),None=>println!("Язнаю,чтоx —этоNone!"),}

ЯзыкпрограммированияRust

309Макросы

Page 310: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

СырыеуказателиСтандартнаябиблиотекаRustсодержитрядразличныхтиповумныхуказателей,носреди

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

*constT и*mutT вRustназываются«сырымиуказателями»(rawpointers).Иногда,при написании определенных видов библиотек, вам по какой-то причине нужно обойтигарантии безопасности Rust. В этом случае, вы можете использовать сырые указатели вреализации вашей библиотеки, вместе с тем предоставляя безопасный интерфейс дляпользователей. Например,* указатели допускают псевдонимы, позволяя им бытьиспользованнымидлязаписитиповсразделяемойсобственности,идажепоточно-безопасныетипыпамяти(Rc<T> иArc<T> типыиреализованполностьювRust).

Вотнекоторыефактыосырыхуказателях,которыеследуетпомнитьикоторыеотличаютихотдругихтиповуказателей.Они:

не гарантируют, что они указывают на действительную область памяти, и негарантируют,чтоониявляетсяненулевымиуказателями(вотличиеотBox и& );не имеют никакой автоматической очистки, в отличие отBox , и поэтому требуютручногоуправленияресурсами;это простые структуры данных (plain-old-data), то есть они не перемещают правособственности,опятьжевотличиеотBox ,следовательно,компиляторRustнеможетзащититьотошибок,такихкакиспользованиеосвобождённойпамяти(use-after-free);лишенысроковжизнивкакой-либоформе,вотличиеот& ,ипоэтомукомпиляторнеможетделатьвыводыовисячихуказателях;ине имеют никаких гарантий относительно псевдонимизации или изменяемости, заисключениемизменений,недопустимыхнепосредственнодля*constT .

ОсновыСозданиесырогоуказателясовершеннобезопасно:

Авотегоразыменованиенеявляется.Следующийкоднебудетработать:

letx=5;letraw=&xas*consti32;

letmuty=10;letraw_mut=&mutyas*muti32;

letx=5;letraw=&xas*consti32;

println!("rawpointsat{}",*raw);

ЯзыкпрограммированияRust

310Сырыеуказатели

Page 311: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Онвыдаеттакуюошибку:

error:dereferenceofunsafepointerrequiresunsafefunctionorblock[E0133]println!("rawpointsat{}",*raw);^~~~

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

Для более подробной информации по операциям с сырыми указателями, обратитесь кAPIдокументациионих.

FFIСырые указатели полезны для FFI:*const T и*mut T в Rust приблизительно

соответствуютconstT* иT* вC.ДляболееподробнойинформацииобэтомобратитеськглавеFFI.

СсылкиисырыеуказателиВо время выполнения и сырой указатель,* , и ссылка, указывающая на тот же кусок

данных, имеют одинаковое представление. По факту, ссылка&T будет неявно приведена ксырому указателю*const T в безопасном коде, аналогично и для вариантовmut (обаприведениямогутбытьвыполненыявно,спомощью,соответственно,valueas*constTиvalueas*mutT ).

Переход в обратном направлении, от*const к ссылке& , не является безопасным.Ссылка&T всегда валидна, и поэтому, как минимум, сырой указатель*const T долженуказывать на правильный экземпляр типаT . Кроме того, в результате указатель долженудовлетворятьправилампсевдонимизациииизменяемостиссылок.Компиляторпредполагает,что эти свойства верны для любых ссылок, независимо от того, как они были созданы, ипоэтому любое преобразование из сырых указателей равносильно утверждению, что онисоответствуютэтимправилам.Программистдолженгарантироватьэто.

Рекомендуемымметодомпреобразованияявляется

letx=5;letraw=&xas*consti32;

letpoints_at=unsafe{*raw};

println!("rawpointsat{}",points_at);

ЯзыкпрограммированияRust

311Сырыеуказатели

Page 312: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Разыменование с помощью конструкции&*x являетсяболеепредпочтительным,чемсиспользованиемtransmute .Последнееявляетсягораздоболеемощныминструментом,чемнеобходимо, а более ограниченноеповедение сложнееиспользоватьнеправильно.Например,онатребует,чтобыx представляетсобойуказатель(вотличиеотtransmute ).

leti:u32=1;

//explicitcastletp_imm:*constu32=&ias*constu32;letmutm:u32=2;

//implicitcoercionletp_mut:*mutu32=&mutm;

unsafe{letref_imm:&u32=&*p_imm;letref_mut:&mutu32=&mut*p_mut;}

ЯзыкпрограммированияRust

312Сырыеуказатели

Page 313: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

НебезопасныйкодГлавная сила Rust  — в мощных статических гарантиях правильности поведения

программывовремяисполнения.Нопроверкибезопасностиоченьосторожны:насамомделе,существуют безопасные программы, правильность которых компилятор доказать не в силах.Чтобыписатьтакиепрограммы,нуженспособнемногоослабитьограничения.ДляэтоговRustестьключевоесловоunsafe .Код,использующийunsafe ,ограниченменьше,чемобычныйкод.

Давайтерассмотримсинтаксис,азатемпоговоримосемантике.unsafe используетсявчетырёхконтекстах.Первый —этообъявлениетого,чтофункциянебезопасна:

Например, все функции, вызываемые черезFFI, должны быть помечены какнебезопасные.Другоеиспользованиеunsafe  —этоотметканебезопасногоблока:

Третье —небезопасныетипажи:

Ичетвёртое —реализация(impl )такихтипажей:

Важно явно выделить код, ошибки в котором могут вызвать большие проблемы. ЕслипрограмманаRustпадаетс"segmentationfault",можетебытьуверены —проблемавучастке,помеченномкакнебезопасный.

Чтозначит"безопасный"?В контекстеRust "безопасный" значит "не делает ничего небезопасного". Также важно

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

Deadlock'иУтечкапамятиилидругихресурсовВыходбезвызовадеструкторовЦелочисленноепереполнение

unsafefnberegis_avtomobilya(){//страшныевещи}

unsafe{//страшныевещи}

unsafetraitScary{}

unsafeimplScaryfori32{}

ЯзыкпрограммированияRust

313Небезопасныйкод

Page 314: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Вдополнениекэтому,нижепредставленсписокнеопределённогоповедения(undefinedbehavior)вRust.Избегайтеэтихвещей,дажекогдапишетенебезопасныйкод:

ГонкаданныхРазыменованиенулевогоиливисячегоуказателяЧтениенеинициализированнойпамятиНарушениеправилосовпаденииуказателейспомощьюсырыхуказателей&mutT и&T следуют модели LLVMnoalias, кроме случаев, когда&T содержитUnsafeCell<U> .Небезопасныйкоднедолженнарушатьэтигарантиисовпаденияуказателей.ИзменениенеизменяемогозначенияилиссылкибезиспользованияUnsafeCell<U>Получениенеопределённогоповеденияспомощьюintrinsic-операцийкомпилятора:

Индексация вне границ объекта с помощьюstd::ptr::offset(offset intrinsic), кроме разрешённого случая "один байт за концомобъекта".Использованиеstd::ptr::copy_nonoverlapping_memory (intrinsic-операцииmemcpy32 /memcpy64 )спересекающимисябуферами

Неправильныезначенияпримитивныхтипов,дажевскрытыхполях:Нулевыеиливисячиессылкиилиупаковки(boxes)Любоезначениелогическоготипа,кромеfalse (0)илиtrue (1)Вариантперечисления,невключённыйвегоопределениеСуррогатное значениеchar или значениеchar , превыщающееchar::MAXПоследовательностибайт,неявляющиесяUTF-8,вstr

РазмоткастекавкоднаRustизчужогокода(черезграницыFFI),илиразмоткаизкоданаRustвчужойкод

СверхспособностинебезопасногокодаВ небезопасном блоке или функции, Rust разрешает три ситуации, которые обычно

запрещены.Всеготри.Вотони:

1. Доступкилиизменениестатическойизменяемойпеременной.2. Разыменованиесырогоуказателя.3. Вызовнебезопасныхфункций.Этосамаямощнаявозможность.

Это всё. Важно отметить, чтоunsafe , например, не "выключает проверкузаимствования". Объявление какого-то кода небезопасным не изменяет его семантику;небезопасность не означает принятие компилятором любого кода. Но она позволяет писатьвещи,которыенарушаютнекоторыеизправил.

ЯзыкпрограммированияRust

314Небезопасныйкод

Page 315: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Давайтепоговоримотрёхупомянутыхвозможностях,доступныхвнебезопасномкоде.

ДоступилиизменениеstaticmutRust позволяет пользоваться глобальным изменяемым состоянием с помощьюstatic

mut . Это может вызвать гонку по данным, и в сущности небезопасно. Подробнее смотритеразделоstatic.

РазыменованиесырогоуказателяСырыеуказателиподдерживаютпроизвольнуюарифметикууказетелей,имогутвызвать

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

ВызовнебезопасныхфункцийЭта возможность затрагивает то, откуда можно делать вызов небезопасного кода:

небезопасныефункциимогутвызыватьсятолькоизнебезопасныхблоков.

Мощь и полезность этой возможности сложно переоценить. Rust предоставляетнекоторыеintrinsic-операции компилятора в виде небезопасных функций, а некоторыенебезопасные функции обходят проверки безопасности для достижения большей скоростиисполнения.

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

ЯзыкпрограммированияRust

315Небезопасныйкод

Page 316: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

НестабильныевозможностиRustRust обеспечивает три канала распространения для Rust: nightly, beta и stable.

НестабильныефункциидоступнытольковnightlyRust.Дляболееподробнойинформацииобэтомпроцессесмотрите«Стабильностькакрезультат».

ЧтобыустановитьnightlyRust,выможетеиспользоватьrustup.sh :

$curl-shttps://static.rust-lang.org/rustup.sh|sh-s----channel=nightly

Если вы беспокоитесь опотенциальной безопасности использования данной командыcurl|sh ,топродолжайтечитатьдалее.Вытакжеможетеиспользоватьдвухступенчатыйвариантустановкииизучитьнашустановочныйскрипт:

$curl-f-Lhttps://static.rust-lang.org/rustup.sh-O$shrustup.sh--channel=nightly

Если же вы используете Windows, то, пожалуйста, скачайте один из установочныхпакетов:32-битныйили64-битныйизапуститеего.

УдалениеЕсливырешили,чтоRustвамбольшененужен,томыбудемчуть-чутьогорчены,ноэто

нормально.Некаждыйязыкпрограммированияотличноподходитдлявсех.Простозапуститескриптдеинсталляции:

$sudo/usr/local/lib/rustlib/uninstall.sh

Если вы использовали установщик Windows, то просто повторно запустите.msi ,которыйпредложитвамвозможностьудаления.

Некоторые люди, причём не безосновательно, насторожились, когда мы сказалииспользоватьcurl|sh .Когдавыделаете так, выдолжныдоверять темхорошимлюдям,которые поддерживают Rust, и не бояться, что они попытаются взломать ваш компьютер исделать какие-либо плохие вещи. Озабоченность своей безопасностью - это очень хорошо.Если вы один из таких людей, пожалуйста посмотрите в документации каксобрать Rust изисходныхкодовилискачайтеужескомпилированныйRust.Мыобещаем,чтоданныйспособнебудетиспользоватьсядляустановкиRustвсегда:скриптбылсделандлябыстрогообновленияпокаRustнаходитсявстадииalpha.

Мытакжедолжныупомянутьофициальноподдерживаемыеплатформы:

Windows(7,8,Server2008R2)Linux(2.6.18иболееновые,разныедистрибутивы),x86иx86-64OSX10.7(Lion)иболееновые,x86иx86-64

ЯзыкпрограммированияRust

316НестабильныевозможностиRust

Page 317: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Напоследок, замечание о Windows. Rust считает, что Windows  — это первокласснаяплатформадлярелиза,ноеслибытьчестными,тоопытразработкидляWindowsненастолькохорош,какдляLinux/OSX.Мыработаемнадэтим!Есличто-тонеработает,тоэтоошибка.Пожалуйста,дайтенамзнать,еслитакоепроизойдёт.КаждыйкоммиттестируетсянаWindows,впрочемтакже,какиналюбойдругойплатформе.

ЕсливыужеустановилиRust,тооткройтетерминаливведитеэто:

$rustc--version

Выдолжныувидетьверсию,хэшкоммита,датукоммитаидатусборки:

rustc1.0.0-nightly(f11f3e7ba2015-01-04)(built2015-01-06)

Итак,теперьувасестьустановленныйRust!Поздравляем!

Установщик также устанавливает документацию, которая доступна без подключения ксети.НаUNIX системах она располагается в каталоге/usr/local/share/doc/rust .ВWindows —вдиректорииshare/doc ,относительнотогокудавыустановилиRust.

Также есть ещёрядмест, гдеможнополучитьпомощь.Канал#rustна irc.mozilla.org, ккоторомувыможетеподключитьсячерезMibbit.Нажмитенаэтуссылку,ивыбудетеобщатьсяв чате с другими Rustaceans (это дурашливое прозвище, которым мы себя называем), и мыпоможем вам. Другие полезные ресурсы, посвящённые Rust:форум пользователей, /r/rustsubreddit, stack overflow. Русскоязычные ресурсы:канал #rust-ru на irc.mozilla.org, googlegroups.

ЯзыкпрограммированияRust

317НестабильныевозможностиRust

Page 318: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Плагиныккомпилятору

Введениеrustc , компилятор Rust, поддерживает плагины. Плагины  — это разработанные

пользователямибиблиотеки,которыедобавляютновыевозможностивкомпилятор:этомогутбытьрасширениясинтаксиса,дополнительныестатическиепроверки(lints),идругое.

Плагин  — это контейнер, собираемый в динамическую библиотеку, и имеющийотдельную функцию для регистрации расширения вrustc . Другие контейнеры могутзагружатьэтирасширенияспомощьюатрибута#![plugin(...)] .Такжесмотритеразделrustc::plugin сподробнымописаниеммеханизмаопределенияизагрузкиплагина.

Передаваемыев#![plugin(foo(...args...))] аргументынеобрабатываютсясамимrustc .Онипередаютсяплагинуспомощьюметодаargs структурыRegistry .

В подавляющем большинстве случаев плагин должен использоватьсятолько черезконструкцию#![plugin] , а не черезextern crate . Компоновка потянула бывнутренние библиотекиlibsyntax иlibrustc как зависимости для вашего контейнера.Обычноэтонежелательно,иможетпотребоватьсятолькоесливысобираетеещёодин,другой,плагин. Статический анализplugin_as_library проверяет выполнение этойрекомендации.

Обычная практика  — помещать плагины в отдельный контейнер, не содержащийопределений макросов (macro_rules! ) и обычного кода на Rust, предназначенного длянепосредственноконечныхпользователейбиблиотеки.

РасширениясинтаксисаПлагины могут по-разному расширять синтаксис Rust. Один из видов расширения

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

Давайте напишем плагинroman_numerals.rs , который реализует целочисленныелитералысримскимицифрами.

ЯзыкпрограммированияRust

318Плагиныккомпилятору

Page 319: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Теперьмыможемиспользоватьrn!() каклюбойдругоймакрос:

#![crate_type="dylib"]#![feature(plugin_registrar,rustc_private)]

externcratesyntax;externcraterustc;

usesyntax::codemap::Span;usesyntax::parse::token;usesyntax::ast::{TokenTree,TtToken};usesyntax::ext::base::{ExtCtxt,MacResult,DummyResult,MacEager};usesyntax::ext::build::AstBuilder;//типаждляexpr_usizeuserustc::plugin::Registry;

fnexpand_rn(cx:&mutExtCtxt,sp:Span,args:&[TokenTree])->Box<MacResult+'static>{

staticNUMERALS:&'static[(&'staticstr,u32)]=&[("M",1000),("CM",900),("D",500),("CD",400),("C",100),("XC",90),("L",50),("XL",40),("X",10),("IX",9),("V",5),("IV",4),("I",1)];

lettext=matchargs{[TtToken(_,token::Ident(s,_))]=>token::get_ident(s).to_string(),_=>{cx.span_err(sp,"аргументдолженбытьединственнымидентификатором");returnDummyResult::any(sp);}};

letmuttext=&*text;letmuttotal=0;while!text.is_empty(){matchNUMERALS.iter().find(|&&(rn,_)|text.starts_with(rn)){Some(&(rn,val))=>{total+=val;text=&text[rn.len()..];}None=>{cx.span_err(sp,"неправильноеримскоечисло");returnDummyResult::any(sp);}}}

MacEager::expr(cx.expr_u32(sp,total))}

#[plugin_registrar]pubfnplugin_registrar(reg:&mutRegistry){reg.register_macro("rn",expand_rn);}

ЯзыкпрограммированияRust

319Плагиныккомпилятору

Page 320: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

У этого подхода есть преимущества относительно простой функцииfn(&str) ->u32 :

Преобразование (в общем случае, произвольной сложности) выполняется во времякомпиляции;Проверкаправильностизаписилитералатакжепроизводитсявовремякомпиляции;Можнодобавить возможностьиспользования литерала в образцах (patterns), чтопосутипозволяетсоздаватьлитералыдлялюбоготипаданных.

Вдополнениекпроцедурныммакросам,выможетеопределятьновыеатрибутыderiveидругиевидырасширений.СмотритеразделRegistry::register_syntax_extensionидокументациюперечисленияSyntaxExtension .Вкачествеболеепродвинутогопримерасмакросами,можноознакомитьсясмакросамирегулярныхвыраженийregex_macros .

СоветыихитростиНекоторыесоветыпоотладкемакросовприменимыивслучаеплагинов.

Можно использоватьsyntax::parse , чтобы преобразовать деревья токенов ввысокоуровневыеэлементысинтаксиса,вродевыражений:

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

СохраняйтеSpanы всего, что вы разбираете, чтобы лучше сообщать об ошибках. ВыможетеобернутьвашиструктурыданныхвSpanned .

ВызовExtCtxt::span_fatal сразу прервёт компиляцию. Вместо этого, лучшевызватьExtCtxt::span_err и вернутьDummyResult , чтобы компилятор могпродолжитьработуиобнаружитьдальнейшиеошибки.

Выможетеиспользоватьspan_note иsyntax::print::pprust::*_to_stringчтобынапечататьсинтаксическийфрагментдляотладки.

#![feature(plugin)]#![plugin(roman_numerals)]

fnmain(){assert_eq!(rn!(MMXV),2015);}

fnexpand_foo(cx:&mutExtCtxt,sp:Span,args:&[TokenTree])->Box<MacResult+'static>{

letmutparser=cx.new_parser_from_tts(args);

letexpr:P<Expr>=parser.parse_expr();

ЯзыкпрограммированияRust

320Плагиныккомпилятору

Page 321: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Пример выше создавал целочисленный литерал с помощьюAstBuilder::expr_usize .ВкачествеальтернативытипажуAstBuilder , libsyntaxпредоставляет набормакросов квазицитирования. Они не документированы и совсем неотполированы. Однако, эта реализация может стать неплохой основой для улучшеннойбиблиотекиквазицитирования,котораяработалабыкакобычныйплагин.

ПлагиныстатическихпроверокПлагины могут расширятьинфраструктуру статических проверок Rust, предоставляя

новые проверки стиля кодирования, безопасности, и т.д. Полный пример можно найти вsrc/test/auxiliary/lint_plugin_test.rs .Здесьмыприводимегосуть:

Тогдакодвроде

выдастпредупреждениекомпилятора:

foo.rs:4:1:4:16warning:itemisnamed'lintme',#[warn(test_lint)]onbydefaultfoo.rs:4fnlintme(){}^~~~~~~~~~~~~~~

Плагинстатическогоанализасостоитизследующихчастей:

одинилибольшевызововdeclare_lint! ,которыеопределяютстатическиеструктурыLint ;

declare_lint!(TEST_LINT,Warn,"Предупреждатьобэлементах,названных'lintme'");

structPass;

implLintPassforPass{fnget_lints(&self)->LintArray{lint_array!(TEST_LINT)}

fncheck_item(&mutself,cx:&Context,it:&ast::Item){letname=token::get_ident(it.ident);ifname.get()=="lintme"{cx.span_lint(TEST_LINT,it.span,"элементназывается'lintme'");}}}

#[plugin_registrar]pubfnplugin_registrar(reg:&mutRegistry){reg.register_lint_pass(boxPassasLintPassObject);}

#![plugin(lint_plugin_test)]

fnlintme(){}

ЯзыкпрограммированияRust

321Плагиныккомпилятору

Page 322: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

структура,содержащаясостояние,необходимоеанализатору(вданномслучае,егонет);

реализация типажаLintPass ,определяющая,какпроверятькаждыйэлементсинтаксиса. ОдинLintPass может вызыватьspan_lint для несколькихразличныхLint ,ноондолжензарегистрироватьихвсечерезметодget_lints .

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

Статическиепроверки,определяемыеплагинами,управляютсяобычнымиатрибутамиифлагами компилятора, т.е.#[allow(test_lint)] или-A test-lint . Этиидентификаторы выводятся из первого аргументаdeclare_lint! , с учётомсоответствующихпреобразованийрегистрабуквипунктуации.

Выможетевыполнитькомандуrustc-Whelpfoo.rs ,чтобыувидетьвесьсписокстатическихпроверок,известныхrustc ,включаяте,чтозагружаютсяизfoo.rs .

ЯзыкпрограммированияRust

322Плагиныккомпилятору

Page 323: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ВстроенныйассемблерныйкодЕсли вам нужно работать на самом низком уровне или повысить производительность

программы,тоувасможетвозникнутьнеобходимостьуправлятьпроцессоромнапрямую.Rustподдерживает использование встроенного ассемблера и делает это с помощью с помощьюмакросаasm! .СинтаксиспримерносоответствуетсинтаксисуGCCиClang:

Использованиеasm является закрытой возможностью (требуется указать#![feature(asm)] для контейнера, чтобы разрешить ее использование) и, конечно же,требуетunsafe блока.

Примечание: здесь примеры приведены для x86/x86-64 ассемблера, ноподдерживаютсявсеплатформы.

ШаблонинструкцииассемблераШаблон инструкции ассемблера (assembly template) является единственным

обязательнымпараметром,иондолженбытьпредставленстрокойсимволов(т.е."" )

(Далееатрибутыfeature(asm) и#[cfg] будутопущены.)

asm!(assemblytemplate:outputoperands:inputoperands:clobbers:options);

#![feature(asm)]

#[cfg(any(target_arch="x86",target_arch="x86_64"))]fnfoo(){unsafe{asm!("NOP");}}

//otherplatforms#[cfg(not(any(target_arch="x86",target_arch="x86_64")))]fnfoo(){/*...*/}

fnmain(){//...foo();//...}

ЯзыкпрограммированияRust

323Встроенныйассемблерныйкод

Page 324: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Выходные операнды (output operands), входные операнды (input operands), затираемое(clobbers) и опции (options) не являются обязательными, но вы должны будете добавитьсоответствующееколичество: еслихотитепропуститьих:

Пробелыиотступытакженеимеютзначения:

ОперандыВходные и выходные операнды имеют одинаковый формат::"ограничение1"

(выражение1), "ограничение2"(выражение2), ..." . Выражения для выходныхоперандов должны быть либо изменяемыми, либо неизменяемыми, но еще неиницилиализированными,L-значениями:

Однако,есливызахотитеиспользоватьреальныеоперанды(регистры)вэтойпозиции,товампотребуетсязаключитьиспользуемыйрегистрвфигурныескобки{} ,ивыдолжныбудетеуказать конкретный размер операнда. Это полезно для очень низкоуровневогопрограммирования,когдаважнырегистры,которыевыиспользуете:

Затираемое(Clobbers)Некоторые инструкции могут изменять значения регистров, поэтому мы используем

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

asm!("xor%eax,%eax":::"{eax}");

asm!("xor%eax,%eax":::"{eax}");

fnadd(a:i32,b:i32)->i32{letc:i32;unsafe{asm!("add$2,$0":"=r"(c):"0"(a),"r"(b));}c}

fnmain(){assert_eq!(add(3,14159),14162)}

letresult:u8;asm!("in%dx,%al":"={al}"(result):"{dx}"(port));result

ЯзыкпрограммированияRust

324Встроенныйассемблерныйкод

Page 325: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

Еслиассемблеризменяетрегистркодаусловияcc ,тоондолженбытьуказанвкачествеодногоиз затираемых.Точно также, если ассемблермодифицируетпамять, то должнобытьуказаноmemory .

ОпцииПоследний раздел,options , специфичен для Rust. Формат представляет собой

разделенные запятыми текстовые строки (т.е.:"foo","bar","baz" ).Ониспользуетсядлятого,чтобызадатьнекоторыедополнительныеданныедлявстроенногоассемблера:

Натекущиймоментразрешеныследующиеопции:

1. volatile  — эта опция аналогична__asm__ __volatile__ (...) вgcc/clang;

2. alignstack  — некоторые инструкции ожидают, что стек был выровненопределеннымобразом(т.е.SSE),иэтаопцияуказываеткомпиляторувставитьсвойобычныйкодвыравниваниястека;

3. intel —этаопцияуказываетиспользоватьсинтаксисIntelвместоиспользуемогопоумолчаниюсинтаксисаAT&T.

БольшеинформацииТекущая реализация макросаasm! --- это прямое связывание свстроенным

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

//Putthevalue0x200ineaxasm!("mov$$0x200,%eax":/*nooutputs*/:/*noinputs*/:"{eax}");

letresult:i32;unsafe{asm!("moveax,2":"={eax}"(result):::"intel")}println!("eaxiscurrently{}",result);

ЯзыкпрограммированияRust

325Встроенныйассемблерныйкод

Page 326: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

БезstdlibПоумолчанию,std компонуетсяскаждымконтейнеромRust.Внекоторыхслучаяхэто

нежелательно, и этого можно избежать с помощью атрибута#![no_std] , примененного(привязанного)кконтейнеру.

Очевидно, должно быть нечто большее, чем просто библиотеки:#[no_std] можноиспользовать с исполняемыми контейнерами, а управлять точкой входа можно двумяспособами: с помощью атрибута#[start] , или с помощью переопределения прокладки(shim)дляCфункцииmain поумолчаниюнавашусобственную.

В функцию, помеченную атрибутом#[start] , передаются параметры команднойстрокивтомжеформате,чтоивC:

Чтобы переопределить вставленную компилятором прокладкуmain , нужно сначалаотключить ее с помощью#![no_main] , а затем создать соответствующий символ справильным ABI и правильным именем, что также потребует переопределение искажения(коверкания)именкомпилятором(#[no_mangle] ):

//aminimallibrary#![crate_type="lib"]#![feature(no_std)]#![no_std]

#![feature(lang_items,start,no_std,libc)]#![no_std]

//Pullinthesystemlibclibraryforwhatcrt0.olikelyrequiresexterncratelibc;

//Entrypointforthisprogram#[start]fnstart(_argc:isize,_argv:*const*constu8)->isize{0}

//Thesefunctionsandtraitsareusedbythecompiler,butnot//forabare-boneshelloworld.Thesearenormally//providedbylibstd.#[lang="stack_exhausted"]externfnstack_exhausted(){}#[lang="eh_personality"]externfneh_personality(){}#[lang="panic_fmt"]fnpanic_fmt()->!{loop{}}

ЯзыкпрограммированияRust

326Безstdlib

Page 327: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

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

Втораяизэтихтрехфункций,eh_personality ,используетсявмеханизмеобработкиошибок компилятора.Она часто отображается нафункцию personality (специализации)GCC(для получения дополнительной информации смотриреализацию libstd), но можно суверенностьюсказать,чтодляконтейнеров,которыеневызываютпанику,этафункцияникогдане будет вызвана. Последняя функция,panic_fmt , также используются в механизмеобработкиошибоккомпилятора.

Использованиеосновнойбиблиотеки(libcore)

Примечание: структура основной библиотеки (core) является нестабильной, ипоэтому рекомендуется использовать стандартную библиотеку (std) там, где этовозможно.

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

#![feature(no_std)]#![no_std]#![no_main]#![feature(lang_items,start)]

externcratelibc;

#[no_mangle]//дляуверенностивтом,чтоэтотсимволбудетназываться`main`навыходеpubexternfnmain(argc:i32,argv:*const*constu8)->i32{0}

#[lang="stack_exhausted"]externfnstack_exhausted(){}#[lang="eh_personality"]externfneh_personality(){}#[lang="panic_fmt"]fnpanic_fmt()->!{loop{}}

ЯзыкпрограммированияRust

327Безstdlib

Page 328: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Основная библиотека имеет очень мало зависимостей и гораздо более компактна, чемстандартнаябиблиотека.Крометого,основнаябиблиотекаимеетбольшуючастьнеобходимойфункциональностидлянаписанияидиоматическогоиэффективногокоданаRust.

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

#![feature(lang_items,start,no_std,core,libc)]#![no_std]

externcratecore;

usecore::prelude::*;

usecore::mem;

#[no_mangle]pubexternfndot_product(a:*constu32,a_len:u32,b:*constu32,b_len:u32)->u32{usecore::raw::Slice;

//ConverttheprovidedarraysintoRustslices.//Thecore::rawmoduleguaranteesthattheSlice//structurehasthesamememorylayoutasa&[T]//slice.////Thisisanunsafeoperationbecausethecompiler//cannottellthepointersarevalid.let(a_slice,b_slice):(&[u32],&[u32])=unsafe{mem::transmute((Slice{data:a,len:a_lenasusize},Slice{data:b,len:b_lenasusize},))};

//Iterateovertheslices,collectingtheresultletmutret=0;for(i,j)ina_slice.iter().zip(b_slice.iter()){ret+=(*i)*(*j);}returnret;}

#[lang="panic_fmt"]externfnpanic_fmt(args:&core::fmt::Arguments,file:&str,line:u32)->!{loop{}}

#[lang="stack_exhausted"]externfnstack_exhausted(){}#[lang="eh_personality"]externfneh_personality(){}

ЯзыкпрограммированияRust

328Безstdlib

Page 329: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Обратитевнимание,что здесь,вотличаеотпримеров,рассмотренныхвыше,естьодиндополнительныйlangэлементpanic_fmt .Ондолженбытьопределёнпотребителямиlibcore,потому что основная библиотека объявляет панику, но не определяет её. lang элементpanic_fmt определяет панику для этого контейнера, и необходимо гарантировать, что онникогданевозвращаетзначение.

Каквидновэтомпримере,основнаябиблиотекапредназначенадляпредоставлениявсеймощи Rust при любых обстоятельствах, независимо от требований платформы.Дополнительныебиблиотеки,такиекакliballoc,добавляютфункциональностьдляlibcore,дляработы которой нужно сделать некоторые платформо-зависимые предположения; но этибиблиотекивсёравноболеепереносимы,чемстандартнаябиблиотекавцелом.

ЯзыкпрограммированияRust

329Безstdlib

Page 330: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Внутренниесредства(intrinsics)

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

Они импортируются как если бы они были FFI функциями, со специальнымrust-intrinsic ABI. Например, если, находясь в отдельном (автономном) контексте, хочетсяиметь возможностьtransmute между типами, а также использовать эффективнуюарифметикууказателей,томожноимпортироватьэтифункциичерезобъявление,такоекак

Как и с любыми другими FFI функциями, их вызов всегда небезопасен и помечен какunsafe .

extern"rust-intrinsic"{fntransmute<T,U>(x:T)->U;

fnoffset<T>(dst:*constT,offset:isize)->*constT;}

ЯзыкпрограммированияRust

330Внутренниесредства(intrinsics)

Page 331: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Элементыязыка(langitems)

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

У компилятораrustc есть некоторые подключаемые операции, т.е. функционал, невстроенный жёстко в язык, а реализованный в библиотеках и специально помеченный какэлемент языка.Метка — это атрибут#[lang="..."] . Есть различные значения... , т.е.разные«элементыязыка».

Например, для указателейBox нужныдва элемента языка —для выделенияпамятиидляосвобождения.Вотпрограмма,неиспользующаястандартнуюбиблиотеку,иреализующаяBox черезmalloc иfree :

ЯзыкпрограммированияRust

331Элементыязыка(langitems)

Page 332: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Заметьте, чтоexchange_malloc долженвозвращатьдопустимыйуказатель,поэтомуонпроизводитпроверкувнутрииделаетabort ,еслионанепрошла.

Нижеперечисленыдругиевозможности,предоставляемыеэлементамиязыка:

перегружаемые операторы через типажи: типажи, соответствующие== , < ,разыменованию (* ), + и другим операторам, помечены как элементы языка;конкретноэтитипажипомеченыкакeq ,ord ,deref иadd ;раскрутка стека и общая ошибка; это элементыeh_personality , fail иfail_bounds_check ;типажи в модулеstd::marker , используемые чтобы помечать различные типы;элементыsend ,sync иcopy ;типы-метки и индикаторы вариантности изstd::marker ; это элементыcovariant_type ,contravariant_lifetime идругие.

#![feature(lang_items,box_syntax,start,no_std,libc)]#![no_std]

externcratelibc;

extern{fnabort()->!;}

#[lang="owned_box"]pubstructBox<T>(*mutT);

#[lang="exchange_malloc"]unsafefnallocate(size:usize,_align:usize)->*mutu8{letp=libc::malloc(sizeaslibc::size_t)as*mutu8;

//mallocзавершилсяошибкойifpasusize==0{abort();}

p}#[lang="exchange_free"]unsafefndeallocate(ptr:*mutu8,_size:usize,_align:usize){libc::free(ptras*mutlibc::c_void)}

#[start]fnmain(argc:isize,argv:*const*constu8)->isize{letx=box1;

0}

#[lang="stack_exhausted"]externfnstack_exhausted(){}#[lang="eh_personality"]externfneh_personality(){}#[lang="panic_fmt"]fnpanic_fmt()->!{loop{}}

ЯзыкпрограммированияRust

332Элементыязыка(langitems)

Page 333: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Элементыязыка загружаютсякомпиляторомлениво, т.е. еслипрограмманеиспользуетBox , вамненужноопределятьэлементыexchange_malloc иexchange_free . rustcвыдастошибку,еслиэлементязыканеобходим,ноненайденнивтекущемконтейнере,нивегозависимостях.

ЯзыкпрограммированияRust

333Элементыязыка(langitems)

Page 334: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Продвинутое руководстве по компоновке(advancedlinking)

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

Аргументыкомпоновки(linkargs)Есть только один способ тонкой настройки компоновки — атрибутlink_args . Этот

атрибутприменяетсякблокамextern ,иуказываетсырыеаргументы,которыедолжныбытьпереданыкомпоновщикуприсозданииартефакта.Например:

Обратите внимание, что эта возможность скрыта заfeature(link_args) , так какэто нештатный способ компоновки. В данный моментrustc вызывает системныйкомпоновщик(набольшинствесистемэтоgcc ,наWindows —link.exe ),поэтомупередачааргументовкоманднойстрокиимеетсмысл.Нореализацияневсегдабудеттакой —вбудущемrustc может напрямуюиспользоватьLLVMдля связывания с нативными библиотеками, итогдаlink_args станет бессмысленным. Того же эффекта можно достигнуть с пощощьюпередачиrustc аргумента-Clink-args .

Крайне рекомендуетсяне использовать этот атрибут, и пользоваться вместо него болееточноопределённыматрибутом#link(...) дляблоковextern .

СтатическоесвязываниеСтатическое связывание  — это процесс создания артефакта, который содержит все

нужные библиотеки, и потому не потребует установленных библиотек на целевой системе.БиблиотекинаRustпоумолчаниюсвязываютсястатически,поэтомуприложенияибиблиотекина Rust можно использовать без установки Rust повсюду. Напротив, нативные библиотеки(например,libc иlibm ) обычно связываются динамически, но это можно изменить, исделатьчтобыонитакжесвязывалисьстатически.

Компоновка —этопроцесс,которыйреализуетсяпо-разномунаразныхплатформах.Нанекоторых из них статическое связывание вообще не возможно! Этот раздел предполагаетзнакомствоспроцессомкомпоновкинавашейплатформе.

#![feature(link_args)]

#[link_args="-foo-bar-baz"]extern{}

ЯзыкпрограммированияRust

334Продвинутоеруководствепокомпоновке(advancedlinking)

Page 335: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

LinuxПо умолчанию, программы на Rust для Linux компонуются с системнойlibc и ещё

некоторымибиблиотеками.Давайтепосмотримнапримерна64-битноймашинесLinux,GCCиglibc (самойпопулярнойlibc наLinux):

$catexample.rsfnmain(){}$rustcexample.rs$lddexamplelinux-vdso.so.1=>(0x00007ffd565fd000)libdl.so.2=>/lib/x86_64-linux-gnu/libdl.so.2(0x00007fa81889c000)libpthread.so.0=>/lib/x86_64-linux-gnu/libpthread.so.0(0x00007fa81867e000)librt.so.1=>/lib/x86_64-linux-gnu/librt.so.1(0x00007fa818475000)libgcc_s.so.1=>/lib/x86_64-linux-gnu/libgcc_s.so.1(0x00007fa81825f000)libc.so.6=>/lib/x86_64-linux-gnu/libc.so.6(0x00007fa817e9a000)/lib64/ld-linux-x86-64.so.2(0x00007fa818cf9000)libm.so.6=>/lib/x86_64-linux-gnu/libm.so.6(0x00007fa817b93000)

Иногда динамическое связывание на Linux нежелательно: например, если вы хотитеиспользоватьвозможностиизновыхбиблиотекнастарыхсистемахилинацелевыхсистемахнеттакихбиблиотек.

Статическое связывание возможно с альтернативнойlibc , musl . Вы можетескомпилировать свою версию Rust, которая будет использоватьmusl , и установить её вотдельнуюдиректорию,спомощьюинструкции,приведённойниже:

ЯзыкпрограммированияRust

335Продвинутоеруководствепокомпоновке(advancedlinking)

Page 336: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

$mkdirmusldist$PREFIX=$(pwd)/musldist$$#Buildmusl$wgethttp://www.musl-libc.org/releases/musl-1.1.10.tar.gz[...]$tarxfmusl-1.1.10.tar.gz$cdmusl-1.1.10/musl-1.1.10$./configure--disable-shared--prefix=$PREFIX[...]musl-1.1.10$make[...]musl-1.1.10$makeinstall[...]musl-1.1.10$cd..$du-hmusldist/lib/libc.a2.2Mmusldist/lib/libc.a$$#Buildlibunwind.a$wgethttp://llvm.org/releases/3.6.1/llvm-3.6.1.src.tar.xz$tarxfllvm-3.6.1.src.tar.xz$cdllvm-3.6.1.src/projects/llvm-3.6.1.src/projects$svncohttp://llvm.org/svn/llvm-project/libcxxabi/trunk/libcxxabillvm-3.6.1.src/projects$svncohttp://llvm.org/svn/llvm-project/libunwind/trunk/libunwindllvm-3.6.1.src/projects$sed-i's#^\(include_directories\).*$#\0\n\1(../libcxxabi/include)#'libunwind/CMakeLists.txtllvm-3.6.1.src/projects$mkdirlibunwind/buildllvm-3.6.1.src/projects$cdlibunwind/buildllvm-3.6.1.src/projects/libunwind/build$cmake-DLLVM_PATH=../../..-DLIBUNWIND_ENABLE_SHARED=0..llvm-3.6.1.src/projects/libunwind/build$makellvm-3.6.1.src/projects/libunwind/build$cplib/libunwind.a$PREFIX/lib/llvm-3.6.1.src/projects/libunwind/build$cdcd../../../../$du-hmusldist/lib/libunwind.a164Kmusldist/lib/libunwind.a$$#Buildmusl-enabledrust$gitclonehttps://github.com/rust-lang/rust.gitmuslrust$cdmuslrustmuslrust$./configure--target=x86_64-unknown-linux-musl--musl-root=$PREFIX--prefix=$PREFIXmuslrust$makemuslrust$makeinstallmuslrust$cd..$du-hmusldist/bin/rustc12Kmusldist/bin/rustc

Теперь у вас есть сборка Rust сmusl ! Поскольку мы установили её в отдельнуюкорневую директорию, надо удостовериться в том, что система может найти исполняемыефайлыибиблиотеки:

$exportPATH=$PREFIX/bin:$PATH$exportLD_LIBRARY_PATH=$PREFIX/lib:$LD_LIBRARY_PATH

ЯзыкпрограммированияRust

336Продвинутоеруководствепокомпоновке(advancedlinking)

Page 337: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Давайтепопробуем!

$echo'fnmain(){println!("hi!");panic!("failed");}'>example.rs$rustc--target=x86_64-unknown-linux-muslexample.rs$lddexamplenotadynamicexecutable$./examplehi!thread'<main>'panickedat'failed',example.rs:1

Успех!ЭтапрограммаможетбытьскопировананапочтилюбуюмашинусLinuxстойжеархитектуройпроцессораибудетработатьбезпроблем.

cargo build также принимает опцию--target , так что вы можете собиратьконтейнерыкакобычно.Однако,возможновампридётсяпересобратьнативныебиблиотекисmusl ,чтобыиметьвозможностьскомпоноватьсясними.

ЯзыкпрограммированияRust

337Продвинутоеруководствепокомпоновке(advancedlinking)

Page 338: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ТестыпроизводительностиRust поддерживает тесты производительности, которые помогают измерить

производительность вашего кода. Давайте изменим нашsrc/lib.rs , чтобы он выгляделследующимобразом(комментарииопущены):

Обратите внимание на включение возможности (feature gate)test , что включает этунестабильнуювозможность.

Мы импортировали контейнерtest , который включает поддержку измеренияпроизводительности.Унасестьноваяфункция,аннотированнаяспомощьюатрибутаbench .В отличие от обычных тестов, которые не принимают никаких аргументов, тестыпроизводительности в качестве аргумента принимают&mut Bencher . Bencherпредоставляет методiter , который в качестве аргумента принимает замыкание. Этозамыканиесодержиткод,производительностькоторогомыхотелибыпротестировать.

Запусктестовпроизводительностиосуществляетсякомандойcargobench :

$cargobenchCompilingadderv0.0.1(file:///home/steve/tmp/adder)Runningtarget/release/adder-91b3e234d4ed382a

running2teststesttests::it_works...ignoredtesttests::bench_add_two...bench:1ns/iter(+/-0)

testresult:ok.0passed;0failed;1ignored;1measured

#![feature(test)]

externcratetest;

pubfnadd_two(a:i32)->i32{a+2}

#[cfg(test)]modtests{usesuper::*;usetest::Bencher;

#[test]fnit_works(){assert_eq!(4,add_two(2));}

#[bench]fnbench_add_two(b:&mutBencher){b.iter(||add_two(2));}}

ЯзыкпрограммированияRust

338Тестыпроизводительности

Page 339: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Все тесты, не относящиеся к тестампроизводительности, были проигнорированы.Вы,наверное, заметили, что выполнениеcargobench занимаетнемногобольшевременичемcargotest . Это происходит потому, что Rust запускает наш тест несколько раз, а затемвыдает среднее значение. Так как мы выполняем слишком мало полезной работы в этомпримере,унасполучается1ns/iter(+/-0) ,нобылабывыведенадисперсия,еслибыбылодин.

Советыпонаписаниютестовпроизводительности:

Внутриiter циклапишитетолькототкод,производительностькотороговыхотитеизмерить;инициализациювыполняйтезапределамиiter циклаВнутриiter циклапишитекод,которыйбудетидемпотентным(будетделать«тожесамое»накаждойитерации);ненакапливайтеинеизменяйтесостояниеВнеiter циклапишитекодкоторыйтакжебудетидемпотентным;скореевсего,онбудетзапущенмногоразвовремятестаВнутриiter цикла пишите код, который будет коротким и быстрым, так чтобызапуски тестов происходили быстро и калибратор мог настроить длину пробега сточнымразрешениемВнутриiter цикла пишите код, делающий что-то простое, чтобы помочь ввыявленииулучшения(илиуменьшения)производительности

ОсобенностиоптимизацииАвот другой сложныймомент, относящийся к написанию тестов производительности:

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

выведетследующиерезультаты

running1testtestbench_xor_1000_ints...bench:0ns/iter(+/-0)

testresult:ok.0passed;0failed;0ignored;1measured

#![feature(test)]

externcratetest;usetest::Bencher;

#[bench]fnbench_xor_1000_ints(b:&mutBencher){b.iter(||{(0..1000).fold(0,|old,new|old^new);});}

ЯзыкпрограммированияRust

339Тестыпроизводительности

Page 340: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Движок для запуска тестов производительности оставляет две возможности,позволяющие этого избежать. Либо использовать замыкание, передаваемое в методiter ,которое возвращает какое-либо значение; тогда это заставит оптимизатор думать, чтовозвращаемоезначениебудетиспользовано,из-зачегоудалитьвычисленияполностьюбудетневозможно.Дляпримеравышеэтогоможнодостигнуть,измениввызоваb.iter

Либо использовать вызов функцииtest::black_box , которая представляет собой«черный ящик», непрозрачный для оптимизатора, тем самым заставляя его рассматриватьлюбойаргументкакиспользуемый.

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

Выполнение одного из вышеперечисленных изменений дает следующие результатыизмеренияпроизводительности

running1testtestbench_xor_1000_ints...bench:131ns/iter(+/-3)

testresult:ok.0passed;0failed;0ignored;1measured

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

b.iter(||{//notelackof`;`(couldalsouseanexplicit`return`).(0..1000).fold(0,|old,new|old^new)});

#![feature(test)]

externcratetest;

b.iter(||{letn=test::black_box(1000);

(0..n).fold(0,|a,b|a^b)})

ЯзыкпрограммированияRust

340Тестыпроизводительности

Page 341: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Синтаксисупаковкиишаблоны`match`В настоящее время единственный стабильный способ созданияBox  —это создание с

помощьюметодаBox::new .ВстабильнойсборкеRustтакженевозможнодеструктурироватьBox при использовании сопоставления с шаблоном. В нестабильной сборке может бытьиспользованоключевоесловоbox ,какдлясоздания,такидлядеструктуризацииBox .Нижепредставленпримериспользования:

Обратите внимание, что эти возможности в настоящее время являются скрытыми:box_syntax (созданиеупаковки)иbox_patterns (деструктурированиеисопоставлениесобразцом),потомучтосинтаксисвсеещеможетизменитьсявбудущем.

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

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

#![feature(box_syntax,box_patterns)]

fnmain(){letb=Some(box5);matchb{Some(boxn)ifn<0=>{println!("Boxcontainsnegativenumber{}",n);},Some(boxn)ifn>=0=>{println!("Boxcontainsnon-negativenumber{}",n);},None=>{println!("Nobox");},_=>unreachable!()}}

ЯзыкпрограммированияRust

341Синтаксисупаковкиишаблоны`match`

Page 342: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Идея состоит в том, что, при передаче упаковки, происходит копирование толькоуказателя,аневсехint ,изкоторыхсостоитBigStruct .

ЭтоантипаттернвRust.Вместоэтогоследуетнаписатьтак:

Этодаетвамгибкостьбезущербадляпроизводительности.

Выможете подумать, что такое использование даст нам ужасную производительность:возвращается значение, а затем оно сразу упаковывается?! Разве это не паттерн худшего издвух миров? Rust намного умнее. В этом коде не происходит копирование.main выделяет

structBigStruct{one:i32,two:i32,//etcone_hundred:i32,}

fnfoo(x:Box<BigStruct>)->Box<BigStruct>{Box::new(*x)}

fnmain(){letx=Box::new(BigStruct{one:1,two:2,one_hundred:100,});

lety=foo(x);}

#![feature(box_syntax)]

structBigStruct{one:i32,two:i32,//etcone_hundred:i32,}

fnfoo(x:Box<BigStruct>)->BigStruct{*x}

fnmain(){letx=Box::new(BigStruct{one:1,two:2,one_hundred:100,});

lety:Box<BigStruct>=boxfoo(x);}

ЯзыкпрограммированияRust

342Синтаксисупаковкиишаблоны`match`

Page 343: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

достаточно места дляbox , передает указатель на эту память вfoo в видеx , а затемfooзаписываетзначениепрямовBox<T> .

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

ЯзыкпрограммированияRust

343Синтаксисупаковкиишаблоны`match`

Page 344: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Шаблоны`match`длясрезовЕсливыхотитевкачествешаблонадлясопоставленияиспользоватьсрезилимассив,то

выможетеиспользовать& иактивироватьвозможностьslice_patterns :

Отключаемаявозможностьadvanced_slice_patterns позволяетиспользовать.. ,чтобы обозначить любое число элементов в шаблоне. Этот символ подстановки можноиспользовать в массиве один раз. Если перед.. есть идентификатор, результат среза будетсвязансэтимименем.Например:

#![feature(slice_patterns)]

fnmain(){letv=vec!["match_this","1"];

match&v[..]{["match_this",second]=>println!("Thesecondelementis{}",second),_=>{},}}

#![feature(advanced_slice_patterns,slice_patterns)]

fnis_symmetric(list:&[u32])->bool{matchlist{[]|[_]=>true,[x,inside..,y]ifx==y=>is_symmetric(inside),_=>false}}

fnmain(){letsym=&[0,1,4,2,4,1,0];assert!(is_symmetric(sym));

letnot_sym=&[0,1,7,2,4,1,0];assert!(!is_symmetric(not_sym));}

ЯзыкпрограммированияRust

344Шаблоны`match`длясрезов

Page 345: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

АссоциированныеконстантыСвключеннойвозможностьюassociated_consts выможетеопределитьконстанты

вродеэтой:

ЛюбаяреализацияFoo должнабудетопределитьID .Безэтогоопределения:

выдастошибку

error:notalltraititemsimplemented,missing:`ID`[E0046]implFoofori32{}

Такжеможетбытьреализованозначениепоумолчанию:

#![feature(associated_consts)]

traitFoo{constID:i32;}

implFoofori32{constID:i32=1;}

fnmain(){assert_eq!(1,i32::ID);}

#![feature(associated_consts)]

traitFoo{constID:i32;}

implFoofori32{}

ЯзыкпрограммированияRust

345Ассоциированныеконстанты

Page 346: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Каквыможетевидеть,приреализацииFoo ,можнооставитьконстантунеопределенной,как в случаедляi32 .Тогдабудетиспользованозначениепоумолчанию.Нотакжеможноидобавитьсобственноеопределение,каквслучаедляi64 .

Ассоциированныеконстантымогутбытьассоциированынетолькостипажом.Этотакжепрекрасноработаетисблокомimpl дляstruct :

#![feature(associated_consts)]

traitFoo{constID:i32=1;}

implFoofori32{}

implFoofori64{constID:i32=5;}

fnmain(){assert_eq!(1,i32::ID);assert_eq!(5,i64::ID);}

#![feature(associated_consts)]

structFoo;

implFoo{pubconstFOO:u32=3;}

ЯзыкпрограммированияRust

346Ассоциированныеконстанты

Page 347: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ПользовательскиеменеджерыпамятиВыделениепамяти—этонесамаяпростаязадача,иRustобычнозаботитсяобэтомсам,

ночастонужнотонкоуправлятьвыделениемпамяти.Компилятористандартнаябиблиотекавнастоящее время позволяют глобально переключить используемый менеджер во времякомпиляции.ОписаниесейчаснаходитсявRFC1183,ноздесьмырассмотримкаксделатьвашсобственныйменеджер.

СтандартныйменеджерпамятиВнастоящеевремякомпиляторсодержитдвастандартныхменеджера:alloc_system

и alloc_jemalloc (однако у некоторых платформ отсутствует jemalloc). Эти менеджерыстандартны для контейнеров Rust и содержат реализацию подпрограмм для выделения иосвобождения памяти. Стандартная библиотека не компилируется специально дляиспользованиятолькоодногоизних.Компиляторбудетрешатькакойменеджериспользоватьвовремякомпиляциивзависимостиоттипапроизводимыхвыходныхартефактов.

Поумолчаниюисполняемыефайлысгенерированныекомпиляторомбудутиспользоватьalloc_jemalloc (тамгдевозможно).Втакомслучаекомпилятор"контролируетвесьмир",втомсмыслечтоунегоестьвластьнадокончательнойкомпоновкой.

Однако динамические и статические библиотеки по умолчанию будут использоватьalloc_system .ЗдесьRustобычновролигостявдругомприложениииливообщевдругоммире, где он не может авторитетно решать какой менеджер использовать. В результате онвозвращается назад к стандартным API (таких какmalloc иfree ), для получения иосвобожденияпамяти.

ПереключениеменеджеровпамятиНесмотрянаточтовбольшинствеслучаевнамподойдётто,чтокомпиляторвыбираетпо

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

#![feature(alloc_system)]

externcratealloc_system;

fnmain(){leta=Box::new(4);//выделениепамятиспомощьюсистемногоменеджераprintln!("{}",a);}

ЯзыкпрограммированияRust

347Пользовательскиеменеджерыпамяти

Page 348: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

НаписаниесвоегоменеджерапамятиИногдадажевыборамеждуjemallocисистемнымменеджеромнедостаточноинеобходим

совершенноновыйменеджерпамяти.Вэтомслучаемынапишемнашсобственныйконтейнер,который будет предоставлять API менеджера памяти (также как иalloc_system илиalloc_jemalloc ). Для примера давайте рассмотрим упрощенную и аннотированнуюверсиюalloc_system :

#![feature(alloc_jemalloc)]#![crate_type="dylib"]

externcratealloc_jemalloc;

pubfnfoo(){leta=Box::new(4);//выделениепамятиспомощьюjemallocprintln!("{}",a);}

//Компиляторунужноуказать,чтоэтотконтейнерявляетсяменеджеромпамяти,для//тогочтобыприкомпоновкеоннеиспользовалдругойменеджер.#![feature(allocator)]#![allocator]

//Менеджерампамятинепозволяютзависетьотстандартнойбиблиотеки,котораяв//своюочередьзависитотменеджера,чтобыизбежатьциклическойзависимости.//Однакоэтотконтейнерможетиспользоватьвсеизlibcore.#![no_std]

//Давайтедадимкакое-нибудьуникальноеимянашемуменеджеру.#![crate_name="my_allocator"]#![crate_type="rlib"]

//Нашсистемныйменеджербудетиспользоватьпоставляемыйвместескомпилятором//контейнерlibcдлясвязисFFI.Имейтеввиду,чтонаданныймоментвнешний//(crates.io)libcнеможетбытьиспользован,посколькуонкомпонуетсясо//стандартнойбиблиотекой(`#![no_std]`всеещенестабилен).#![feature(libc)]externcratelibc;

//Нижеперечисленыпятьфункций,необходимыепользовательскомуменеджерупамяти.//Ихсигнатурыиименанаданныймоментнепроверяютсякомпилятором,ноэто//вскоребудетреализовано,такчтоонидолжнысоответствоватьтому,что//находитсяниже.////Имейтеввиду,чтостандартные`malloc`и`realloc`непредоставляютопцийдля//выравнивания,такчтоэтареализациядолжнабытьулучшенаиподдерживать//выравнивание.#[no_mangle]pubexternfn__rust_allocate(size:usize,_align:usize)->*mutu8{

ЯзыкпрограммированияRust

348Пользовательскиеменеджерыпамяти

Page 349: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

После того как мы скомпилировали этот контейнер, мы можем использовать егоследующимобразом:

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

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

Любой артефакт может быть скомпонован только с одним менеджером.Исполняемые файлы, динамические библиотеки и статические библиотеки должныбыть скомпонованы с одним менеджером, и если не один не был указан, токомпиляторсамвыберетодин.ВтожевремяRustбиблиотеки(rlibs)ненуждаютсявкомпоновкесменеджером(ноэтовозможно).

unsafe{libc::malloc(sizeaslibc::size_t)as*mutu8}}

#[no_mangle]pubexternfn__rust_deallocate(ptr:*mutu8,_old_size:usize,_align:usize){unsafe{libc::free(ptras*mutlibc::c_void)}}

#[no_mangle]pubexternfn__rust_reallocate(ptr:*mutu8,_old_size:usize,size:usize,_align:usize)->*mutu8{unsafe{libc::realloc(ptras*mutlibc::c_void,sizeaslibc::size_t)as*mutu8}}

#[no_mangle]pubexternfn__rust_reallocate_inplace(_ptr:*mutu8,old_size:usize,_size:usize,_align:usize)->usize{old_size//libcнеподдерживаетэтотAPI}

#[no_mangle]pubexternfn__rust_usable_size(size:usize,_align:usize)->usize{size}

externcratemy_allocator;

fnmain(){leta=Box::new(8);//выделениепамятиспомощьюнашегоконтейнераprintln!("{}",a);}

ЯзыкпрограммированияRust

349Пользовательскиеменеджерыпамяти

Page 350: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Потребитель какого-либо менеджера памяти имеет пометку#![needs_allocator] (в данном случае контейнерliballoc ) и какой-либоконтейнер#[allocator] неможеттранзитивнозависетьотконтейнера,которомунужен менеджер (т.е. циклическая зависимость не допускается). Это означает, чтоменеджерыпамятивданныймоментдолжныограничитьсебятолькоlibcore.

ЯзыкпрограммированияRust

350Пользовательскиеменеджерыпамяти

Page 351: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

ГлоссарийНе каждыйпользовательRust имеет опыт работы с системамипрограммирования, или

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

АбстрактноесинтаксическоедеревоКогдакомпиляторкомпилируетпрограмму,онделаетцелыйрядразличныхвещей.Одна

из вещей, которые он делает, это преобразует текст вашей программы в 'Абстрактноесинтаксическое дерево,' или 'AST.' Это дерево является представлением структуры вашейпрограммы.Например,2+3 можетбытьпреобразовановдерево:

+/\23

А2+(3*4) будетвыглядетьследующимобразом:

+/\2*/\34

АрностьАрностьозначаетчислоаргументов,которыепринимаетфункцияилиоперация.

Вприведенномвышепримереx иy имеютарность2.z имеетарность3.

ВыражениеВпрограммировании,выражение —этокомбинациязначений,постоянных,переменных

ифункций,котораявычисляетсяводно значение.Например,2+(3*4)  —выражение,вычисляющееся в значение14 . Стоит заметить, что у выражений могут быть побочныеэффекты. Например, функция, участвующая в выражении, может делать что-то ещё помимонепосредственновозвратазначения.

Язык,ориентированныйнавыражения

letx=(2,3);lety=(4,6);letz=(8,2,6);

ЯзыкпрограммированияRust

351Глоссарий

Page 352: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

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

ОператорВпрограммировании, оператор — это наименьший отдельный элемент языка, который

обозначает выполнение компьютером законченного действия. Например, в языке Cprintf("42");  —этооператор.

ЯзыкпрограммированияRust

352Глоссарий

Page 353: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

БиблиографияЭто — список материалов, имеющих отношние к Rust. Он включает в себя

предварительныеисследования,которыевтотилииноймоментоказаливлияниенаструктуруRust'а,атакжепубликацииоRust.

Систематипов

RegionbasedmemorymanagementinCycloneSafemanualmemorymanagementinCycloneTypeclasses:makingad-hocpolymorphismlessadhocMacrosthatworktogetherTraits:composableunitsofbehaviorAliasburying-Wetriedsomethingsimilarandabandonedit.ExternaluniquenessisuniqueenoughUniquenessandReferenceImmutabilityforSafeParallelismRegionBasedMemoryManagement

Многозадачность

Singularity:rethinkingthesoftwarestackLanguagesupportforfastandreliablemessagepassinginsingularityOSSchedulingmultithreadedcomputationsbyworkstealingThreadschedulingformultiprogrammingmultiprocessorsThedatalocalityofworkstealingDynamiccircularworkstealingdeque-TheChase/LevdequeWork-first and help-first scheduling policies for async-finish task parallelism - Moregeneralthanfully-strictworkstealingAJavafork/joincalamity-critiqueofJava'sfork/joinlibrary,particularlyitsapplicationofworkstealingtonon-strictcomputationSchedulingtechniquesforconcurrentsystemsContentionawareschedulingBalancedworkstealingfortime-sharingmulticoresThreelayercakeforshared-memoryprogrammingNon-blockingsteal-halfworkqueuesReagents:expressingandcomposingfine-grainedconcurrencyAlgorithmsforscalablesynchronizationofshared-memorymultiprocessorsEpoch-basedreclamation.

Другое

ЯзыкпрограммированияRust

353Библиография

Page 354: Язык программирования Rust - GitHub · Полезные ссылки Чаты Ссылки для обсуждения языка, получения помощи

Crash-onlysoftwareComposingHigh-PerformanceMemoryAllocatorsReconsideringCustomMemoryAllocation

СтатьиоRust

GPU Programming in Rust: Implementing High LevelAbstractions in a Systems LevelLanguage.EarlyGPUworkbyEricHolk.Parallelclosures:anewtwistonanoldidea

notexactlyaboutRust,butbynmatsakisPatina: A Formalization of the Rust Programming Language. Early formalization of asubsetofthetypesystem,byEricReed.Experience Report: Developing the Servo Web Browser Engine using Rust. By LarsBergstrom.ImplementingaGenericRadixTrieinRust.UndergradpaperbyMichaelSproul.Reenix: Implementing aUnix-LikeOperating System inRust.Undergrad paper byAlexLight.EvaluationofperformanceandproductivitymetricsofpotentialprogramminglanguagesintheHPCenvironment.Bachelor'sthesisbyFlorianWilkens.ComparesC,GoandRust.Nom, a byte oriented, streaming, zero copy, parser combinators library in Rust. ByGeoffroyCouprie,researchforVLC.Graph-BasedHigher-OrderIntermediateRepresentation.AnexperimentalIRimplementedinImpala,aRust-likelanguage.CodeRefinementofStencilCodes.AnotherpaperusingImpala.ParallelizationinRustwithfork-joinandfriends.LinusFarnstrand'smaster'sthesis.SessionTypesforRust.PhilipMunksgaard'smaster'sthesis.ResearchforServo.OwnershipisTheft:ExperiencesBuildinganEmbeddedOSinRust-AmitLevy,et.al.

ЯзыкпрограммированияRust

354Библиография