DimonVideo, я не могу ничего написать в комментариях у себя в блоге. Ни ответить, ни оставить комментарий. Хотел написать вам сообщение в личку [...] читать полностью



Ох и упрятал Дмитрий кнопочку для добавления новой записи в блог 🤔. Я уж было подумал, что он решил прикончить этот раздел, но присмотревшись, понял, что тот сам сдох (а между нами, девочками, говоря, сколько я тут ни писал - живым оно и не было; верно, потому меня так всегда сюда влекло - люблю тишину склепов, в присутстствии вечности думается хорошо; есть и еще одна меркантильная причина, но я о ней умолчу).
Как поживаете, м?


Здравствуйте 🖖.Ох и упрятал Дмитрий кнопочку для добавления новой записи в блог 🤔. Я уж было подумал, что он решил прикончить этот раздел, [...] читать полностью
2438


И вот теперь у меня появилась робкая надежда положить конец этим терзаниям. Тема довольно объемная, поэтому я разобью её обзор на несколько постов. Итак, приступим.
Знаете ли вы, что такое \"абстракция\"? Зачастую нам приходится иметь дело со сложной и плохо усваиваемой информацией, причем связанной между собой, поэтому её желательно поглощать пусть и по кускам, но системно. А умишко-то невелик! О чём, кстати, предупреждал ещё Козьма Прутков, глаголя, что нельзя объять необъятное. Поэтому давным-давно для преодоления этого затруднения принято использовать анализ, который позволяет увидеть за нагромождениями хаоса чёткие контуры главенствующей идеи, которая всем этим заправляет. Не всегда выявление этого сравнительно компактного ядра возможно, порою приходится использовать допущения, чтобы втиснуть всю информацию в задуманное нами прокрустово ложе, но итоговый результат того стоит, поскольку сия идея задумывается и является ключом к пониманию обозреваемой проблемы. Так вот, эта исключительно умозрительная конструкция и называется абстракцией. Кто-нибудь что-нибудь понял? Нет, ну да не беда, это и не было целью.
Главное понять, что грамотная абстракция позволяет усвоить информацию практически любого уровня сложности. Пример популярной абстракции — пресловутый \"кот Шредингера\".
Я долго размышлял над тем, какую абстракцию было бы удобно использовать для понимания сути кастования, каста. Примерно 1 час. Очень долго, согласитесь. И нашёл! Таковой абстракцией являются МНОЖЕСТВА. Я не помню математического определения сей субстанции из теории множеств, поэтому буду использовать упрощенное описательное. Множество состоит из элементов, но может быть и пустым. Множество может содержать подмножества; фактически любое множество содержит бесконечное число подмножеств, учитывая оговорку выше про пустые множества, поскольку множество пустых множеств не содержит ни одного элемента. Можно представить множества как сита с ячейками разного размера и формы, параметры ячеек описывают критерии отбора в множество: чем меньше ячейки и изощреннее форма, тем меньше элементов попадает в множество. Все эти смысловые экзерсисы, впрочем, нам не особенно нужны, мы и так не раз имели дело с пустыми множествами наличных денег и туалетной бумаги, и можем смело утверждать их небольшую практическую ценность. Итак, мы имеем, что:
• множество = подмножество_1 + подмножество_2 + … + подмножество_N
• множество = элемент_1 + элемент_2 + … + элемент_N
Запомнили и идём дальше.
Сразу — ага, через несколько абзацев от начала — обозначу два ключевых понятия:
• cast, casting — преобразование;
• coercion — приведение (не путать с ghost, привидением то бишь).
В дальнейшем я буду использовать эти понятия в их русском переводе, потому что это позволит их удобно склонять и создавать производные слова, но вы не затруднитесь напоминать себе англоязычные термины, которые за ними стоят.
Преобразование отвечает на вопрос \"что?\" ( \"что делать?\"), приведение расскажет, \"как\" это происходит. Преобразование указывает программист, привидение (чёрт, я и сам склонен ошибаться!) производит компилятор. Приведение валидно и допустимо, если удовлетворяет правилам приведения. Их много, и их все фиг запомнишь, но вы не расстраивайтесь: в случае чего, компилятор, как и всегда, оплюет вас стэктрейсом, который принесет с собой искру истины в виде сообщения об ошибке. Подробно правила приведения я разберу в следующих постах, которые войдут в открытое этим текстом множество постов о приведениях и преобразованиях (видите, как я ловко проверил вас на предмет усвоения изложенного материала?).
Главная пуанта заключается в том, что приводить элементы одного множества к элементам другого множества можно только в том случае, если эти множества являются подмножествами более общего множества.
Числовые преобразования
Закрепим и завершим первый пост рассмотрением преобразования чисел.
Удивительным образом числовые типы, соответствуя в плане семантики математическим числам, обладают таким свойством, как и различные подмножества всех действительных чисел. Поэтому будет достаточно просто привести пример на основе приведения экземпляров .
Таким образом, для закрепления, когда у вас возникает необходимость представить объект одного типа как объект другого типа, то вы указываете необходимость преобразования, а компилятор производит приведение, согласно правилам приведения. У приведения НЕТ синтаксиса, это строго вотчина компилятора. У преобразования синтаксис имеется, это бинарный оператор as:
let x = +1i8 as u8; // читается это как угодно, один из вариантов: \"элемент `1` из множества знаковых 8-битных чисел представить как аналогичный элемент из множества беззнаковых 8-битных чисел\"
Обратите пристальное внимание на мой комментарий. Как вы догадались, он не просто так написан. Согласно Чехову, висящее на стене ружье обязано выстрелить, и вот наша абстракция совершает пробную пристрелку.
Почему я записал 1 как +1? Знаковый числовой тип потому и знаковый, что его экземпляры имеют знак, указывающий по какую сторону от нуля на числовой оси они находятся, в области отрицательных чисел или же в области положительных чисел. Но в синтаксисе языков программирования принято то же допущение, что и в математике: у положительных чисел знак можно не указывать, потому что он не несет семантического значения (как и в жизни хорошим ребятам прощаются некоторые вольности просто потому, что они положительные). Но об этом не нужно забывать.
Как мы знаем, тип i8 является множеством, охватывающим … а сколько чисел он охватывает? Давайте посчитаем. i8 => для описания требуется 8 битов; каждый бит может иметь 2 значения: 0 или 1 (двоичная система счисления, дамы и господа!), отсюда, используя комбинаторику, получаем размер множества из формулы 2^8 = 256, то есть двойка в восьмой степени. Можно записать и по-другому, используя операцию побитового сдвига:
1 << 8 даёт 256
И как же располагаются эти 256 чисел? От -128 до +127, включая 0 как первый положительный элемент.
В то же время множество элементов типа u8 заключает в себе также 256 чисел, но поскольку тип беззнаковый, то числа эти исключительно положительные с добавлением нуля, то есть диапазон от 0 до 255.
И каким же образом мы можем преобразовать элементы из одного диапазона в элементы из другого диапазона? Здесь нужно вспомнить о свойстве, не присущем числам, но присущем числовым типам при преобразованиях: они кольцевые. В то время, как мат. числа находятся в диапазоне от -∞ до +∞, числовые типы данных, во-первых, не могут рассчитывать на такую роскошь ввиду того, что компьютерная память всегда ограничена какими-то конкретными пределами, а во-вторых, по большей части и не нуждаются в этом. Как мы уже выяснили выше, такой тип, как i8, охватывает всего две с половиной сотни значений. Но что же происходит, когда в результате операций мы выходим за границы диапазона числового типа? Оказывается, границы дипазона \"соединены\" друг с другом так, что диапазон образует кольцо, с тем лишь нюансом, что границы не совпадают, а идут друг за другом: +255_u8 + 1 даст 0_u8, -128_i8 - 1 даст +127_i8. То есть, если мы выходим из диапазона за пределы одной границы на N единиц, то этим самым входим в диапазон со стороны другой границы на N - 1 единиц.
Кстати, в Rust есть compile-time проверка выхода за пределы диапазона для числовых типов (это еще часто называется \"переполнением\") для констант (во всяком случае, работает она на этапе синтаксического анализа), поэтому примеры, которые я указал выше, наподобие +255_u8 + 1, не скомпилируются. Для успешной компиляции их нужно переписать с использованием промежуточной переменной:
fn main() {
let a = +255_u8; // правая граница диапазона типа `u8`
println!(\"{:?}\", a + 1);
}
// выведет: 0 — левая граница диапазона типа `u8`
Вернемся же к поставленному ранее вопросу: каким же образом мы можем преобразовать элементы из одного диапазона в элементы из другого диапазона?
Идентификаторы числовых типов в Rust состоят из двух составляющих: категории и размера в битах. Первая обозначается символом, например, \"i\" в \"i32\", второй числом, например, \"16\" в \"u16\".
Итак, у нас есть число a типа CN (категории C, размера N), которое мы хотим преобразовать в число a1 типа C1N1 (категории C1, размера N1). Какими правилами приведения руководствуется компилятор при таких исходных (здесь мы резко переходим от моего словесного скоморошества к сухому и беспристрастному изложению документации):
Вы должно быть помните, что тыщей строк ранее я объяснял про объединение границ диапазона для его закольцовывания. На практике это трансформируется в следующее: остаток от деления исходного числа a на размер дипазона типа итогового числа a1, т.е. C2N1, в который оно преобразуется, даст нам число a1. Разумеется, если N меньше N1, то ничего переводить и не надо, ибо a1 = a, поскольку больший диапазон включает меньший диапазон той же категории. Также можно заметить, что никаких действий не нужно производить, если a меньше длины диапазона типа C1N1, поскольку нацело на него не делится и само по себе является искомым остатком. Нас интересуют все остальные случаи. Преобразование 257_u16 as u8 после вычисления 257 % 256 даст 1_u8, что нам и нужно.
• C != C1, N == N1 (i32 -> u32): преобразование между двумя целыми одинакового размера является \"нулевой операцией\", NOP. В это трудно поверить, но для подобного преобразования компилятору достаточно сменить у числа метку типа с исходного на конечный, и всё, но это будет сделано в compile-time, а для рантайма будет сгенерирована инструкция NOP, которая, естественно, будет удалена в ходе оптимизаций.
Давайте попробуем разобраться в сути на примере условных типов i2 и u2. Их нет среди числовых типов Rust, но умозрительно с ними будет проще работать. Возможно, вы обратили внимание, что максимальное значение, которое могут выразить знаковые типы, в почти 2 раза меньше максимального значения, которое могут выразить соответствующие беззнаковые типы! При этом для представления значений знаковые типы используют то же количество битов, что и соответствующие беззнаковые! Ситуация еще больше усложнится, если вспомнить, что компьютер представляет всю информацию в двоичном виде, в виде последовательности нулей и единиц, в том числе и числа. ОК, как перевести число из одной системы исчисления в другую мы знаем, но как выразить знаковость числа? \"Плюс\" или \"минус\" просто так не выразишь в двочном виде, это не числа.
Принято соглашение, что значение самого старшего бита знаковых типов указывает на знак числа этого типа: 0 — положительное число (т.е. знак необязателен и отсутствует), 1 — отрицательное число (знак обязателен и присутствует). Беззнаковые типы все биты используют для представления значения, поэтому имеют лишний бит для этого, с чем и связана способность выражать вдвое большее максимальное математическое число.
Типы i2 и u2 содержат по 2^2 = 4 значения. Выпишем их, учитывая соглашение об указании знака в старшем бите у знаковых типов:
i2:
-2 — 10 (знак обязателен, поэтому старший бит содержит 1)
-1 — 11 ^
0 — 00 (знак необязателен, поэтому старший бит содержит 0)
1 — 01 ^
u2:
0 — 00 (знаковый бит отсутствует, все биты служат для выражения значения)
1 — 01
2 — 10
3 — 11
Выпишем парами значения обоих типов, чьи двоичные представления совпадают:
0 0
1 1
2 -2
3 -1
Если проанализировать, то можно сделать заключение, что половина диапазона соответствующих друг другу знакового и беззнакового типа совпадают. Это положительная половина знакового диапазона знакового типа. Дальше начинаются чудеса. Выглядит так, словно дальнейшее соответствие приводит к тому, что отрицательная часть диапазона знакового типа \"приставляется\" после правой границы его диапазона, благодаря чему и достигается закольцованность диапазона, которая оказывается не математической абстракцией, а практическим следствием реализации представления чисел в машине! Действительно, если к двоичному представлению 1_i2 добавить единицу, то получится -2_i2 (01 + 1 = 10), то есть от правой границы происходит переход к левой, а если к двоичному представлению 3_u2 добавить единицу, то получится 0_u2 (11 + 1 = 10; на самом деле получится 100, но лишний старший бит откидывается, поскольку у нас тип ограничен 2-мя бита для представления значений).
Но постойте, остановит меня внимательный читатель. Разве при движении от 0 в отрицательную сторону двоичное представление каждого последующего числа не должно быть больше предыдущего, что не выполняется у нас, ведь соответствующее -2 10 меньше соответствующего -1 11?
Настало время раскрыть карты. Во-первых, в математике как раз чем ближе к 0, тем отрицательное число больше. Во-вторых, подобное соображение действительно справедливо, но для прямого кода. Но на большинстве популярных ныне процессорных архитектур используется дополнительный код (\"two\'s complement\", оригинальный англоязычный термин).
Приведу краткую цитату, чтобы пояснить как были получены двоичные представления для -1 и -2.
Преобразование числа из прямого кода в дополнительный осуществляется по следующему алгоритму:
* Если старший (знаковый) разряд числа, записанного в прямом коде, равен 0, то число положительное и никаких преобразований не делается;
* Если старший (знаковый) разряд числа, записанного в прямом коде, равен 1, то число отрицательное, все разряды числа, кроме знакового, инвертируются, а к результату прибавляется 1.
То есть бинарные представления положительных значений выглядят одинаково как при прямом кодировании, так и при дополнительном. Чтобы получить представления для -1, берем 01, инвертируем (т.е. вместо каждого значения битов записываем обратный: вместо 0 — 1, а вместо 1 — 0), получая 10 (так называемый \"обратный код\"), а прибавив 1, получим 11. Вуаля.
Так как я сам искал примерно минуту в черновике то место, к которому потребовалось приложить столь пространные лирические отступления, повторю его еще раз:
• C != C1, N == N1 (i32 -> u32): преобразование между двумя целыми одинакового размера является \"нулевой операцией\", NOP.
Как мы увидели, соответствующие друг другу значения знакового и беззнакового типа имеют одинаковое бинарное представление, поэтому преобразовывать их каким-либо образом друг в друга не нужно.
• C == C1, N > N1: преобразование из целого большего размера в целое меньшего размера одной категории (u32 -> u8) приведет к усечению пространства, которое используется для представления значения числа, т.е. освободится (N - N1) бит памяти.
Просто выкидываем нужное количество старших битов, и всего-то. Хотя есть один нюанс, ведь самый старший бит у знаковых типов является знаковым, и как быть с ним? Его тоже отбрасывают, НО новый самый старший бит заполняют 1.
• C == C1, N меньше N1: преобразование из целого меньшего размера в целое большего размера (i32 -> u32) приведёт к:
* zero extension, если исходное число беззнаковое целое.
* sign extension, если исходное число знаковое целое.
То есть если число положительное, то добавляем нужное количество разрядов и забиваем новые старшие биты нолями, если отрицательное — забиваем единицами.
Оказывается, это корректно и отлично работает. Если хотите, уделите время, чтобы восхититься еще раз этой великолепной абстракцией под названием \"дополнительный код\".
• Преобразование из числа с плавающей точкой к целому числу приведет к округлению первого в направлении нуля.
ПРИМЕЧАНИЕ: на текущий момент подобное округление приведёт к UB (Undefined Behavior), если округленное значение не может быть представлено конечным целым типом, включая Inf и NaN. Это баг, и будет исправлено.
• Преобразование из целого в число с плавающей точкой произведет соответствующее исходному целому число с плавающей точкой, округленное при необходимости (стратегия округления не определена).
• Преобразование из f32 в f64 совершенно и происходит без потерь.
• Преобразование из f64 в f32 произведет максимально близкое возможное значение (стратегия округления не определена).
ПРИМЕЧАНИЕ: на текущий момент подобное округление приведёт к UB (Undefined Behavior), если значение конечно, но больше или меньше, чем большее или меньшее конечное значение представимое f32. Это баг, и будет исправлено.
================================================================================
Неожиданно для меня материал получился довольно большим. Но остальное займет меньше времени. Стэй тюнед, о\'ревуар.
Примечания:
вообще-то я знаком с Rust\'ом только первый год ;



Сколько лет себя помню изучающим Rust , столько меня изводило весьма смутное понимание, что же такое \"каст\" в Rust. Я понимал, что это выглядит [...] читать полностью
2627


Но когда напишу, будет интересно, обещаю.



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


Для получения результата идем по вложенным пунктам панели меню: Tools > Build System > New Build System…
Открывается новая вкладка с заготовкой конфига новой сборочной системы. Заменяем её на следующий код:
{
\"cmd\": ,
\"selector\" : \"./source.c\",
\"shell\": true,
\"working_dir\" : \"$file_path\",
\"variants\" :
},
{
\"name\": \"Compile && Run\",
\"cmd\" :
},
]
}
Теперь сохраняем файл (Ctrl+S) в той директории, в которой откроется файловый диалог, под любым именем, главное, оставить расширение `.sublime-build`. Далее закрываем за дальнейшей ненадобностью вкладку с конфигом (Ctrl+W), переходим к исходнику, Tools > Build System > %наша_сборочная_система_для_C%.
Но это еще не все. Нажав Ctrl+Shift+B, мы увидим список команд. Это команды, описанные в конфиге. Первая — это корневая \"cmd\", остальные — перечисленные по порядку варианты из \"variants\". В нашем случае названия говорят сами за себя, разберетесь. Выбранная единожды, команда запоминается и далее будет доступна по \"прямому\" шоткату Ctrl+B.
Да, забыл оговорить, что конфигурация предназначена для ОС GNU/Linux, на Windows понадобится указывать параметр \"path\", путь к исполняемому файлу компилятора, а также еще какие-то меры.
===========================================================================================
Update (от 8 мая)
Cargo прекрасная вещь, что касается работы с проектами. Но что делать, если нам не нужен проект, а нужно набросать небольшую программку? Раньше я, чертыхаясь, создавал на скорую руку полуанонимный проект, наспех заполнял Cargo.toml, и только после всей этой бюрократии мог писать и тестировать код. Недавно я увидел в одном тьюториале хорошую идею и написал еще одну сборочную команду.
Назовём её RustScript:
{
\"shell_cmd\": \"rustc $file_name && $file_base_name\"
}
Запускаем компилятор Rust и передаем ему полное имя исходника для компиляции, а затем запускаем получившийся исполняемый файл, чье имя будет именем исходника без расширения \".rs\"
Всё!



Понадобилось мне сегодня потыкать сорец написанный на ЯП Си. Потыкать и пособирать, проверяя его работоспособность. Как я уже писал, для кодинга [...] читать полностью
2658


Из плюсов: в настройках (открываем страницу установленных дополнений по Ctrl+Shift+A или через пункт в меню браузера и ищем мое дополнение) можно указать, какую именно папку с закладками открывать (все же в курсе, что закладки необязательно сваливать в одну кучу, а предусмотрительнее распихивать их по смыслу в разные папки?), а также задавать css-стиль для оформления списка с закладками, что позволяет настроить цвета, размер и шрифты в списке. Удобно, не правда ли?
Зачем оно надо, если в браузере искаропки есть возможность поместить на ту же панель штатную кнопку с закладками, еще и более фичастую? Я не знаю



Есть у меня на AMO (addons.mozilla.org) дополнение для Firefox Quantum (Firefox >= 57.0), My Clean Bookmarks. Ничего особенного, добавляет на панельку, находящуюся справа от [...] читать полностью
1405


Я зашел туда сегодня, чтобы почитать что-нибудь нового в блоге \\\"Rust\\\", хаб \\\"Программирование\\\". Нашел много чего, в частности и переведенный мною недавно материал про типовые состояния. Но ничего оригинального, одни переводы. С*ка, хоть \\\"крестики-нолики\\\" напишите, да расскажите об этом, лишь бы контент был уникальный. Нет, это не значит, что никто не делает с помощью Rust ничего оригинального и интересного. Но популяризовать тоже надо уметь и желать.



Все мы знаем, что такое Хабрахабр, Хабр, Хабра и т.д. А если вы не знаете, то идите отсюда — здесь неинтересное рассказывают.Я зашел туда сегодня, [...] читать полностью
1448


Мой вольный авторский перевод заинтересовавшего меня поста, который я прочитал, просматривая свежий выпуск \"Неделя в Rust\". Оригинал можно найти здесь. Но лучше прочитать мой перевод.
Давным давно, Rust был языком с типовыми состояниями. Официально типовые состояния были выкинуты задолго до Rust 1.0. В этом тексте я раскрою вам секретный секрет сообщества Rust: в Rust по-прежнему есть типовые состояния. БА-ДУМ-ТССС!
Секундочку, а что такое \"типовое состояние\"?
Рассмотрим объект, представляющий файл — давайте обозначим его структурой данных MyFile. До того момента, как MyFile будет открыт, он не может быть прочитан. В случае если MyFile закрыт, он также не может быть прочитан. Между двумя этими состояниями файл может быть прочитан. Типовые состояния это механизм, дающий тайпчекеру возможность проверить, что вы не делаете следующих ошибок:
fn read_contents_of_file(path: &Path) -> String {
let mut my_file = MyFile::new(path);
my_file.open(); // Ошибка: эта операция может завершиться неудачно.
let result = my_file.read_all(); // Невалидно, если `my_file.open()` завершилось неудачно.
my_file.close();
my_file.seek(Seek::Start); // Ошибка: `my_file` уже закрыт.
result
}
В этом примере мы сделали две ошибки:
1. мы прочитали файл, который может не быть открытым;
2. мы установили текущую позицию чтения в файле, который уже был закрыт.
В большинстве языков программирования мы легко можем спроектировать API MyFile так, чтобы быть уверенными, что первая ошибка будет невозможной, просто бросая исключение, когда файл не получается открыть. Некоторые стандартные библиотеки и рамки решают не следовать этому принципу ради гибкости, но возможности существуют в самом языке.
Вторую ошибку намного сложнее перехватить. Большинство ЯП поддерживают необходимые возможности, чтобы сделать эту ошибку сложной для воспроизведения, как правило, закрывая файл при уничтожении или в конце вызова функции высокого порядка, но единственный неакадемический язык, о котором я знаю, который может фактически полностью предотвратить эту ошибку, это Rust.
Тривиальные типовые состояния в Rust
Итак, как мы делаем это в Rust?
Простейший способ сделать это — ввести здоровые типы (ради Бога, если знаете более приличный перевод термина \"sane types\", сообщите мне) для наших операций над MyFile:
impl MyFile {
// `open` это единственный способ создания экземпляра `MyFile`.
pub fn open(path: &Path) -> Result { ... }
// `seek` требует экземпляр `MyFile`.
pub fn seek(&mut self, pos: Seek) -> Result { ... }
// `read_all` требует экземпляр `MyFile`.
pub fn read_all(&mut self) -> Result { ... }
// `close` принимает аргумент `self`, не ссылки `&self` или `&mut self`,
// который означает, что этот метод *перемещает* свой объект, эффективно поглощая его.
pub fn close(self) -> Result { ... }
}
impl Drop for MyFile {
// Определяем деструктор для автоматического закрытия экземпляра `MyFile`,
// если мы еще не закрыли его.
fn drop(&mut self) { ... }
}
Давайте перепишем наш изначальный пример:
fn read_contents_of_file(path: &Path) -> Result {
let mut my_file = MyFile::open(path)?;
// Обратите внимание на `?`. Это простой оператор, который просит
// компилятор проверить успешность выполнения операции.
// *Единственный* способ получить экземпляр `MyFile`
// это иметь успешно завершенным `MyFile::open`.
// В этой строке `my_file` это объект `MyFile`,
// который можно использовать.
let result = my_file.read_all()?; // Корректно.
my_file.close(); // Корректно.
// Поскольку `my_file.close()` поглощает `my_file`,
// эта переменная больше не существут.
my_file.seek(Seek::Start)?; // Ошибка, которую обнаружит компилятор.
result
}
Это работает и в более сложных случаях:
fn read_contents_of_file(path: &Path) -> Result {
// Аналогично.
let mut my_file = MyFile::open(path)?;
let result = my_file.read_all()?; // Корректно.
if are_we_happy_yet() {
my_file.close(); // Корректно.
}
// Поскольку `my_file.close()` поглощает `my_file`,
// эта переменная больше не существует, по крайней мере,
// в данной ветви выполнения кода.
my_file.seek(Seek::Start)?; // Ошибка, которую обнаружит компилятор.
result
// Если мы не закрыли `my_file`, то по выходе из блока кода,
// в котором мы его определили (в данном случае, это тело функции),
// здесь сработает его деструктор, который закроет файл.
}
Ключевой момент заключается в том, что система типов Rust обеспечивает соблюдение того факта, что переменная не может быть использована после перемещения. В нашем случае my_file.close() переместил значение, эффективно поглотив его.
Даже если бы мы попытались скрыть переменную где-то в другом месте и повторно использовать её после вызова my_file.close(), эта попытка была бы пресечена компилятором:
fn read_contents_of_file(path: &Path) -> Result {
// Аналогично.
let mut my_file = MyFile::open(path)?;
let result = my_file.read_all()?;
let mut my_file_sneaky_backup = my_file;
// Здесь мы переместили `my_file` в `my_file_sneaky_backup`.
// Поэтому мы не можем больше использовать.
my_file.close(); // Ошибка, которую обнаружит компилятор.
my_file_sneaky_backup.seek(Seek::Start)?;
result
// Если мы не закрыли `my_file`,
// здесь сработает его деструктор, который закроет файл.
}
Давайте испробуем иной путь, чтобы обмануть компилятор, для доступа к файлу после его закрытия:
fn read_contents_of_file(path: &Path) -> Result {
let my_shared_file = Rc::new(RefCell::new(MyFile::open(path)?));
// Здесь `my_shared_file` это shared указатель на мутабельный
// экземпляр `MyFile`, наподобие обычной
// ссылки в Java, C# или Python.
let result = my_shared_file.borrow_mut()
.read_all()?; // Корректно.
let my_shared_file_sneaky_backup = my_shared_file.clone();
// Мы клонировали указатель, создав второй способ доступа к `my_shared_file`.
// Давайте удостоверимся, что мы можем использовать бэкап
// и оригинальный файл:
my_shared_file_sneaky_backup.seek(Seek::Start)?; // Корректно.
my_shared_file.seek(Seek::Start)?; // Также корректно.
// Ха-ха, теперь-то мы, конечно, можем закрыть `my_shared_file`,
// а потом манипулировать `my_shared_file_sneaky_backup`,
// словно мы в Java, C# или Python!
// Да вот беда, мы не можем вызвать `my_shared_file.close()`,
// потому что укрытый `MyFile` разделен (shared),
// что означает, что никто не может *перемещать* его.
my_shared_file.close(); // Ошибка, которую обнаружит компилятор.
my_shared_file_sneaky_backup.seek(Seek::Start)?;
result
// Если мы не закрыли `my_file`, деструктор закроет его здесь.
}
И снова наши поползновения были пресечены компилятором. В самом деле, если явно не перейти в unsafe режим, мы не сможем разбить инвариант, в котором seek не может быть использован после закрытия.
Этот пример демонстрирует первый кирпич типовых состояний в Rust: типизированную операцию перемещения. Но мы разобрались только с тривиальным случаем, в котором файлы имеют лишь два состояния: открыт и закрыт.
Давайте посмотрим, как мы можем управиться с более сложными случаями.
Сложные типовые состояния
Вместо файлов, давайте теперь рассмотрим следующий (и, по общему признанию, довольно тупой) сетевой протокол:
1. Отправитель отправляет сообщение \"HELLO\".
2. Приемник получает сообщение \"HELLO\", отвечает сообщением \"HELLO, YOU\".
3. Отправитель получает сообщение \"HELLO, YOU\", отвечает случайным числом.
4. Получатель получает номер от отправителя, отвечает тем же номером.
5. Отправитель получает одинаковое количество от приемника, отвечает \"BYE\".
6. Получатель получает \"BYE\" от отправителя, отвечает \"BYE, YOU\".
7. Вернитесь к 1.
Любое другое сообщение игнорируется.
Мы можем спроектировать нашего Отправителя (и аналогично Получателя) так, чтобы гарантировать, что операции выполняются в правильном порядке. На данный момент мы не заботимся об идентификации корреспондента или числа.
Для этой цели мы объединим типовые состояния с другим методом, хорошо известным функциональным программистам, таким как фантомные типы.
// Серия типов нулевого размера (нет полей => нет состояния => в рантайме не существуют),
// представляющих состояние отправителя.
// Значение не имеет значения, только тип
// (отсюда и название \"фантомный тип\").
struct SenderReadyToSendHello;
struct SenderHasSentHello;
struct SenderHasSentNumber;
struct SenderHasReceivedNumber;
struct Sender {
/// Актуальная реализация сетевого I/O.
inner: SenderImpl;
/// Поле нулевого размера, не существующее во время выполнения.
state: S;
}
/// Следующие методы могут быть вызваны независимо от состояния.
impl Sender {
/// Порт, используемый для подключения отправителя.
fn port(&self) -> usize;
/// Закрыть отправителя, раз и навсегда.
fn close(self);
}
/// Следующий метод может быть вызван только в состоянии SenderReadyToSendHello.
impl Sender {
/// Отправить приветствие.
///
/// Этот метод поглощает отправителя в его текущем состоянии
/// и возвращает его в новом состоянии.
fn send_hello(mut self) -> Sender {
self.inner.send_message(\"HELLO\");
Sender {
/// Перемещаем реализацию сетевого I/O.
/// Компилятор достаточно умён,
/// чтобы понять, что это не требует никакой рантайм-операции.
inner: self.inner,
/// Заменяем поле нулевого размера.
/// Эта операция будет вычищена во время выполнения.
state: SenderHasSentHello
}
}
}
/// Следующий метод может быть вызван только в состоянии SenderHasSentHello.
impl Sender {
/// Ждать, пока получатель не отправит \"HELLO, YOU\",
/// ответить числом.
///
/// Вернуть отправитель в состоянии `SenderHasSentNumber`
fn wait_respond_to_hello_you(mut self) -> Sender {
// ...
}
/// Если получатель отправляет \"HELLO, YOU\", ответить числом и
/// вернуть отправитель в состоянии `SenderHasSentNumber`.
///
/// Иначе вернуть неизмененное состояние.
fn try_respond_to_hello_you(mut self) -> Result {
// ...
}
}
/// Следующий метод может быть вызван только в состоянии SenderHasSentNumber.
impl Sender {
/// Ждать, пока получатель не отправит число, ответить \"BYE\".
///
/// Вернуть отправитель в состоянии `SenderReadyToSendHello`
fn wait_respond_to_hello_you(mut self) -> Sender {
// ...
}
/// Если получатель отправляет число, ответить и вернуть отправитель
/// в состоянии `SenderReadyToSendHello`.
///
/// Иначе вернуть неизмененное состояние.
fn try_respond_to_hello_you(mut self) -> Result {
// ...
}
}
Из этого дизайна очевидно, что Отправитель может следовать только такому протоколу:
1. из шага 1 (SenderReadyToSendHello) он может идти только к шагу 3;
2. из шага 3 (SenderHasSentHello) он может только либо оставаться на шаге 3 или идти к шагу 5;
3. из шага 5 (SenderHasSentNumber) он может только либо оставаться на шаге 5 или идти к шагу 1.
Любая попытка нарушить протокол будет заблокирована системой типов.
Если вам когда-нибудь понадобится работать с сетевыми протоколами, драйверами устройств, промышленными устройствами со специфичными инструкциями безопасности или OpenGL/DirectX/чем угодно еще, что потребует от вас выполнения сложных взаимодействий с \"железом\", вы определенно будете счастливы иметь такие гарантии!
Добро пожаловать в мир типовых состояний.
Краткое примечание: за типовые состояния
Продолжая наш сетевой пример, что если мы захотим, скажем, сохранять число, отправленное сервером, чтобы проверить, что означает ответ? Чтобы сделать это, мы можем сохранять число в SenderHasSentNumber:
struct SenderHasSentNumber {
number_sent: u32,
}
И снова, компилятор будет проверять, что код получает доступ к number_sent, только когда отправитель находится в состоянии SenderHasSentNumber.
Мы потеряем (крошечный) объем производительности, компилятор не сможем оптимизировать трансформацию Отправителя между идентичными представлениями, но вообще это того стоит.



Ти́ повые состояния (`typestates`) in RustМой вольный авторский перевод заинтересовавшего меня поста, который я прочитал, просматривая свежий [...] читать полностью
2320


Во-первых, в огнелисе для доступа к API используется корневой объект `browser`, в то время как Chrome в качестве такового использует `chrome`. Во-вторых, несмотря на идентичное API, существует разница и в способе его использования: в Хроме нужно передавать в функции API (методы chrome) колбэки, что провоцирует COP — колбэк-ориентированное программирование, в то время как Файерфоксе функции API возвращают промисы, что, на мой взгляд, гораздо удобнее.
Когда я обнаружил эти отличия, я немного приуныл, поскольку это означало, что при портировании дополнения между браузерами его либо придется переписывать самым обезьянним образом, либо изначально городить диспатчинг.
Но когда зарелизился FF 57, я в ходе разработки обнаружил, что в Файерфоксе заработал и объект `chrome`. К сожалению, я не проверил, является ли он обычным алиасом к `browser` или он копирует поведение хромовского оригинала и позволяет работать с ним с использованием колбэков, а не промисов. Если это так, то это бы значительно упростило портирование дополнений с Хрома в Файерфокс.
Пустяковое замечание, но для истории полезно.



Когда революционная версия 57 еще пребывала в статусе beta, а я начинал делать адд-оны к \\\"огнелису\\\", я столкнулся с тем, что зачастую в примечаниях [...] читать полностью
1558


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



Как я и обещал — а я всегда держу обещания, когда мне это выгодно — пишу о том, что моими растовыми декораторами пользоваться можно, но осторожно. [...] читать полностью
1563