Как дюпать вещи в RUST
Всем привет, дорогие читатели. Сегодня мы поговорим о том, как дюпать вещи в RUST, попутно разобравшись в том, что это такое и возможно ли это делать в принципе. Не будем откладывать в долгий ящик и сразу перейдем к делу.
Что такое дюп вещей в RUST
Дюп, на игровом сленге RUST’a – означает читерское действие, направленное на преувеличение и приумножение своих ресурсов в игре.
Суть его заключается в использовании игровых багов, недоработок. Способов дюпа – существует несколько, и мы попытаемся разобрать несколько примеров, в полной мере показывающих данное действие в RUST.
Командный способ
Для выполнения данного задания – вам понадобится тиммейт. Причем ник его – должен быть таким же, как и у вас. Еще одно обязательное условие – это то, что на сервере должен быть установлен плагин Oxide, без которого ничего не получится, а как переименоваться – вы можете прочитать в соответствующей статье у нас на сайте.
Итак, вы переименовались, зашли на сервер, став, своего рода, клонами. Теперь вам понадобится ввести в чат команду /remove (о ней мы тоже несколько раз уже писали, в одной из предыдущих статей, про удаления). Остается только ударить по предмету, который, якобы, собираетесь удалить.
Бьете по нему и видите, что предмет не удалился, мало того – он клонировался в инвентарь вашего тиммейта. И так можно обузить любые предметы, пока вы не насобираете достаточно ресурсов. Это и есть классический пример дюпа.
Способ дюпа предметов в соло
Для этого вам понадобится сторонняя программа и немного навыков программирования. Для осуществления данного плана – вам будет нужно:
Примечательно, что вам нужно будет успеть все это провернуть за 3 минуты.
Суть этого бага заключается в том, что вы, как бы, отменяете свое последнее действие, но при этом ваш инвентарь успевает обновиться и сохраниться, а сундуки – нет.
Вот так, дорогие читатели, и можно дюпать вещи в RUST. Повторимся, что данное действие – считается читерским, так что за него может прилететь вполне заслуженный бан. Если поймают, конечно.
Прочитать позже Отправить статью на e-mail
Мы не собираем ваши данные и тем более не передаем их третьим лицам Отправить
Чего не стоит делать в Rust, если начали играть в 2021 году
Rust – это необычный симулятор выживания, который привлек к себе внимание огромное количество геймеров. При этом новички часто думают, что в этом проекте нет ничего сложного, и уже с самого начала делают все то, что и в других играх с элементами выживания.
К сожалению, Rust не отличается особым гостеприимством по отношению к новым игрокам, поэтому стартовать бывает довольно сложно. Перед вами подборка главных ошибок, которые делают новички, решившие поиграть в Rust в 2021 году.
Одному будет тяжело
Rust – далеко не самая лучшая многопользовательская игра для одного человека. Здесь есть несколько этапов развития, и добраться до каждого из них можно только за счет продолжительного гринда. Если играть в команде со своими друзьями, то вы гораздо быстрее достигните цели, чем в одиночку.
Также стоит отметить, что 99% других игроков не дадут вам мирно существовать в виртуальном мире игры. Вам постоянно придется отбиваться от обезумивших «дикарей», которые захотят отобрать ваши вещи и ресурсы. Естественно, ни у одного новичка не получится защитить себя от оравы более опытных игроков, поэтому лучше изначально залетать в Rust хотя бы с парой друзей.
Никому нельзя верить
Этот пункт частично противоречит предыдущему, но при этом он еще более важен. Прежде всего вам стоит забыть о том, что взаимодействие с другими игроками в многопользовательских проектах – это норма. Rust вообще не та игра, где нужно объединяться с незнакомыми людьми, чтобы вместе получить больше лута или ресурсов. Здесь вы можете рассчитывать только на себя, и если начнете доверять первому встречному игроку, то очень скоро поймете, почему этого нельзя делать. Особенно это касается товарищей с хорошей экипировкой, которых вы встретите на своем пути.
Дело в том, что в Rust каждый играет сам за себя, а опытные игроки очень часто обманывают новичков самыми разными способами. Незнакомец, который предложит побегать с ним по виртуальному миру и при этом будет носить броню заметно лучше вашей, скорей всего грифер. Это такой игрок, который при первой же удобной возможности просто вас убьет и заберет все вещи. Так что, начиная играть в Rust, никому не доверяйте!
Курс юного строителя
Если вы вдруг не знали, то в Rust есть строительство, и здесь оно играет довольно важную роль. При этом данная механика имеет ряд особенностей, которые придется изучить в самом начале знакомства с игрой, иначе ваши архитектурные «шедевры» будут попросту разваливаться, а вы впустую потратите ценные ресурсы.
Прежде всего стоит отметить, что у каждого строительного блока есть мягкая и твердая сторона. Во время строительства блок всегда нужно устанавливать таким образом, чтобы твердая сторона находилась снаружи будущего здания. Если не соблюдать это правило и размещать материалы как попало, то вашу постройку сможет развалить первый попавшийся игрок, причем с помощью обычного топора или кирки. Согласитесь, будет не очень приятно наблюдать за тем, как несколько часов ваших трудов кто-то разбирает по кирпичикам за считаные минуты.
Все вещи в одном месте
Огромное количество игроков в Rust вообще не уделяют время крафту. Они считают, что гораздо проще украсть готовые предметы у других пользователей, чем стоять у станка и пытаться что-то сделать. Именно поэтому в этой игре противопоказано хранить все свои вещи в одном месте.
Ни в коем случае не размещайте абсолютно все запасы на единственной базе, да еще и в конкретном помещении. В таком случае после случайного налета кучки любителей халявы вы потеряете абсолютно все. Конечно, вряд ли у новичка хватит ресурсов, чтобы построить себе 4-5 домов и правильно распределить по ним ценные предметы, но хотя бы попробуйте сделать что-то подобное. Неплохим решением будет на территории одной базы построить несколько «нычек» и распределить по ним ресурсы и предметы.
Не забывайте про аптечки
Если вы решите, что аптечки вам не нужны и со своим крутым автоматом вы сможете одолеть кого угодно, то Rust очень быстро вас разочарует. Здесь очень просто погибнуть, и иногда вы даже не будете понимать, почему это вообще произошло. В результате игрок, у которого было полно аптечек, просто завалит вас рандомной палкой и заберет тот самый крутой автомат.
Поставьте себе домофон
Если же вы не можете сделать кодовый замок или уже поставили везде обычные двери, то не делайте ключ. Пускай доступ к зданию будет только у вас. Отсутствие ключа гарантировано защитит ваши владения, даже если вы внезапно погибнете.
Не используйте факел
Дело в том, что свет от факела моментально привлечет к вам внимание других игроков. Часть из них будет гриферами, которые быстро прибегут на ваш «сигнал» и просто убьют. На этом ваш многообещающий забег в Rust просто закончится и придется начинать все сначала. Первое время лучше бегайте без факела и пытайтесь ориентироваться на карте с помощью своего зрения.
Вы всегда в опасности
Многие новички ошибочно думают, что после того, как они построят себе укрытие и обзаведутся хоть какой-то экипировкой, можно просто расслабиться и наслаждаться игровым процессом Rust. Этот проект не об этом, вы всегда будете под прицелом у других игроков! Причем если у вас вдруг все слишком хорошо и на это обратят внимание остальные пользователи игровой сессии, то очень скоро вас ждет набег незваных гостей.
Перестрелка – не самая лучшая идея
Некоторые новички в Rust почему-то считают, что это экшен-шутер, в котором прямо-таки необходимо ввязываться в перестрелки и каждую минуту показывать, кто здесь круче. На самом деле проект про выживание, и я вам гарантирую, что ваша беготня с автоматом закончится очень быстро, если вы вдруг решите, что можете держать всю карту в страхе.
Вот такие советы мы решили дать новичкам, которые только надумали залететь в Rust! Делая все эти вещи, вы гарантировано проживете в виртуальном мире игры чуточку дольше и при этом гораздо лучше узнаете все тонкости проекта. Главное, не забывайте всегда быть начеку, здесь нет зоны комфорта.
RUST: Советы и хитрости для начинающих 2021
У RUST есть много ниш внутри себя, таких как электрические и строительные системы, которые могут больше понравиться некоторым игрокам, чем система PVP. Что бы ни интересовало вас больше всего в игре, важно иметь всесторонние знания, потому что независимо от того, цените вы их или нет, вокруг вас всегда существуют параллельные системы.
Совет №1 – выбор сервера.
Выбирая свой первый сервер для игры, вы можете стремиться к менее чем 100 игрокам. А может, даже меньше 50 игроков. Таким образом, вы сможете быстрее прогрессировать и легче изучить механику игры, не убиваясь слишком много раз.
Хотя вы можете подумать, что игра на официальном сервере Facepunch будет лучшим вариантом, эти серверы, как правило, заполнены гораздо более опытными игроками, не имеют ограничений по размеру группы и, как правило, имеют немного более высокий шанс столкнуться с хакерами.
Обычно лучше всего просматривать сервер сообщества или модифицированный сервер с активным персоналом, который будет следить за игроками и запрещать хакеров, читеров или любого, кто злоупотребляет ограничением размера группы, что дает вам больше шансов. Если вы обнаруживаете, что прогресс немного медленный, вы можете попробовать перейти на сервер 2x вместо этого, чтобы немного ускорить работу с Rust.
Я бы не рекомендовал использовать сервер выше 3х, иначе это не совсем Rust. Вы можете проверить всю эту информацию в описании сервера в большинстве случаев, просто щелкнув сервер в списке серверов.
Совет №2: научитесь PVP.
Совет №3: научитесь строить.
Я бы рекомендовал потратить некоторое время на творческий сервер или сервер песочницы, чтобы попрактиковаться в создании базовой базы по умолчанию, которую вы можете использовать при каждой очистке, гарантируя, что вы быстро ее получите. Чем больше раз вы его построите, тем быстрее будете.
Большинство людей используют основание один на два или два на два. Если вы не уверены, есть много руководств по этому поводу на YouTube и других базах для начинающих.
Совет №4: выбирайте, где строить.
Выбор места для строительства – это то, с чем сталкивается каждый игрок в начале каждого вайпа. Независимо от того, сколько часов у вас есть, место, где вы строите свою стартовую или основную базу, может быть разницей между действительно хорошим вайпом и действительно плохим.
Вы хотите строить достаточно близко к деревьям, чтобы вы могли обрабатывать древесину, и достаточно близко к холмистой местности, чтобы были узлы для фермы для камня, металла и серы. Вы также захотите построить довольно близко к дороге и радому городу, чтобы вы могли добывать лом и компоненты. Но будьте осторожны, насколько близко, поскольку они, как правило, являются горячей точкой для других игроков, которые хотят PVP и получить добычу.
Если вы хотите свести к минимуму количество игроков, с которыми вы сталкиваетесь, попробуйте расположиться в заснеженной местности, так как многие игроки стараются избегать этого. Просто не забудьте изготовить и надеть достаточно одежды, чтобы не замерзнуть. По мере того, как вы набираете все больше и больше опыта в игре, вы можете начать строить все ближе и ближе к населенным пунктам.
Совет №5: не фармите слишком много, пока не получите базу.
Не фармите больше, чем нужно, и не фармите там, где вы появляетесь. Бегите прямо туда, где хотите поселиться, и постройте базу, как только у вас будет достаточно ресурсов. Чрезмерное количество фарма – одна из основных причин того, что люди рано умирают и бросают курить, потому что они продолжают терять все свои кровно заработанные материалы и предметы.
Если у вас есть база, даже если она одна за другой, вы можете хранить свою добычу каждые несколько минут, чтобы минимизировать свои потери.
Совет №6 – спальные мешки.
Поступая так, вам не придется сталкиваться с длинными мешками каждый раз, когда вас убивают. Вы можете посмотреть мои другие видео с советами и приемами, чтобы увидеть, насколько близко могут быть друг к другу сумки, прежде чем они начнут показывать таймер возрождения.
Мы также хотим использовать это на свежем вайпе при запуске от начальной точки появления до того места, где вы хотите построить. Когда у вас будет достаточно ткани, создавайте сумки и размещайте их по пути туда, где вы хотите построить, как путевые точки. Если вы умрете, вы можете возродиться в одной из этих сумок, которая находится не только подальше от населенной зоны возрождения, но также немного ближе к тому месту, где вы хотите построить в конце.
Совет № 7 – смотреть назад.
Хотя это может показаться глупым, он может пригодиться, когда вы бежите, фармите или занимаетесь чем-то еще. Если вы нажмете alt, вы сможете свободно смотреть, не меняя направления бега или взгляда.
Это означает, что вы можете фармить узел или дерево перед собой, глядя назад и в сторону. Вы также можете осматриваться, пока бежите по прямой, что действительно помогает увидеть, не гонится ли за вами кто-то.
Совет № 8 – это сельскохозяйственный лом.
Если вы обнаружите, что сельское хозяйство на дороге не очень хорошо для вас, потому что вы продолжаете умирать для других игроков или не можете найти никакой добычи, то это отличная идея попробовать выращивать лом в океане. Не так много людей склонны собирать металлолом в океане, особенно в начале вайпа, поэтому есть больше возможностей найти больше добычи и меньше шансов умереть. Вам просто нужно научиться использовать лодки в Rust, и оттуда это довольно просто.
Хотя у лодок действительно низкая стоимость топлива, вы обычно можете покрыть это разбиванием красных бочек, которые вы собираете на найденных плотах. Это означает, что поддержание ваших морских трофеев практически не требует затрат.
Если вы найдете снаряжение для дайвинга во время сельского хозяйства, вы также можете использовать его для сбора подводных ящиков, которые можно узнать по плавающей бутылке на поверхности воды. На их получение уходит немного больше времени, но они, как правило, содержат лучшие компоненты и ресурсы.
Совет № 9 – выращивание ресурсов.
Если вам сложно фармить ресурсы, потому что вы не можете их найти или снова продолжаете умирать, это поможет. Попробуйте отправиться в более тихую часть карты для фарма. Основные ресурсы, которые вам нужны, – это древесина, металл, камень, сера, ткань и продукты животного происхождения, такие как жир, для получения низкосортных продуктов.
Древесину можно более или менее найти где угодно на карте. В первую очередь в зеленых зонах и более густо в лесных районах, которые можно найти в коричневатых областях карты. Дерево, металл и камень снова можно найти где угодно на карте. Однако гораздо более плотно он появляется в горах на карте, которые обычно белые.
Совет № 10 – это почетные упоминания.
Если у вас более 100 часов в ржавчине или у вас есть другие полезные советы, обязательно оставьте комментарий ниже.
Куча способов переиспользовать код в Rust
Я нашел эту статью авторства Alexis Beingessner как наиболее понятное описание системы типов в Rust, и что с ними можно делать. Надеюсь, кому-нибудь этот перевод будет полезен. Не смотрите на то, что вначале описываются очевидные вещи — под конец можно утонуть. Статья огромная и скорее всего будет разобрана на главы. Переведено достаточно вольно. Авторский стиль сохранен. — прим.пер.
(статья написана о Rust 1.7 stable)
В системе типов Rust есть много всякого. Насколько я знаю, практически вся сложность этого всякого заключается в том, чтобы выразить программу в максимально обобщённом виде. Притом народ еще и требует большего! У меня всегда были проблемы с простым пониманием наиболее сложных вещей, потому этот пост скорее напоминалка самому себе. Но тем не менее, мне также нравится делать что-то, полезное другим, поэтому в данной статье также есть вещи, которые я вряд ли забуду, но о которых некоторые могут не знать.
В этой статье не будет исчерпывающего описания синтаксиса или общих деталей описываемых возможностей. Здесь рассказывается, почему происходит так или иначе, так как подобные вещи я всегда забываю. Если вы нашли эту статью в попытках выучить Rust полноценно, вам определенно стоит для начала ознакомиться с Книгой (оригинал вот — прим.пер.). В то же время я здесь буду уточнять некоторые произвольные теоретические аспекты того, что происходит.
Скорее всего, в этой статье полно ошибок, и она не должна претендовать на звание официального руководства. Это просто сборник того, что я накопал за неделю, пока искал новую работу.
Краткое описание принципов переиспользования кода
(здесь и далее под «переиспользованием» я подразумеваю «повторное использование» — звучит не так неуклюже, понимается быстрей — прим.пер.)
Желание использовать части кода более одного раза существует с тех самых ранних времен, когда самые первые вычислительные машины получили свой первый полезный результирующий бит. Определенно, я не имею ни малейшего представления о том, как в то прекрасное время выглядело переиспользование кода. Может, листики-шпаргалки? Или стопки перфокарт? Понятия не имею. Мне интересней, как это делается сейчас.
Наиболее известная форма повторного использования кода — это, безусловно, функция. Ну как бы да, функции привычны всем. Однако, в зависимости от того, на каком языке вы пишете и что вам необходимо сделать, возможностей функций как приема переиспользования кода может быть недостаточно. Возможно, вам требуется применить что-то, существующее под современными терминами «метапрограммирование» (когда код создает сам себя) или «полиморфизм» (когда код можно применить для различных типов данных).
Технически эти принципы совершенно разные, тем не менее их часто приходится использовать вместе. В современных языках реализации этих принципов представлены достаточно широко: макросы, шаблоны, дженерики, наследование, указатели на функции, интерфейсы, перегрузка, группировка (union) и так далее. Однако, все это лишь семантическое разнообразие реализации трех основных принципов — мономорфизм, виртуализация, перечисление.
Мономорфизм
Мономорфизм — по сути практика копипасты куска кода, с незначительными изменениями в каждой новой копии. Главная выгода мономорфизма — возможность «идеально кастомизировать» реализацию, не пугая компилятор сложными конструкциями. Это же и главный недостаток принципа — в худшем случае мы получим изрядно растолстевший код, из-за множества практически идентичных частей, которые физически скопированы во все места, где используются. К толстому бинарнику и увеличенному времени компиляции здесь добавляется и чудовищная нагрузка на кэш инструкций в процессоре. По сути, никаким переиспользованием кода здесь и не пахнет!
Семантическое ограничение мономорфизма в том, что его нельзя использовать (напрямую) в обработке нескольких различных типов данных одновременно. К примеру, я хочу построить очередь исполнения (job queue), принимающую различные задачи, и с ее помощью выполнить эти задачи в порядке поступления. При условии, что все задачи идентичны, все решается мономорфизмом довольно просто. Проблемы появляются, когда задачи различны — становится неясно, как это реализовать только мономорфизмом. Поэтому и название у него — мономорфизм. Абстракция над кодом, который делает только что-нибудь одно.
Распространенные примеры мономорфизма: шаблоны С++, макросы С, Go Generate, дженерики C#. Большинство из них работает при компиляции, кроме дженериков С#, которые мономорфируют во время выполнения кода. Все, что создается во время компиляции — шаблон. Мономорфизм жутко популярен как средство оптимизации при обычной (inline) и JIT-компиляции.
Виртуализация
Прямая противоположность мономорфизма, к которой приходит каждый разработчик, наигравшись в копипаст: прикрутить вариативность применения. И данные, и исполняемый код могут быть виртуализированы, после чего все, что видит пользователь виртуального интерфейса — тот на что-то указывает.
Виртуализация позволяет коду работать с типами различного размера и структуры совершенно одинаково. Виртуализация функции позволяет ей иметь альтернативное поведение без необходимости копипаста. Пример с очередью исполнения, на котором мономорфизм ломает зубы, прекрасно решается виртуализацией — любая задача, которую необходимо выполнить, являет собой указатель на функцию, которую можно найти и запустить. Нужны данные отдельно для каждой задачи — без вопросов, добавляем еще указатель на данные, они подгрузятся вместе с функцией.
Главный недостаток виртуализации — она обычно влияет на производительность, вариативность кода выливается в частое выделение памяти в куче, прыжки по указателям (кэш негодует) и определение, с чем же именно мы в данный момент имеем дело.
Однако виртуализация может быть производительней мономорфирования! Каждый раз, когда функция дергается статически, компилятор способен заинлайнить ее, но делает это не всегда, так как, как уже было сказано, это перегружает и замусоривает бинарь. По тем же причинам выгодной бывает насильная виртуализация редко использующихся функций. К примеру, хендлерам исключений вовсе нежелательно постоянно запускаться, и лучше их виртуализировать, расчистив таким образом кэш инструкций для «безошибочной» ветки выполнения.
Распространенные примеры виртуализации: указатели на функцию и пустые (void) указатели в С, обратные вызовы, наследование, дженерики Java, прототипы Javascript. Заметьте, что во многих из этих примеров нет различия между виртуализацией данных и исполняемого кода. Например, если у меня указатель на Животное, за ним может стоять и Кот, и Пёс, и когда я прошу это Животное подать голос() — он же откуда-то знает, сказать ему «Гав» или «Мяу»?
Обычный способ реализации виртуализации для каждого объекта каждого типа — иерархия наследований для скрытого хранения указателей на разные куски имплементаций, которые могут понадобиться в процессе работы программы, называемая «vtable». Обычно в vtable хранят пачку указателей на функции (в том числе и на голос() из примера выше), но могут также и размер, выравнивание в памяти, конкретный тип объекта.
Перечисления
Перечисления (enums) это компромисс между виртуализацией и мономорфизмом. Во время выполнения мономорфированный код может быть только один, без вариантов, виртуализированный может быть каким угодно. Код из перечисления может быть любым из ограниченного списка вариантов. Обычно использование перечислений заключается в работе с неким целочисленным «тегом», определяющим вариант из списка, который нужно использовать.
Например, наша очередь исполнения, реализованная перечислением, может определять три типа возможных задач, «Создать», «Изменить», «Удалить». Для использования, к примеру, «Создания», нужно всего лишь отправить очереди данные для Создания, помеченные тегом, соответствующим функции «Создать». Очередь видит тег, понимает из него, что от нее хотят и что лежит в данных, и запускает соответствующий код.
Как и в виртуализации, перечисление может понимать разные типы данных с помощью одного и того же кода, который теперь нет необходимости копировать. Как и в мономорфизме, здесь нет нужды в вариативности — меняется только тег. Кроме того, оптимизировать перечисления существенно проще.
Нужно, однако, отметить, что если вариативность не использовать совсем, перечислимый тип серьезно разрастется, так как каждому объекту типа придется хранить информацию для наибольшего типа, который присутствует в перечислении. Для того, чтобы «Удалить», хватит и только имени, а вот «Создать» попросит имя, тип, автора, содержимое, и так далее, и даже если так случилось, что очередь используется в основном для «удаления», памяти она будет просить как для постоянного «создания».
Ну и разумеется, вам нужно знать наперед весь ассортимент возможностей, это главное ограничение перечислений. И мономорфизм, и виртуализацию можно расширить при необходимости в любой момент, чего не скажешь о перечислении — шаблон можно наложить на новый тип, класс можно унаследовать, а перечисление уже выжжено в коде намертво. Лучше не ковырять его с попытками обмануть и расширить — вы с большой долей вероятности сломаете код тех, кто уже его использует!
Поэтому данная стратегия отчасти невразумительна. Много у каких языков она встречается в виде enum, тем не менее ее использование серьезно ограничено, из-за невозможности ассоциировать данные отдельно для каждого варианта в перечислении. С позволяет определить вариант как группу из двух типов, но решение вопроса, что из этих типов данные, а что код, взваливается на пользователя перечисления. Во многих функциональных языках есть группы с тегами (tagged unions), которые являются объединением перечислений и группой в С, позволяющим приклеить произвольные данные к разным вариантам перечисления.
А как в Rust?
Макросы
Тут все просто. Чистое переиспользование кода. В Rust они работают поверх основного синтаксического дерева (AST, abstract syntax tree) — кормишь макрос синтаксическим деревом, результатом получаешь другое дерево. Информации о типах вроде «хм, эта строка похожа на чьё-то имя» в макросах нет (на самом деле немного есть — прим. пер.).
Обычно макросы используют по двум причинам: расширить сам язык или сделать копию существующего кода. Первый местами открыто используется в стандартной библиотеке Rust (println!, thread_local!, vec!, try!, и всё такое):
а последний используется внутри для реализации многих повторяющихся интерфейсов:
Насколько мне представляется, макросы это наихудший из предоставленных способов переиспользования кода. Они какбе должны помогать (имена переменных не используются внутри и не утекают из макроса), но много где ими чересчур увлекаются (использование unsafe в макросах дает странные побочные эффекты (интересно, какие? — прим. пер.)). В основе обработчика макросов лежит регулярное выражение (если закрыть глаза на то, что expr и tt парсить совсем не тривиально), и вообще, регулярки никто читать не любит!
Более важно, ИМХО, что макросы тут по сути метапрограммирование с динамической типизацией. Компилятор не проверяет, что тело макроса соответствует его сигнатуре, он генерирует согласно макросу код, получает что-то на выходе, и только тогда производит проверку, что приводит к типичной проблеме динамического программирования — поздняя привязка ошибок. Так мы можем получить аналог нетленного «undefined is not a function» для Rust:
Вот что тут за ошибка? Конечно, я забыл про $, потому макрос понимает name не как переменную, а как литерал и всегда отдает
(если честно, так себе повод относиться к макросам прохладно — прим. пер.)
Далее, если в сгенерированном по макросу коде вылезет обычная ошибка, в логах будет неперевариваемая каша:
Ну… зато у нас в плюсах, как и у других динамически типизированных языков, существенно больше гибкости в выражениях. Короче, макросы прекрасны в тех областях, где их применение оправдано, они просто… хрупкие, что ли.
Стоит упомянуть: расширение синтаксиса и генерация кода
Конечно, у макросов есть пределы. Они не выполняют произвольный код во время компиляции. Это хорошо для безопасности и частой сборки, но иногда мешает. В Rust это исправляемо двумя способами: расширения синтаксиса (известные как процедурные макросы) и генерация кода (build.rs) (в нестабильной ветке языка еще есть плагины к компилятору — прим. пер.). Все они дают вам зеленый свет для выполнения чего угодно для генерации чего угодно.
Расширения синтаксиса выглядят как макросы или аннотации, но у них есть способность просить компилятор выполнить произвольные действия для (в идеале) изменения дерева синтаксиса. Файлы build.rs понимаются пакетным менеджером Cargo как что-то, что нужно собирать и запускать каждый раз при сборке пакета. Очевидно, что им позволено копошиться в проекте как заблагорассудится. Ожидаемо, что лучше это использовать для недоступной макросам генерации кода.
Я бы еще мог добавить пару-тройку примеров, но особо не в теме этих возможностей, и совершенно к ним равнодушен. Ну генерация кода, и ладно. И вообще, я эту статью уже не первый день пишу и основательно подзадолбался (авторский капс убран — прим. пер.).
Перечисления
В точности описанные ранее группировки с тегами.
Чаще всего встречаются в лице Option и Result, выражающие успешный/неудачный результат чего-либо. То есть, это буквально перечисления с вариантами «Успех» и «Поломка».
Можно и свои перечисления написать. Вот, к примеру, вам надо код работы с сетью, который работает с ipv4 и ipv6. Вам совершенно точно не нужна возможная поддержка гипотетического ipv8, да и будь он даже необходим, все равно пес его знает, что с ним в коде делать. Пишем перечисление для того, что точно есть:
Всё. Дальше можно работать с общим типом IpAddress, а если кому необходимо узнать точный тип внутри, его можно способом, описанным выше, выудить с помощью match.
Трейты
До этого момента все было просто, сейчас пойдет посложней и поинтересней.
Если коротко, трейты в Rust предназначены для описания всего остального. Мономорфирование, виртуализация, рефлексия, перегрузка операторов, преобразование типов, семантика копирования, потокобезопасность, функции высших порядков, итераторы для циклов — весь этот цветастый паноптикум работает через трейты. Далее, все новые пользовательские возможности языка наиболее вероятно реализуются через трейты.
А вообще трейты это интерфейсы. Нет, серьезно.
Очень часто общение с трейтами не отличается от общения с интерфейсами в Java или С#, но иногда приходится переступать черту. Трейты задуманы архитектурно более гибкими. В С# и Java реализовывать MyTrait для MyType может только владелец MyType. В Rust такое разрешено и для владельца MyTrait. Это позволяет авторам библиотек с трейтами писать их реализации также для, скажем, типов из стандартной библиотеки.
Безусловно, пускание такой фичи на самотек весьма чревато — мало ли что кому куда взбредет реализовывать. Потому это счастье ограничено видимостью имплементаций только для того кода, у которого есть в области видимости соответствующий трейт. Отсюда, кстати, все проблемы с работой с вводом-выводом, если не импортировать Read и Write явно.
В тему: согласованность
Знакомые с Haskell могут увидеть в трейтах много общего с классами типов (type classes). Они же вправе задать совершенно очевидный и обоснованный вопрос: а что, собственно, будет, если реализовать один и тот же трейт для одного и того же типа несколько раз в разных местах? Вопрос согласованности, то есть. В согласованном мире есть только одна пара реализации трейт-тип. А в Rust для достижения согласованности существует большее количество ограничений, чем есть таковые в Haskell. Ограничения же сводятся к следующему — вы должны быть владельцем либо трейта, либо типа, что его реализует, а также у вас не должно быть круговых зависимостей.
Это красиво, просто и понятно, но немного неправда, так как вы можете нарисовать что-то вроде:
даже если вы понятия не имеете, где физически находятся Trait и MyType. Корректная обработка подобных манипуляций и является основной сложностью согласованности. Она регулируется т.н. «правилами уникальности» (orphan rules), которые требуют условия, что для всей паутины зависимостей лишь один крейт содержит реализацию трейта для определенной комбинации типов (о комбинациях будет ниже — прим.пер.). В результате две различные библиотеки, содержащие конфликтные реализации, будучи импортированными одновременно, просто не скомпилируются. Это иногда раздражает до такой степени, что Нико Матсакиса хочется натурально проклять (Niko Matsakis, один из главных коммиттеров Rust — прим.пер.).
Забавно, что нарушения согласованности в стандартной библиотеке Rust (которая внутри склеена из нескольких непересекающихся частей) встречаются сплошь и рядом, поэтому некоторые трейты, имплементации и типы всплывают довольно в неожиданных местах. Еще более забавно, что тасовать так типы помогало не очень, в результате чего родился костыль #[fundamental], приказывающий компилятору закрывать на несогласованность глаза.
Дженерики
(я понимаю, что правильно их назвать «обобщенные типы», но это во-первых, долго, во вторых, менее понятно, так как все всё равно пользуются термином «дженерики» — прим.пер.)
Так как же использовать трейты для повторного использования кода? Тут Rust нам дает выбор! Мы можем мономорфировать, можем виртуализировать. Мономорфизм в подавляющем большинстве случаев является выбором в стандартной библиотеке, а также в большей части кода, что я видел. Вероятно, это потому что мономорфизм в целом более эффективен, а также явно более обобщен. Тем не менее мономорфный интерфейс можно виртуализировать, что я чуть позже покажу.
Мономорфный интерфейс реализуется в Rust дженериками:
Фух.
Как мы видим, как только встанет необходимость определять интерфейсы и их реализации, выбор у нас богат для разных вариаций обобщенности. А под капотом компилятора, как я уже говорил, все это мономорфизируется. Как минимум до первой оптимизации мы получим вот такой промежуточный код:
Возможно вы удивитесь (или не удивитесь), но некоторые важные функции заинлайнены очень много где. К примеру, brson нашел в коде Servo более 1700 копий Option::map. В общем верно, виртуализация всех этих вызовов убьет производительность рантайма напрочь.
Тоже важно: определение типа и оператор «турбо-рыба»
(я не могу перевести «turbofish» лучше. варианты приветствуются — прим.пер.)
Дженерики в Rust определяют тип автоматически. Если тип где-то указан, все работает как часы. А если не указан, начинается фейерверк:
Трейт-объекты
Так как же у нас происходит виртуализация? Как мы удаляем информацию про конкретный тип, чтобы стать просто безликим «чем-то»? У Rust это происходит с помощью трейт-объектов. Вы просто говорите, что данный экземпляр типа это экземпляр трейта, а компилятор делает все остальное. Разумеется, вам также нужно абстрагироваться от размера экземпляра, потому мы также прячемся за указатель, вроде &, &mut, Box, Rc, Arc:
Обратите внимание, требование прятать конкретный тип за указателем имеет больше последствий, чем может показаться на первый взгляд. Вот, например, наш старый знакомый трейт:
Трейт определяет функцию, которая возвращает экземпляр собственного типа по значению.
А вот сколько места надо зарезервировать на стеке для y? Какого он вообще типа?
Ответ в том, что мы не знаем этого на этапе компиляции. Это говорит о том, что трейт-объект Clone по факту бессмысленен. Точнее, трейт не может быть превращен в трейт-объект, если в нем есть упоминание собственного типа как значения (а не указателя — прим.пер.).
Трейт-объекты реализованы в Rust довольно неожиданным образом. Давайте вспомним — обычно для таких целей применяются виртуализируемые таблицы функций. Есть минимум две причины, которые в данном способе раздражают.
Первая — все хранится за указателем, независимо от того, есть ли в этом необходимость. То есть если тип определен как виртуализируемый, то хранить этот указатель нужно всем экземплярам типа.
Вторая — получить нужные вам функции из виртуальной таблицы задача не такая и тривиальная. Это все из-за того, что интерфейсы это в общем-то частный случай множественного наследования (у С++ множественное наследование полноценное). В качестве примера вот вам такой набор:
Как организовать хранение указателей на функции в случае смешанных типов, таких как Животное + Питомец, или Животное + Кошачьи? Животное + Питомец состоит из Cat и Dog. Мы их подравняем по указателям:
А теперь Cat и Tiger непохожи. Ок, поменяем местами Питомца и Кошачьи у Cat:
Ээ, теперь Cat и Dog различаются. Переклеим разметку под функции еще раз, вот так.
Хорошо. Только это не масштабируется. Получается, у каждого интерфейса должно быть свое уникальное смещение, чтобы каждая виртуальная таблица функций могла теоретически вместить любой необходимый интерфейс, но это также значит, что каждая таблица должна хранить информацию о всех без исключения интерфейсах. Да, неиспользуемое место в конце таблицы можно и обрезать, но это слабое утешение, памяти расходуется слишком много. К тому же мы не можем знать смещения интерфейсов, которые импортированы из динамических библиотек. Поэтому в большинстве языков таблицы функций определяются только во время выполнения программы.
Однако к Rust это все не имеет отношения. Rust не хранит виртуальные таблицы в типах. Трейт-объекты в Rust — это так называемые толстые указатели. &Pet это не один указатель, а два, на данные и на виртуальную таблицу. Виртуальная таблица трейт-объекта, в свою очередь, не привязана к определенному типу. Она уникальна для каждой комбинации типов.
Аналогично, у наборов Животное + Питомец и Животное + Кошачьи разные таблицы. То есть, таблицы функций мономорфизированы для каждого уникального набора типов.
Данный подход полностью устраняет проблему строения виртуальной таблицы функций. Значения, не принимающие участие в виртуализации, не хранят дополнительных данных, и можно статически определить, где в любом типе, реализующем Питомца находится та или иная функция Питомца.
Недостатки у этого способа тоже есть. Толстый указатель занимает вдвое больше места, что можеть обернуться проблемами, если указателей много. Мы также мономорфизируем таблицу функций для каждой запрошенной комбинации типов. Это возможно благодаря тому, что мы можем узнать статически тип каждого объекта в некоторый определенный момент времени, и все приведения к трейт-объектам в том числе. Заменить здесь мономорфирование виртуализацией чревато серьезным падением производительности. (а еще поэтому в языке очень ограниченная возможность приведения типов — прим.пер.).
Внимание: возможность придраться!
Толстые указатели можно в принципе обобщить дальше, до тучных указателей. Как «толстый указатель», тип Животное + Кошачьи указывает на одну общую виртуальную таблицу, тем не менее нет причин не разделить эту таблицу на две, отдельно для каждого трейта, наградив тип двумя на них указателями, соответственно. В теории, этим можно ограничить мономорфизм таблицы, за счет еще сильнее растолстевших указателей. Данная идея регулярно всплывает, но серьезно за нее никто не берется.
Наконец, вспомним недавнее утверждение — мономорфный интерфейс пользователь может сделать виртуализированным. Это возможно благодаря такой штуке, как «реализовать трейт для трейта» (impl Trait for Trait), а точнее — трейт-объект реализует собственный трейт (имхо самая крутая фича системы типов — прим.пер.). В итоге вот такой код валиден:
Ассоциированные типы
Какие последствия того, что мы определяем нечто как обобщенное по некоторому типу? И что мы вообще хотим этим выразить? Собственно, и выражение, и последствие здесь одно, мы хотим определить, как работать с типом, который нам подсунут. По факту — у нас тип это входящий параметр. struct Foo говорит, что мы можем собрать полноценный тип только из Foo и Т вместе, Foo сам по себе незавершенный. Если у вас страсть к специальным терминам, вы можете сказать, что Foo — конструктор типов — функция, принимающая тип как аргумент, и возвращающая тип как результат. То есть тип высшего порядка.
Ну ок, а мы тут при чем? Это можно показать на примере итераторов:
Получается, мы совершенно не в восторге, что Т как входящий тип для Iterator — это тот же входящий тип Т для StackIter. Тем не менее от этого никуда не деться, ведь мы как пользователь не можем хардкодить выдаваемые Iterator::next() типы. Эту информацию нам обязан выдавать тип, реализующий итератор!
В этот не слишком радостный момент пора познакомиться с ассоциированными типами.
Ассоциированные типы позволят нам указать, что реализация трейта должна указать дополнительные типы, ассоциированные с определенной реализацией. То есть сказать, что трейту требуются конкретные типы точно так же, как и конкретные функции. Вот вам соответствующим образом переделанный Iterator:
И вот теперь нам нельзя имплементить Iterator несколько раз. Хотя ассоциированные типы могут быть обобщенными, их нельзя определить отдельно от других типов, как, например, вот тут:
Поэтому ассоциированныым типам можно вправе дать название «исходящие типы«.
Так, мы ограничили реализацию трейта ассоциированными типами, что это нам дает еще? Теперь нам можно выразить что-нибудь эдакое, недоступное ранее?
А то!
Вот машина состояний (наш слегка подправленный итератор):
Итак, это тип, экземпляр которого мы можем попросить совершить step(), результатом которого будет превращение его в другой экземпляр этого же типа. Выразим его дженериком.
… и радостно получаем бесконечною рекурсию типов. Так как обобщенный тип дженерика — входящий, его должен определить пользователь трейта. В данном случае этот тип — сам трейт. Тем не менее, можно обойтись и без ассоциированных типов — у нас же есть виртуализация!
Здесь мы нарисовали нашу старую машину состояний, только без ассоциированных типов. Оригинальный экземпляр мы поглощаем (self у нас не заимствованный, то есть после завершения step() его использовать уже нельзя — прим.пер.), на выходе получаем что-то, реализующее машину состояний. Но чтобы это работало, нам нужно ограничить возможности использования всех машин состояний только их реализациями в куче, а еще мы теряем сведения о конкретном типе машины после первого же вызова step(). С ассоциативными типами ни первого, ни второго не происходит.
А, вот еще: трейт-объекты не работают с ассоциированными типами. По той же причине, почему они не работают с Self по значению — неизвестен конкретный реализующий тип. Способ заставить их подружиться — указать все конкретные типы. Box выругается, а Box > вполне себе заведется.
Условия «Where»
Теперь же мы умеем в ассоциированные типы — чем нам это поможет?
И тут решение есть — условие «Where».
Where позволяет ограничивать трейты произвольными типами. Ни больше, ни меньше. Это более гибко, чем инлайнить тип в сигнатуре. Where можно совать в реализацию трейта, определения трейта, функции, структуры, перечисления — грубо говоря, везде, где есть обобщенные типы.
Кроме того, что некоторые развеселые определения могут с ходу вынести мозг (прочтите кто-нибудь impl Send for MyReference where &T: Send с первого раза (а дальше будет еще веселей — прим.пер.)), у Where взаимодействие с трейт-объектами также сопровождается некоторыми спецэффектами. Помните, я говорил, что трейт, подразумевающий обращение к самому себе по значению, использовать в трейт-объекте нельзя? Короче, спецэффектами можно это поправить:
Ограничения трейтов высшего порядка
Если к этому моменту крыша у вас еще не поехала — вам просто повезло, так как следующий раздел шансов ей не оставляет. Тут будет происходить и описываться откровенно мутное болото, удовольствие от ковыряния в котором могут получить только отъявленные фанатики накрученных систем типов.
Сейчас мы будем писать функции высших порядков, что, в свою очередь, есть «функции, работающие с функциями». Прекрасный и всем известный пример — map():
Пятью чашками кофе назад вы могли наткнуться на ненавязчивый комментарий, что Rust это делает через трейты. «Ааа, ооо, чудесные трейты, было дело, да», скажете вы, и немедля добавите «но это же просто трейты!». Вот они: Fn, FnMut и FnOnce. Сейчас для нас их отличия несущественны, и мы просто будем брать из них тот, который красивее ложится на то, что мы хотим объяснить.
Заимствования без времен жизни — ребус в чистом виде. Железное правило: если заимствование это часть типа — приложи время жизни! Другое дело, что Rust достаточно мудр, чтобы не напрягать нас этом правилом в 99% случаев, просто подставляя дефолтное время жизни (еще один термин, который нет особого смысла переводить дословно, им и так все пользуются — прим.пер.).
Получается, get_first у нас на самом деле вот такой монстр:
Еще одно правило: заимствования в сигнатуре функции значит, что функция обобщена относительно них. Соответственно, реализовывать надо обобщенный трейт:
Все страньше и страньше. Нам надо, чтоб pred() работал с временем жизни &val. Увы, явно назвать это время жизни мы не способны, даже если мы повесим условие Where на next() (вообще нам Iterator так не позволит). Данное время жизни просто появляется и исчезает внутри функции, мы не можем его ни выделить, ни назвать. И нам еще и надо, чтоб pred() работал с тем-чье-имя-нельзя-назвать! Полезем в лоб — потребуем от pred() работы со всеми временами жизни. Внезапно:
То есть for читается почти дословно: для всех ‘a (for all ‘a)!
Назовем это ограничением трейта высшего порядка (higher rank trait bound (HRTB)). Вам с ними работать не надо, разве что вас уже всосало в какое-нибудь болото со сложными структурами типов. Обычно HTRB показываются при работе с функциональными трейтами, да и там накрыты синтаксическим сахаром, то есть часто прозрачны для пользователя. Еще на данный момент ограничения трейтов высших порядков работают только с временами жизни.
Типы высших порядков
Например, нам нужна структура данных, которая использует считающие ссылки указатели (reference-counted pointers). Rust предлагает два варианта, Rc и Arc. Rc производительный, Arc потокобезопасный. Допустим, с точки зрения имплементации в нашей структуре эти типы полностью взаимозаменяемы. А вот для пользователей структуры очень важно, какой именно указатель-счетчик используется.
Конечно же мы хотим, чтоб наша структура была обобщена относительно Rc и Arc. В идеале мы бы написали эдакое:
что, как мы уже знаем, просто сахар для
Раздражающим здесь пунктом является факт, что мы вхардкодили заимствование как крайний объект, торчащий из типа наружу, то есть нам Self::Item надо где-то хранить. С удовольствием решили бы данный вопрос, выбросив это заимствование:
Вроде еще красивей и обобщенней, ведь можно написать Self::Item = &mut T, и теоретически мы довольны. Пока не замечаем, что Item тихонько превратился в конструктор типов, а их обобщать нельзя!
Хотя если хорошенько попросить компилятор, то можно. Но я вам этого не говорил. Не палите, пожалуйста, контору.
Ключевое знание тут — понимание того, что у трейта есть входящие и исходящие типы, то есть трейт это функция поверх типов. Вот смотрите:
Конструктор типов же! Пошли имплементить обобщенный итератор счетчиков ссылок RefIter:
Не уверен, что у меня получится расшифровать происходящее выше, да и не уверен, что это необходимо — обычная последовательность действий, описанных здесь ранее. И вообще, оно нам поможет разобраться с Rc / Arc?
А вот и да!
Увы. Как уже было сказано, ограничения трейтов высшего порядка не работают ни с чем, кроме времен жизни. Может их когда-нибудь докрутят полноценно, или хотя бы для типов, тогда можно смело решить проблему создания типов высших порядков! Но не сегодня.
(если я правильно понимаю, ситуацию может спасти реализация вот этого RFC, но пока что авторы водят вокруг него хороводы, пытаясь уцепиться хоть за что-то, с чего можно его начать имплементить — прим.пер.)
Вообще, я надеюсь, вы уже понимаете, что разговоры о конструкторах типов, даже в том ограниченном виде, который нужен нам сейчас, в среде разработчиков Rust вызывают приступы острой головной боли. Хотя в идеале, типы высших порядков должны поддерживаться языком на уровне синтаксиса.
Обобщаемость
Вот вы задумывались когда-нибудь, что два экземпляра одного и того же типа фактически взаимозаменяемы? Чтобы если у нас было два Виджета, и мы их поменяли местами, никто и усом не повел. В большинстве случаев это весьма приветствуется. А что если запретить экземплярам одного типа взаимозаменяться?
Массивы, например. Когда мы перебираем массив, это обычно делается для получения его элементов. Ну да, данная задача требует от нас предоставить соответствующие итераторы для различных потребностей доступа к элементам: Iter, IterMut и IntoIter. Не было бы удобней, если бы итератор говорил нам, куда смотреть, а мы уже сами решали, как там хозяйничать?
Доступ же по индексам ломает всё вышеописанное. Индексы можно подменить, можно поменять сам массив, делая индексы невалидными, можно, наконец, нечаянно натянуть индексы одного массива на другой. Но все это решаемо, с разной степенью приложенных усилий. Против подмены индексов — тип-обертка над ними, чтоб пользователю были недоступны реальные их значения. Против инвалидации индексов отлично поможет привязка их к времени жизни их массива.
А с подменой самого массива как? У меня два массива, типы их индексов идентичны и де-факто взаимозаменяемы, а мы этого не хотим. Способ получить нами (не)желаемое носит название обобщаемость (generativity). В основе обобщаемости лежит идея обладания разных экземпляров одного и того же типа разными ассоциированными типами. Значение ассоциированного типа зависит от экземпляра типа, то есть.
Я устал, ужасно устал. Мы тут все равно почти у финиша, так что я просто вам скопирую сюда демонстрацию вышеописанного, которую написал какое-то время назад. Все равно все есть в комментариях, читайте их.
Мухаха, не правда ли, все просто? Да нет, чистейший ад. И это все умопомрачительно небезопасно и хрупко. По сравнению с этим претензия Rust против использования Unsafe звучит как вежливая просьба не ругаться матом в падающем здании — вся стабильность системы зависит от филигранной подгонки всех типов (и времен жизни. особенно времен жизни — прим.пер.) друг к другу в любых, даже самых критичных условиях, при этом строка, помеченная unsafe — единственная на все полотно.
Зависимость программы от обобщаемости — это безопасно не более чем курение в пороховом складе, потому я очень ожидаю увидеть непосредственную поддержку обобщаемости в Rust, вместо собирания ее по крупицам HTRB из малоизвестных закоулков существующего синтаксиса.
И вообще, с меня хватит. Больше нет сил что либо писать. Все и так затянулось дальше некуда. Прошу меня простить. Очень прошу.





