Итак, попробую привести аргументы против Rust.
Не всё программирование является системным
Rust — язык системного программирования. Он обеспечивает точный контроль над компоновкой данных и поведением кода во время выполнения, обеспечивая максимальную производительность и гибкость. В отличие от других языков системного программирования, он также обеспечивает безопасность памяти — глючные программы завершаются чётко определенным образом, не допуская (потенциально опасного) неопределённого поведения.
Однако в большинстве случаев не требуется абсолютная производительность или контроль над аппаратными ресурсами. Для этих ситуаций современные управляемые языки, такие как Kotlin или Go, обеспечивают приличную скорость, завидную производительность и безопасность памяти благодаря использованию сборщика мусора с динамическим управлением памятью.
Сложность
Время программиста дорого стоит, а в случае Rust придётся потратить много времени на изучение самого языка. Сообщество приложило немало сил для создания высококачественных учебных материалов, но язык очень большой. Даже если вам выгодно переписать проект на Rust, само изучение языка может оказаться слишком дорогим.
Цена за улучшенный контроль — это проклятие выбора:
struct Foo { bar: Bar }
struct Foo<'a> { bar: &'a Bar }
struct Foo<'a> { bar: &'a mut Bar }
struct Foo { bar: Box<Bar> }
struct Foo { bar: Rc<Bar> }
struct Foo { bar: Arc<Bar> }
В Kotlin вы пишете класс
Foo(val bar: Bar)
и приступаете к решению задачи. В Rust нужно сделать выбор, иногда достаточно важный, со специальным синтаксисом.Вся эта сложность не просто так — мы не знаем, как создать более простой и безопасный для памяти язык низкого уровня. Но не для каждой задачи нужен низкоуровневый язык.
См. также презентацию «Почему C++ держится на плаву, когда корабль „Ваза” затонул».
Время компиляции
Время компиляции — это универсальный фактор. Если программа на каком-то языке медленно запускается, но этот язык позволяет быструю компиляцию, то у программиста будет больше времени для оптимизации, чтобы ускорить запуск программы!
В дилемме дженериков Rust намеренно выбрал медленные компиляторы. В этом есть определённый смысл (рантайм действительно ускоряется), но вам придётся всеми силами бороться за разумное время сборки в более крупных проектах.
rustc
реализует, вероятно, самый продвинутый алгоритм инкрементной компиляции в продакшн-компиляторах, но это немного похоже на борьбу с встроенной в язык моделью компиляции.В отличие от C++, сборка Rust не распараллеливается до предела, количество параллельных процессов ограничено длиной критического пути в графе зависимостей. Разница будет заметна, если у вас больше 40 ядер для компиляции.
В Rust также нет аналогов для идиомы pimpl, так что изменение крейта требует перекомпиляции (а не просто перелинковки) всех его обратных зависимостей.
Зрелость
Пять лет — это определённо малый срок, так что Rust молодой язык. Хотя будущее кажется блестящим, но всё-таки более вероятно, что через десять лет мы будем программировать на С, а не на Rust (см. эффект Линди). Если вы пишете софт на десятилетия, то должны серьёзно рассмотреть риски, связанные с выбором новых технологий (хотя выбор Java вместо Cobol для банковского программного обеспечения в 90-е годы ретроспективно оказался правильным выбором).
Есть только одна полная реализация Rust — компилятор rustc. Наиболее продвинутая альтернативная реализация mrustc целенаправленно пропускает многие статические проверки безопасности. На данный момент rustc поддерживает только один продакшн-ready бэкенд — LLVM. Следовательно, поддержка процессорных архитектур здесь более узкая, чем у C, у которого есть реализация GCC, а также поддержка ряда проприетарных компиляторов, специфичных для конкретных вендоров.
Наконец, у Rust нет официальной спецификации. Текущая спецификация не завершена и не документирует некоторые мелкие детали реализации.
Альтернативы
Кроме Rust, для системного программирования есть и другие языки, в том числе C, C++ и Ada.
Современный C++ предоставляет инструменты и рекомендации для повышения безопасности. Есть даже предложение о безопасности времени жизни объектов в стиле Rust! В отличие от Rust, использование этих инструментов не гарантирует отсутствие проблем с безопасностью памяти. Но если вы уже поддерживаете большой объём кода C++, имеет смысл проверить, возможно, следование рекомендациям и использование санитайзеров поможет в решении проблем безопасности. Это трудно, но явно легче, чем переписывать весь код на другом языке!
Если вы используете C, то можете применить формальные методы, чтобы доказать отсутствие неопределённого поведения, или просто тщательно всё протестировать.
Ada безопасна для памяти, если не использовать динамическую память (никогда не вызывайте
free
).Rust — интересный язык по соотношению затрат к безопасности, но далеко не единственный!
Набор инструментов
Инструменты Rust не назовёшь идеальными. Базовый инструментарий, компилятор и система сборки (cargo) часто называют лучшими в своём классе.
Но, например, некоторые инструменты, связанные со средой выполнения (в первую очередь, для профилирования кучи), просто отсутствуют — трудно размышлять о рантайме, если инструмента просто нет! Кроме того, поддержка IDE тоже далеко не соответствует уровню надёжности Java. На Rust просто невозможен автоматизированный сложный рефакторинг программы с миллионами строк.
Интеграция
Что бы ни обещал Rust, но сегодняшний мир системного программирования говорит на C и C++. Rust намеренно не пытается имитировать эти языки — он не использует классы в стиле C++ или C ABI.
Это означает, что между мирами нужно наводить мосты. Интеграция не будет бесшовной, она небезопасна, не всегда обойдётся без затрат и требует синхронизации между языками. В то время как местами интеграция работает и инструментарий сближается, на этом пути возникают случайные препятствия из-за общей сложности.
Одна из специфических проблем заключается в том, что самоуверенное мировоззрение Cargo (великолепное для чистых проектов Rust) может затруднить интеграцию с более крупными системами сборки.
Производительность
«Использование LLVM» не является универсальным решением всех проблем производительности. Хотя я не знаю бенчмарков, сравнивающих производительность C++ и Rust в целом, но нетрудно придумать задачи, где Rust уступает C++.
Вероятно, самая большая проблема в том, что семантика перемещения Rust основана на значениях (
memcpy
на уровне машинного кода). С другой стороны, семантика C++ использует специальные ссылки, из которых можно взять данные (указатели на уровне машинного кода). Теоретически, компилятор должен видеть цепочку копий, на практике это часто не так: #57077. Связанная с этим проблема заключается в отсутствии размещения новых данных — Rust иногда нужно копировать байты в/из стека, в то время как C++ может создать объект на месте.Несколько забавно, что в дефолтный Rust ABI (в котором пожертвовали стабильностью ради эффективности) иногда работает хуже, чем у C: #26494.
Наконец, хотя теоретически код Rust должен быть более эффективным из-за значительно более богатой информации об алиасах, включение оптимизации, связанной с алиасами, вызывает ошибки LLVM и некорректную компиляцию: #54878.
Но, повторюсь, это редкие примеры, иногда сравнение происходит в другую сторону. Например, в
Box
у Rust нет проблемы производительности, какая есть у std::unique_ptr
.Потенциально более серьёзная проблема заключается в том, что Rust с его определениями дженериков менее выразителен, чем C++. Таким образом, некоторые шаблонные трюки C++ для высокой производительности не могут быть выражены в Rust с помощью хорошего синтаксиса.
Значение unsafe
Возможно, идея
unsafe
даже более важна для Rust, чем владение и заимствование. Выделяя все опасные операции в блоки unsafe
и функции и настаивая на предоставлении им безопасного интерфейса более высокого уровня, можно создать систему, которая одновременно:- надёжная (не отмеченный
unsafe
код не может вызвать неопределённое поведение),
- модульная (различные небезопасные блоки можно проверить отдельно).
Вполне очевидно, что так оно и есть: фаззинг кода Rust находит панику, но не переполнения буфера.
Но теоретические перспективы не столь радужны.
Во-первых, нет определения модели памяти Rust, поэтому невозможно формально проверить, является ли данный небезопасный блок допустимым или нет. Существует неофициальное определение «вещей, которые rustc делает или на которые может полагаться» и продолжается работа над верификатором рантайма, но фактическая модель не ясна. Таким образом, где-то может быть какой-то небезопасный код, который сегодня работает нормально, а завтра будет объявлен недействительным и сломается в новой оптимизации компилятора через год.
Во-вторых, есть мнение, что блоки unsafe на самом деле не являются модульными. Достаточно мощные блоки unsafe могут, по сути, расширить язык. Два таких расширения не делают ничего плохого в изоляции друг от друга, но приводят к неопределённому поведению при одновременном использовании: см. статью «Наблюдаемая эквивалентность и небезопасный код».
Наконец, в компиляторе есть явные ошибки.
Вот некоторые темы, которые я намеренно опустил:
- Экономика («труднее найти программистов на Rust») — мне кажется, раздел «Зрелость» отражает суть этого вопроса, который не сводится к проблеме курицы и яйца.
- Зависимости («stdlib слишком мал / везде слишком много зависимостей») — учитывая, насколько хорош Cargo и соответствующие части языка, я лично не вижу в этом проблемы.
- Динамическое связывание («в Rust должен быть стабильный ABI») — не думаю, что это сильный аргумент. Мономорфизация фундаментально несовместима с динамическим связыванием, и если вам действительно нужно, то есть C ABI. Я действительно думаю, что здесь можно улучшить положение вещей, но вряд ли речь идёт о специфичных изменениях именно в Rust.
Обсуждение темы в /r/rust.
Alex_ME
Попробую высказать критику на критику и дополнения. Я соглашусь с вами в двух пунтках:
Я бы еще добавил, что проблема в том, что Rust позиционируется как более безопасный язык, чем Си, но компилятор он слишком неформализован и активно развивается, чтобы быть сертифицированным для применения в каких-то реально safety-critical задачах, где, к примеру, сертифицирован лишь какой-нибудь IAR.
Еще из недостатков (как следствие молодости) добавлю малое количество библиотек. На С++, не говоря уже о С, больше. Захотел я GUI писать и приуныл. Пока что самая зрелая — веб-гуи на WASM, yew.
Не соглашусь. Rust дает преимущества и не в системном программировании, не за счет модели работы с памятью, а за счет своей системы типов (ADT, pattern matching и все такое). Например, подход Option вместо исключений может сделать более понятным (по сигнатуре видно) и безопасным. Но проблема в том, что как, уже сказано, Rust имеет сложности, и все равно придется думать о владении, памяти, лайфтаймах и всяких Rc<RefCell>
Aleshonne
Для safety-critical я бы посоветовал упомянутую в статье Аду в режиме SPARK.
Там есть контракты, которые в C++ уже давно обещают, но никак не завезут, формальная верификация, много приятностей вроде параллельности из коробки и значений нестандартного размера (вроде массива из 8 элементов по 3 бита каждый) и удобочитаемый синтаксис. Есть, правда, и суровый минус — писать на этом тяжело. Впрочем, это можно считать и плюсом: набирая Очень_Длинный_Идентификатор_Из_Стандартной_Библиотеки_Языка_Ада программист успеет ещё раз обдумать реализацию. И какой нибудь Unchecked_Deallocation явно говорит о том, что надо всё перепроверить.
Siemargl
Как парируем сентенцию из статьи?
Aleshonne
В режиме SPARK нельзя выделять динамическую память вообще (и использовать контролируемые типы, для которых переопределена процедура инициализации). Только автоматическое создание переменной при входе в её область видимости и удаление при выходе. На крайний случай есть пакет Unchecked_Deallocation (когда выгрузить что-то из памяти вот прям очень нужно), но у него даже название звучит страшно и на его использование будет warning.
Впрочем, с высоты моего опыта (аж одна полноценная программа на Аде), динамическая память там нахрен не сдалась, всё и так нормально работает.
Siemargl
Тут вот оно чё, Михалыч.
Как только нет динамической памяти с указателями (или есть GC) и нет многопоточности, то все языки становятся надежными, как Раст =) Остальное плюс минус мелочи.
Впрочем многопоточность тоже минорный фактор в этом сравнении.
arthuriantech
Между прочим, правила MISRA C тоже запрещают malloc/calloc/free.
Siemargl
И на что Ви намекаете?
dmitryrf
С позволяет писать за границы массива независимо от способа выделения памяти.
Siemargl
Как бы Раст тоже позволяет. Будет паника.
Аналогичного результата в С получим с -fsanitize=bounds
dmitryrf
Тишина, все довольны. Или я что-то не так делаю?
Siemargl
Вообще то да. Путаешь массивы и указатели =)
Собственно, это было выдвинуто на премию C’s Biggest Mistake
0xd34df00d
Почему аду, а не агду? В ней есть полноценная внутренняя формальная верификация, зависимые типы и вот это вот всё.
А могли бы типы говорить.
Aleshonne
А есть ли у агды стандарт? Все нормативные документы (вроде ISO 26262) требуют наличия стандарта для языка программирования при использовании его в safety-critical областях, дабы через условные 20 лет в случае катастрофы эксперты вооружились этим стандартом и проверили, что код был написан правильно (или неправильно), а не гадали, что делает какой нибудь оператор вроде [@!], удалённый из языка 15 лет назад (а историю коммитов потеряли 10 лет назад при переезде на другой сервер). На аду есть стандарты ГОСТ и ISO, на си — ISO, даже на яваскрипт есть стандарт ECMA. Яваскрипт, правда, не проходит из-за динамической типизации, так что мы вряд ли увидим ракету, которая сложила -2 и 10 и, получив -210, улетела в сторону ближайшей чёрной дыры хвостом вперёд.
PS Я сам люблю функциональное программирование, но это не те задачи, в которых его сейчас можно применять. Мир к этому ещё не готов.
0xd34df00d
Ну я не спорю, что все эти ады, MISRA и прочие подобные хороши, чтобы ими прикрывать зад — этакая бюрократическая верификация. Но если хочется что-то на самом деле доказать, то они не лучший выбор.
Наличие шильдика ISO не защищает магическим образом от аналогичной потери чего-нибудь при переезде на другой сервер, а агдой всё-таки пользуется достаточно много людей, чтобы распределённая природа гита взяла своё.
Aleshonne
0xd34df00d
А насколько выразителен язык спецификации? То, что я сходу нагуглил, говорит о SMT-разрешимых VC, а это заведомо довольно слабая штука.
А то я просто сейчас вот пишу недостатью, где описываю какой-то модельный язычок, и в этой своей агде я описываю язык и доказываю формально всякие интересные свойства этого языка. Можно так в аде?
Лучше всё ж доверять всего лишь ядру проверялки, а не ядру проверялки + стандартной библиотеке.
Так это вполне себе решаемые вопросы. В хаскеле, например, есть такая штука, как stackage snapshots, и в проекте указывается конкретный снапшот. Я так, например, не так давно вытащил какой-то старый проект, который закончил в 2016-м, и спокойно собрал его в том же окружении, что четыре года назад, с тем же компилятором, теми же версиями зависимостей, и, вполне возможно, получив до бита такой же бинарник. В идрисе, агде и коке такого вроде как нет, но это, опять же, исключительно вопрос тулинга, а не какая-то фундаментальная недоработка.
В конце концов, если очень хочется именно бумагу, никто не мешает распечатать на бумаге состояние репозитория кока в момент сдачи проекта.
Aleshonne
Кстати, вот тут есть справка по тому, что спарк может. А там в конце в разделе Alternative Provers скромно так говорят, что можно в параметрах командной строки написать
--prover=Coq
, что уже несколько расширяет возможности языка. В стандартной библиотеке есть код, на который проверялка ругается. Сырые указатели, дёрганье ОС напрямую и т.д. Остаётся надеяться, что разработчики проверили корректность работы всего этого добра. Вот только задумались о такой нужной вещи только разрабы хаскеля. Я, например, о таком для фортрана мечтаю, но оно и близко не предвидится.sshikov
>>Не всё программирование является системным
>Не соглашусь.
Ну а почему? Реально же не всё. Если бы даже Rust был пригоден только для системного — это разве было бы реально серьезным недостатком?
Ну и наборот тоже, вот скажем упоминание kotlin в одном контексте вас разве не смущает? Вот уж котлин-то вообще для системного программирования скорее всего не годится. Ну и ничего, никто не жалуется, есть ниши, где он хорош — ну и прелестно.