Pull to refresh

Comments 96

да-да, потом еще окажется что в "safe rust" unsound unsound-ом погоняет, что в "unsafe rust" больше шансов поймать UB чем в Си, что это чудо при разработке сначала постоянно вынуждает делать глубокие инвазивные рефакторинги по все кодовой базе (потому что отстает по фичам от C++ лет на 15), а потом компилируется по полчаса (потому что пытается компенсировать это отставание процедурными макросами во все поля).

Взять хотя бы этих акробатов, которые сейчас тащат раст в ядро Linux. Оказалось что в расте не обрабатываются ошибки аллокации при создании структур (Vec::new()), а так как машинерии для работы с исключениями у них тоже нет, то им пришлось наспамить кучу новых методов типа Vec::try_new() для всех структур в стандартной библиотеке.

Это я еще про комьюнити фанатов раста не сказал, которое начинает агрессивно тебе доказывать что ты ничего не понимаешь (либо, как вариант, молча лепит минусы в карму). А потом при проверке оказывается что там один студент без опыта, другой фронтендер, третий в лучшем случае пилит круды по шаблонам...

окажется что в "safe rust" unsound unsound-ом погоняет

Пример в студию!

что в "unsafe rust" больше шансов поймать UB чем в Си

Сильное утверждение, доказать сможете?

что это чудо при разработке сначала постоянно вынуждает делать глубокие инвазивные рефакторинги по все кодовой базе (потому что отстает по фичам от C++ лет на 15)

Не встречал проблем с рефакторингом. Приведите пример.
Из фич знаю только вычисление в const expr, тут пока в Rust не очень. И это явно не 15 лет существует в С++.
По остальным я тоже могу привести фичи, в которых С++ отстает на N лет, и которых в нем никогда не будет.

Оказалось что в расте не обрабатываются ошибки аллокации при создании структур (Vec::new()), а так как машинерии для работы с исключениями у них тоже нет, то им пришлось наспамить кучу новых методов типа Vec::try_new() для всех структур в стандартной библиотеке.

Наспамили, работает, пишут новый код в ядре Linux. Это в вас говорит обида, что С++ в ядро не попал.

либо, как вариант, молча лепит минусы в карму

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

И это явно не 15 лет существует в С++.

Ну 14 лет (хотя, на самом деле implicit constexpr был всегда, а ключевое слово предлагалось лет 20 назад), год - это так принципиально?

Ок, принято.
Может вы сможете привести список фич, помимо constexpr?

UFO landed and left these words here

Не встречал проблем с рефакторингом. Приведите пример

Вот, например, опытный разработчик пишет о том же и объясняет в чём проблема.
Stop Making Me Memorize The Borrow Checker

Тут надо знать контекст, над чем он работал. Из его объяснения не понятно, откуда такие проблемы с временами жизни. Я видел кучу проблем при интеграции с другими языками, и там иногда проще обернуть в Rc или Arc. Но при работе над библиотекой на чистом Rust таких проблем не возникает.

Времена жизни обычно указываются явно в 2 случаях:
- взятие ссылки на поле/поля/слайсы
- оборачивание ссылки на исходный объект
В остальных случаях их опускают, и они вычисляются компилятором. При таком подходе не будет возникать "upwards of 30 or more statements depending on the complexity of your architecture".

С другой стороны, "This is because the borrow checker cannot run until an entire function compiles. Sometimes it seems to refuse to run until my entire file compiles." действительно встречается. Это мешает если не знаешь правила заимствования, и пишешь надеясь что компилятор тебя поправит. Не сказал бы что это проблема, в любом языке желательно "для того, чтобы быть высокопродуктивным программистом Rust ... нужно чтобы вы сделали все правильно с первого раза".

Однако и он пишет, что текущая ситуация в Rust гораздо лучше чем в С++:

This is painful because I am an experienced C++ programmer, and C++ has this exact problem except worse: undefined behavior. In the worst case, C++ simply doesn’t check anything, compiles your code wrong, and then does inexplicable and impossible things at runtime for no discernable reason (or it just deletes your entire function). If you run ubsan (undefined behavior sanitizer), it will at least explode at runtime with an error message.

в отличие от фразы выше "потому что Rust отстает по фичам от C++ лет на 15", которую плюсуют но не аргументируют.

Думаю, что работать над чем-то на чистом Rust в крупных существующих продуктах -- это редкость и роскошь.

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

Согласен. Против этого может помочь разделение на микросервисы. Идеальный пример - Robot Operating System. IPC с разделяемой памятью, там где это важно. Модули пишутся на С, С++, Rust, Python, C# и еще куче языков. Работает в системах с ограниченными ресурсами: миникомпьютеры и даже микроконтроллеры.

Конечно не везде это возможно, но ROS это как иногда достижимый идеал в коде.

Сильное утверждение, доказать сможете?

Именно доказать - нет, потому что некоторые UB из Си в Rust не переехали (знаковое переполнение, например), но новые подводные камни там действительно есть. Банально - в Си нет аналога ситуации "случайно создал &mut T при существующем *mut T, хотя хотел второй *mut T". Есть restrict, но воспользоваться им случайно, как мне кажется, весьма затруднительно.

В Rust случайно создать &mut T из *mut T никак не выйдет. Для этого надо написать конструкцию let ref2 = unsafe {&mut *ptr};, и она специально сделана так странно и неудобно.А вот второй *mut T создается просто копированием: let ptr2 = ptr;. Кроме того для UB нужно создать два&mut T!

потому что некоторые UB из Си в Rust не переехали (знаковое переполнение, например)

Из C в Rust не переехало порядка 99% UB самого языка, и около 90% UB стандартной библиотеки. И я как раз могу это доказать. Для этого достаточно открыть ubbook, и читать в нем статью за статьей, а потом пытаться перенести на Rust. Существенная часть примеров UB в нем работает и в С, и в С++, а другие с минимальными усилиями переносятся на С и тоже вызывают UB.

Как насчет невыровненных ссылок? Или дивного правила integer promotion, про которое не знает ни один программист, которого я лично спрашивал.

Или pointer provenance, который ломает работающий ранее С код, даже в режиме c99. Да, в каком году его добавили в стандарт? Или еще не добавили?

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

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

В Rust случайно создать &mut T из *mut T никак не выйдет.

Я про создание из самого T. Грубо говоря, сделать &mut foo as *mut _ вместо &raw mut foo.

Кроме того для UB нужно создать два&mut T!

Для UB нужно:

  • Создать ptr: *mut T.

  • Создать borrow: &mut T.

  • Использовать ptr, который при создании borrow был помечен как неактивный.

Второй &mut T здесь не нужен, достаточно будет и ptr.write.

Как насчет невыровненных ссылок?

Мы же говорим про unsafe Rust, верно? Указатель на невыровненное поле тоже должен использоваться правильно (через read_unaligned), иначе это UB.

Ну и самое типичное - выходы за пределы массива.

Мы же говорим про unsafe Rust [2]? Всякого рода get_unchecked - собственно, потому и unchecked.

Я это всё не к тому, что в Rust что-то не так (я сам его очень люблю). Просто то, что в нём есть свои способы выстрелить в ногу неопределённым поведением, которых не было в языках-предшественниках, и попытка писать на unsafe Rust как на "ещё одном Си", скорее всего, в один из них упрётся, - факт, который, AFAIK, признают и разработчики языка в том числе.

Я про создание из самого T. Грубо говоря, сделать &mut foo as *mut _ вместо &raw mut foo.

Ну то что нужен unsafe вы совершенно случайно забыли:

unsafe { &mut foo as *mut _ } против &raw mut foo
Случайно это сделать не выйдет, как бы вы ни пытались.

Использовать ptr, который при создании borrow был помечен как неактивный.

Интересно, что это за неактивный указатель? И кем он помечен?

Мы же говорим про unsafe Rust, верно? Указатель на невыровненное поле тоже должен использоваться правильно (через read_unaligned), иначе это UB.

Очевидно что ссылку вы не открывали. Иначе бы убедились, что в Rust в этом случае ошибка компиляции. Исправить ее можно через read_unaligned, или другими способами.

Я это всё не к тому, что в Rust что-то не так (я сам его очень люблю). Просто то, что в нём есть свои способы выстрелить в ногу неопределённым поведением, которых не было в языках-предшественниках, и попытка писать на unsafe Rust как на "ещё одном Си", скорее всего, в один из них упрётся, - факт, который, AFAIK, признают и разработчики языка в том числе.

Вызвать UB можно и в Rust. Но вы не понимаете главной разницы между ними.

В С и С++ самый короткий и простой синтаксис вызывает UB. Чтобы написать безопасный код, приходится использовать vsnprinf вместо prinft, и так по всей стандартной библиотеке.

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

В Rust небезопасные функции требуют unsafe, у них длинные названия
(get_unchecked против get)
и неудобный синтаксис (unsafe { &mut foo as *mut _ })
и есть способы вообще запретить unsafe в крейте:
#![forbid(unsafe_code)].
Всё чтобы не вызвать UB случайно.

которых не было в языках-предшественниках

То что вы описали, есть и в С++ и С. Пройдите наконец по ссылкам и прочитайте по Pointer Provenance. В С это доступно через restrict.

Ну то что нужен unsafe вы совершенно случайно забыли
В Rust небезопасные функции требуют unsafe

Я весь разговор вёл в контексте именно unsafe Rust - напомню изначальную цитату:

что в "unsafe rust" больше шансов поймать UB чем в Си

(выделение моё). Понятно, что в безопасном Rust всех таких проблем нет - ради этого, что называется, вся песня и писалась. Впрочем, в первом случае аргумент не совсем верен - создание указателя не unsafe, unsafe только разыменование (что, само собой, не отменяет факта, что где-то в некорректном коде unsafe действительно есть).

Интересно, что это за неактивный указатель? И кем он помечен?

Правилами алиасинга, вестимо. По крайней мере, вот на такой код Miri ругается:

fn main() {
    let mut val = 42;
    let ptr = &raw mut val;
    &mut val;
    unsafe { ptr.write(0); }
}

...причём явно указывая, что причина - " was later invalidated ... by a Unique retag" с указанием на &mut val.

Очевидно что ссылку вы не открывали. Иначе бы убедились, что в Rust в этом случае ошибка компиляции.

Открывал, почему же. Для ссылок это - ошибка компиляции. Для указателей - нет, а делать по ним ptr.read или *ptr вместо ptr.read_unaligned - это всё так же UB.

В С это доступно через restrict.

Да, я в курсе, я упомянул об этом в первом комментарии. И много вы видели ситуаций со случайно добавленным restrict? А случайно создать ссылку, ломающую работу с указателями, и не понять этого, пока не получишь замечание от Miri, - первый попавшийся пример - https://github.com/rust-lang/rust/issues/128803.

Вы сами написали именно так, и я разобрал эту конкретную ситуацию:

Я про создание из самого T. Грубо говоря, сделать &mut foo as *mut _ вместо &raw mut foo.

И проблема возникнет только если заключать в unsafe большие куски кода, чтобы включить в них еще и разыменование, которое (как вы сами указали) где-то есть.

Правилами алиасинга, вестимо. По крайней мере, вот на такой код Miri ругается:

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

Открывал, почему же. Для ссылок это - ошибка компиляции. Для указателей - нет, а делать по ним ptr.read или *ptr вместо ptr.read_unaligned - это всё так же UB.

Вы рассматриваете какой то сферический Unsafe Rust в вакууме. Если Rust дает возможность работать безопасно с не выровненными полями без unsafe, то зачем же я буду усложнять себе жизнь?

И так по любой концепции: есть безопасный вариант, а есть опасный через unsafe. Причем безопасным пользоваться намного легче. В С нет этого выбора, там всегда будет возможен UB.

что в "unsafe rust" больше шансов поймать UB чем в Си

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

Да, я в курсе, я упомянул об этом в первом комментарии. И много вы видели ситуаций со случайно добавленным restrict? А случайно создать ссылку, ломающую работу с указателями, и не понять этого, пока не получишь замечание от Miri, - первый попавшийся пример - https://github.com/rust-lang/rust/issues/128803.

Видел, и достаточно. restrict был добавлен не случайно, но при рефакторинге его гарантии случайно поломал другой программист.

Кстати, UB можно вызвать и без restrict, через type punning. Который используется в любой библиотеке сериализации и форматирования на С. И не возможен в Rust. Как и сотни других UB, которые вы не замечаете.

И проблема возникнет только если заключать в unsafe большие куски кода, чтобы включить в них еще и разыменование, которое (как вы сами указали) где-то есть.

Не улавливаю мысль, честно говоря. &mut foo as *mut _ можно сделать без unsafe. И оно может сломать разыменование указателя где-то дальше по курсу - пример в моём предыдущем комментарии.

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

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

Если Rust дает возможность работать безопасно с не выровненными полями без unsafe, то зачем же я буду усложнять себе жизнь?

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

Видел, и достаточно. restrict был добавлен не случайно, но при рефакторинге его гарантии случайно поломал другой программист.

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

Я понял, что неправильно прочитал начальный пример. Извините.
Давайте вернемся к началу:

Я про создание из самого T. Грубо говоря, сделать &mut foo as *mut _ вместо &raw mut foo.

Если указатель передается в каллбек (FFI), то UB не будет, т.к. ссылка выйдет из области видимости. Если же использовать указатель сразу, то будет UB в очень ограниченном числе случаев (см. исходный RFC для &raw):

  • невыровненные данные. Но сейчас это уже не скомпилируется, писал выше.

  • неинициализированная память. Используйте MayBeUninit, для типичного случая unsafe не нужен.

  • необитаемые и пустые типы. Тут конечно UB, но зачем на них брать указатель??

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

Правила алиасинга в языке есть, их нужно соблюдать. Компилятор их проверить не может (не всегда может) для указателей, и полагается на программиста с помощью unsafe.

Miri пытается проверять правила алиасинга. Для этого переписывает код после rustc, это не "статический анализатор". Неактивные указатели это терминология Miri, и её нет в языке и компиляторе. Причем там разная терминология для Stacked Borrows и Tree Borrows.

Повторюсь, я изначально всё это писал в предположении, что мы unsafe уже по какой-то причине используем.

Потому что это не получается сделать в Safe Rust, например из-за FFI. Но конкретно случай не выровненных данных не релевантен. Потому что из FFI можно получить указатель, превратить его в ссылку и безопасно читать не выровненные поля без unsafe.

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

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

Про try_new не знал, спасибо, гляну :)

В книжке аж ЖЫРНЫМ выделено

implement Eq whenever you implement PartialEq
be wary of implementing just PartialOrd and not Ord

И никто не парится потому что очень редко когда эту шоблу имплементят вручную.

======

Насчёт try ничего не понял. Try блоки всё никак не зарелизят, да - но Iteratort::try_for_each() никуда не удаляли.

======

И чем их не устроил .fold(0, |a, b| a+b)?

А чем вас не устроил for i in 1..100 { sum += i }?

  1. Понял, принял

  2. Если нужно подсоединиться к базе данных, прочитать что-нибудь и закрыть соединение, Iterator немножко не поможет (как насчёт Iterator<Item=FnOnce>? XD)

  3. <sarcasm>Потому что ФП</sarcasm>

  1. Автор не знаком с концепцией ZeroVer. Она распространена не только в Rust, в Javascript например она никого не смущает.

  2. std::ops::Tryдоступен только в Nightly компиляторе, это "экспериментальная фича компилятора", которой никогда не было в Stable.
    Видимо автор всегда использует только нестабильные версии компиляторов в продакшен, ну ОК.

  3. attempt to multiply with overflow
    Ну да, в Debug режиме Rust обрабатывает переполнение как ошибку. И можно выключить черезoverflow-checks = false, или включить аналогично в Release.
    Если нужно особое поведение, напиши его сам, и подай RFC на включение в стандартную библиотеку.

    И т.д. Очень жирный наброс, ни одной нормальной претензии.

  1. У нас даже документация к cargo гласит про SemVer

  2. Раньше был нормальный, адекватный Try, потом его заменили на т. н. TryV2, причём старый даже не оставили, как в случае с distutils в Python

  3. Я про саму бесполезность метода. В частности, про переполнение

Это все nighty, это все априори нестабильно. В Python же просто нет такой концепции, как нестабильные фичи. Это некорректное сравнение.

Ну не используйте nighty без особенно острой нужды и не будет вот этой боли, что язык якобы постоянно вносит breaking changes.

  1. ZeroVer это подвид SemVer.

  2. То что вы вспоминаете, было еще до версии 1.0 языка. По определению, разработчики могли менять API до выхода 1.0.

  3. В языке есть правила переполнения целочисленных типов, которые я привел выше. Вы же хотите чтобы стандартная библиотека им не следовала.
    Если вы запустите в Release режиме, то будет нужное вам поведение (пруф). Так полезен метод или бесполезен?)

Что интересно, ссылка на ZeroVer описывает состояние, но не объясняет, почему это хорошо 😀

Например, если все версии начинаются с 0, то этот ноль уже ничего не значит и просто мусор.

Возможно, нужно считать, что первый 0 значит, что нет никаких гарантий обратной совместимости между версиями. Но на практике вроде какая-то совместимость есть же?

А только я один обращаю внимание на дату первого апреля или это известная всем не шутка?

О как! Тогда это прекрасно же. А я не домотал до конца страницы.

Как правило считается, что между версиями 0.х может быть сломано API, а 0.х.у нельзя ломать. ZeroVer применяется для библиотек, которые разрабатываются недавно и еще не пришли к какому-то API. Обычно через N лет они все-таки выпускают версию 1, и приходят к обычному SemVer. Но некоторые могут зависнуть в 0.x надолго, т.к. не хотят брать на себя обязательства.

В Rust это популярно из-за статической линковки. Можно конечно делать .dll или .so, но этим крайне редко пользуются. В Go аналогично.

Про брать обязательства - это очень странные мысли. Разработчики что ли кровью договор с дьяволом подписывают, когда выпускают релиз 1.0.0?
Я думаю что это больше ментальная проблема у разработчиков. Они сами себе выдумывают какие-то причины, что бы не делать релиз 1.0.0 для библиотеки, которой уже несколько лет пользуются много людей. Никто их не найдёт по IP-адресу и не побьёт, если они выпустят версию 2.0.0 в случае слома обратной совместимости. Кому надо - обновятся, точно так же как обновлялись с 0.21 на 0.22. А кому не надо - будут сидеть на версии 1.0.

Я написал то же самое, но другими словами.

Я думаю что это больше ментальная проблема у разработчиков.

Как быть с разработчиками в Go и Javascript? Это реальность во многих библиотеках/языках.

Например целый язык программирования Zig сознательно остается на версии 0.x у потому что его разработчики еще не считают, что у них есть стабильное API. Их право. Возможно их версия 1.0 будет гораздо лучше текущей, но мы не знаем когда она выйдет.

Не вижу особой разницы между вариантами:

  • выпустить релиз 0.22 после 0.21.x, потому что "сломал" API;

  • выпустить релиз 2.0 после 1.x, потому что "сломал" API;

Сидение годами на 0-ой мажорке - это сродни суеверным страхам и бесполезным надеждам на то, что можно будет сделать что-то более правильно к своему 70-и летнему юбилею. Например упомянутому в статье крейту base64 уже 9 лет. Я, как и автор статьи, тоже без понятия, чего там можно так долго "мариновать".

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

Выпускать 1.0 нужно когда вы пришли (в первый раз) к стабильному API, и не собираетесь его ломать продолжительное время. Причем тут hotfix вообще?

Например упомянутому в статье крейту base64 уже 9 лет. Я, как и автор статьи, тоже без понятия, чего там можно так долго "мариновать".

Я тоже без понятия. Но я знаю что в мире это широко практикуется не только в Rust. И принимаю мир как он есть.

А вот автор сделал акцент именно на Rust, "смотрите как всё плохо". В совокупности с остальными пунктами (притянуто за уши, местами открытое вранье), и тэгом Юмор, я делаю вывод что это наброс. И об этом ниже еще пара человек написали.

Проблема как раз в том как понять, что API стало стабильным. И что значит "я не собираюсь его ломать"? Это какой-то официальный договор, может закон, который запретит мне что-то ломать если я решу, что так будет лучше? У меня уже было так, что я выпускал мажорный релиз либы и на следующий день вспоминал, что забыл сделать одно ломающее изменение, которое я как раз откладывал до мажорки. И такое может случится в любой момент, особенно в большом проекте, где за всем не уследить. Поэтому нет какой-то настоящей стабильности кроме стагнации.

По моему это всё не более чем пожелания, которые к тому же может быть сложно соблюдать. API станет по настоящему стабильным, когда библиотеку перестанут развивать.

Конкретные числа в версии не имеют большого значения. То что либа имеет версию 10.0, не означает что она в 10 раз лучше чем версия 1.0. Это даже не означает, что 10-ая версия лучше чем первая. Это означает только то, что она не совместима с версией 1.
Значение имеет только само изменение этих чисел в номере версии. И три числа удобнее использовать, чем два.

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

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

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

А насчёт того, что пользователи тратят время на обновление, так с мажорками ситуация точно такая же как с минорками в ZeroVer. Но почему-то выпустить версию 0.22 через пару дней после версии 0.21 как будто не считается чем-то зазорным и малоприятным. Хотя пользователи точно также два дня потратили на обновление и потом узнают, что им надо опять что-то проверять и обновлять. Хотя тут слово "надо" опять притянуто за уши. Ничего им не надо срочно обновлять, если у них сейчас нормально всё работает.

Сравним через PartialOrd: A и B
Пробуем сравнить через PartialEq, чтоб наверняка: A и B
Сравним через PartialOrd: A и B
it is impossible...

Но вы сами написали код, который заставляет программу так себя вести: if (a<b) { ... } else if (a == b) { ... } else if (a>b) { ... } else { ...}. Вот три вызова. Как ещё её выполнять? Хотите один вызов partial_cmp - пишите match по нему.

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

В первой и третьей ветках main'а вызывается partial_cmp. Внутри которого у вас цепочка if-elseif-elseif. Которая выполняется точно так же, перебирая все ветки (и выходит по ветке ==, при этом печатая ваши буквы).

Повторяю, хотите один вызов - не надо писать if-elseif-elseif, пишите один вызов.

impl PartialOrd for NamedNumber {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.0.partial_cmp(other.0))
    }
}


fn main() {
    let a = NamedNumber(10, 'A');
    let b = NamedNumber(10, 'B');
    match a.partial_cmp(b) {
        ...
    }
}

Вы показываете какой-то макрос matches!, это вообще не то.

Назвался груздем — полезай в кузов.

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

И находить минусы, сравнивая с плюсами (каламбур да), которым дофига лет

Вот на тему mut тоже первое время тупил, что он не совсем про ту мутабельность, которая интуитивно понимается. Но потом мне рассказали, что в комьюнити было на эту тему обсуждение, что это ключевое слово надо было называть uniq, тогда бы все в голове сразу встало.

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

Всю статью не читал, осилил только первый наброс про мутабельность, и вот это правильный комментарий.

Ссылки в расте надо интерпретировать именно так:

  • &mut T - уникальная ссылка. Из этого свойства как раз и вытекает возможность безопасной мутации объекта.

  • &T - шаренная ссылка. На этот объект может иметь доступ много кто, поэтому в общем случае изменять данные под ней нельзя. Но можно переложить ответственность за контроль корректного доступа (exactly one writer XOR multiple readers) в рантайм с помощью Mutex, RwLock или иных способов Interior Mutability.

Проблема Раста в том, что он по-сути пытается привлечь не тех людей. Рекламировать его как замену C\C++ и потом спешно допиливать, когда для линукс ядра у тебя не хватает функциональности - это выглядит немного странно. С и С++ разработчики при встрече с Растом справедливо задают вопросы "что это за фигня" и "зачем оно мне нужно" и никого не волнует "сколько уязвимостей они могли бы избежать", если бы писали на Расте.

По моему опыту на Раст лучше всего пересаживаются разработчики, клепающие бекенды на ноде с тайпскриптом. Да, конечно у них возникает много вопросов, но обычно это вопросы общего характера, например, про работу с переменными при многопоточности и подобное (знания, которые им были не нужны для работы с однопоточной нодой). Максимально подробные ошибки и предложения от компилятора помогают новичкам самостоятельно разобраться в вопросе и на кодревью по-сути остаются только какие-то стилистические претензии или просто предложения по улучшению. У себя на работе сначала написал библиотеку для ноды на расте (с помощью napi-rs, рекомендую), а потом командой переписали целый сервис без особых проблем. Особенно впечатлился менеджмент, когда расходы на инфраструктуру для сервиса упали более чем в 10 раз. А я даже не ожидал, что коллеги так быстро вкатятся в Раст и смогут не просто контрибьютить, но и самостоятельно внедрять новую функциональность.

В общем Раст должен завлекать не тех кто более "низкоуровневый" (им и так хорошо), а тех кто сидит "повыше". Если, конечно, в принципе есть в этом хоть какая-то необходимость.

Как у С-разработчика, никаких вопросов про то "что это за фигня" и "зачем оно мне нужно" у меня не было. Это как раз прекрасно понятно: модульность (в какой там версии плюсов обещают? Что с поддержкой компиляторами?), отсутствие необходимости менеджить руками не только память, но и открытые файлы, сокеты и любые другие ресурсы, которые необходимо очищать, значительно более выразительная система типов, с Option, Result и enum'ами. Трейты как полиморфизм здорового человека. Всего не перечислить.
Разве что там где ансейф на ансейфе и ансейфом погоняет кажется все еще удобнее брать С и писать к нему тонкую обертку, но такое даже в ядре не то чтобы часто нужно.

А зачем модульность, чтобы ускорить компиляцию? =)

Чтобы иметь адекватное разделение кода на публичный и приватные части без копипастинга и дурацких compile guard/pragma и прочего. Отдельная история про сишный полиморфизм, когда структура имеет разный состав в разных .c файлах и обрезанный вид в общем публичном интерфейсе.

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

У языков с garbage collection тоже нужно думать и писать область жизни ресурса оператором using потому что когда gc освободит ресурс одному богу известно, возможно что и никогда. А раст освобождает ресурс сразу как только он перестает быть нужным, ну почти.

А статья ни о чем. Прочитал, пожал плечами и закрыл

Всегда казалось, что using (я так понял речь о c#) -- это про IDisposable. Причём тут gc?

Ктоб его знал
¯\_(ツ)_/¯
Видимо потому что обычно встречаются в одном месте, когда кто то рассказывает о работе с GC, в C#.
Но человек не обратил внимания, что это актуально для неуправляемых объектов внутри управляемого объекта, а управляемый объект, в котором отработал Dispose закрывший дескрипторы или очистивший какую-нибудь unmanaged область памяти, которую использовал, удалится всё так же только тогда, когда CLR посчитает, что ему нужно больше памяти или что давно не чистил.

Юзинги определяют область после выхода из которой ресурс гарантированно и немедленно будет освобождён. Без юзинга ресурс тоже будет освобождён (сюрприз! даже файл будет закрыт) но когда именно - как GC захочет. Возможно, только к закрытию программы освободит. Для файлов, сетевых соединений и т.п. это не приемлемо.

Нет не определяют. Юзинги определяют область, после выхода из которой гарантированно будет вызван метод Dispose у объекта, и удалена ссылка на него.

А когда его управляемая тушка будет очищена из памяти, решит GC.

Скажу больше, если вы не добавите очистку неуправляемых ресурсов в Dispose, то using так же вызовет Dispose, и удалит ссылку на объект, а когда GC будет собирать мусор, то удалена будет так же только управляемая тушка управляемого объекта, а неуправляемые ресурсы, на которые он ссылался так и останутся в памяти в принципе без возможности быть удалёнными до момента, пока программа не будет завершена.

Юзинги определяют область, после выхода из которой гарантированно будет вызван метод Dispose у объекта

Это и называется "освобождением".

public class MyClass : IDisposable
{
    private IntPtr nativeResource; // Неуправляемый ресурс

    public MyClass()
    {
        nativeResource = AllocateNativeResource(); // Допустим, это аллокация нативной памяти
        Console.WriteLine("MyClass constructed. Native resource allocated.");
    }

    public void DoSomething()
    {
        Console.WriteLine("MyClass is doing something...");
        // Здесь может быть код, использующий nativeResource
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // Отключаем финализацию
    }

    protected virtual void Dispose(bool disposing)
    {
        if (nativeResource != IntPtr.Zero)
        {
            if (disposing)
            {
                // Освобождаем управляемые ресурсы, если они есть (в данном случае их нет)
                Console.WriteLine("Disposing managed resources (if any).");
            }

            FreeNativeResource(nativeResource); // Освобождаем неуправляемый ресурс
            Console.WriteLine("Native resource freed.");
            nativeResource = IntPtr.Zero;
        }
    }

    ~MyClass()
    {
        Console.WriteLine("Finalizer called.");
        Dispose(false); // Вызываем Dispose для освобождения ресурсов из финализатора
    }

    // Заглушки для демонстрации
    private IntPtr AllocateNativeResource()
    {
        // Имитация аллокации памяти
        return new IntPtr(12345);
    }

    private void FreeNativeResource(IntPtr resource)
    {
        // Имитация освобождения памяти
        Console.WriteLine($"Freeing native resource at {resource}");
    }
}

public class Program
{
    public static void MyFunction()
    {
        MyClass obj = new MyClass();
        obj.DoSomething();
        // obj больше не используется
        Console.WriteLine("MyFunction finished.");
    }

    public static void Main(string[] args)
    {
        MyFunction();
        Console.WriteLine("Waiting for GC...");
        GC.Collect(); // Принудительный запуск сборщика мусора (для демонстрации)
        GC.WaitForPendingFinalizers(); // Ждем завершения финализаторов (для демонстрации)
        Console.WriteLine("Program finished.");
    }
}

}
Пояснение:

MyFunction():
Создается объект obj класса MyClass.
Конструктор MyClass выделяет неуправляемый ресурс.
obj.DoSomething() выполняет некоторую работу.
Функция MyFunction() завершается. Объект obj становится недоступным (выходит из области видимости).
После завершения MyFunction():
Объект obj больше не нужен, но Dispose() не был вызван явно.
Объект obj становится кандидатом на сборку мусора.
Сборка мусора (GC):
Когда сборщик мусора (GC) запускается (в данном примере мы запускаем его принудительно GC.Collect()), он обнаруживает, что obj больше не доступен.
Поскольку MyClass реализует деструктор (~MyClass()), GC помещает obj в очередь финализации.

Финализация:
В отдельном потоке GC вызывает финализатор ~MyClass() для объекта obj.
Финализатор вызывает Dispose(false).
Dispose(false) освобождает неуправляемый ресурс (вызывается FreeNativeResource).

Что произойдет в итоге:

Неуправляемый ресурс, выделенный в конструкторе MyClass, будет освобожден финализатором (~MyClass()) во время сборки мусора.
Это произойдет недетерминированно, в момент, когда GC решит запустить сборку мусора.
Если бы мы использовали using или явно вызвали Dispose(), ресурс был бы освобожден детерминированно, сразу после того, как объект перестал бы быть нужен.

Важно:

Полагаться на финализатор для освобождения ресурсов - плохая практика.
Финализатор должен использоваться как последний рубеж защиты от утечек ресурсов, а не как основной механизм их освобождения.
Всегда используйте using или явно вызывайте Dispose() для объектов, реализующих IDisposable, чтобы гарантировать своевременное и предсказуемое освобождение ресурсов.

Вывод консоли (может немного отличаться в зависимости от среды выполнения):

MyClass constructed. Native resource allocated.
MyClass is doing something...
MyFunction finished.
Waiting for GC...
Finalizer called.
Freeing native resource at 12345
Native resource freed.
Program finished.

Наоборот, как раз у с/с++ разработчик вопросов минимум, потому что многие фичи пересекаются между собой. Например у плюсов есть мув семантика прикрученная сбоку, потому что обратная совместимость, а у раста она по дефолту.

Ну как сказать, у меня опыта общения с С\С++ разработчиками минимум - вокруг сплошные бекендеры разной степени продвинутости. Но те столкновения двух миров, что я наблюдал, выглядели, мягко говоря, совсем не адекватно. Вспомнить только недавнюю драму с презентацией Раста в линукс ядре и сколько мути это подняло. По таким событиям и различным статьям у меня сложилось впечатление, будто сишники испугались, что их хотят просто заменить на кого-то помоложе и попродуктивнее, и они просто таким образом защищаются. А растовики только подливают масла в огонь, заявляя, что всё вокруг уязвимое и срочно нуждается в переписывании.

Там всё началось с того что для некоторого API на С не было документации совсем. И в исходном коде тоже. И чтобы с ней интегрироваться, разработчики на Rust попросили сделать документацию, а те ответили в духе "есть же .h, по ним делайте". Но если заменить Rust на любой другой язык, да хоть С++, вылезла бы та же проблема.

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

Согласен.

Стоит ещё уточнить, что конкретно в том API о котором идёт речь даже по хедеру не всегда понятно, чего именно некоторые функции ожидают на вход, и чего на вход передавать ни в коем случае нельзя, иначе ядро крашнется, или испортит память.
И человек (по моему девушка если не путаю) написала биндинги на rust для этого участка кода, уточняя детали у того, кто этот код писал, что туда можно передать, а что нет. И разговор был о том, что биндинги для раста сами собой выходят самодокументироваными и физически не получится передать что то не то, в отличие от Си в конкретном участке кода.

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

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

Ровно наоборот. Опытные сишники и плюсники на раст не поведутся, потому что а) язык молод и ограничен в возможностях, б) преимущества раста для них не имеют смысла, в силу опыта, в) еще один язык учить, серьёзно?

Учить 20 языков или 21 разницы нет. А интересные концепции почерпнуть можно.

Вот да. Не каждый день не в ФП языке можно пощупать either type, например.

UFO landed and left these words here

не имеют смысла, в силу опыта

ну да, опыт избегания сотен UB в расте не особо нужен

А зачем язык должен кого то завлекать?

Например, эй мужики, тут новые плоскогубцы изобрели - все выбрасывайте, используйте только их!

Каждому инструменту свое применение. Не более.

а представляете какого создателям раста они скперили себя таким синтаксисом и такими правилами, в один прекрасный момент они могут понять что они ошибались , а код уже определен, и синтаксис уже определенный, а спорим с С и С++ (как в этом убедится, сужаем идею кода ну 5 классов, вот уже ограничение начинается, а тут целый язык такой на фишках какихто)

Буквально для этого и введены editions. Как раз через пару месяцев выходит новая. Интересно, как в долгосроке себя покажет.

https://godbolt.org/z/6oTP35sfc вот еще тоже интересно, в том смысле что если переписать либо математику на С либо игру на С(игру на С не удобно изза анимаций), она будет вполне откликаться(у меня есть +- 3д на С но там хромает математика и щас написал рабочую математику - пока интересно), на расте я боюсь представить как бы это выглядело

Извините, не совсем понял, что вы хотели продемонстрировать. Отмечу только, что с -C opt-level=1 и выше жить становится веселее.

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

#pragma GCC optimize ("Ofast,unroll-loops,-ffast-math")
#pragma GCC target("sse")
#include<math.h>
/* Type your code here, or load an example. */
const float square(const float num) {
    return sqrt(num);
}

медиана С ниже от 30 мс до 100 (100 *~9-10 * 100-1000) если брать медиану по 100 в отставании на 1 расчет допустим 4 на 4 матрицы с проверкой sqrt

в то время когда если на стороне раста подгонять скорость он просто станет почти равен и почти идентичен вызовам С

если это задержка и компиляция и выполнение одинаковое то С просто читабельнее, даже при условии malloc/free

в то время когда если на стороне раста подгонять скорость он просто станет почти равен и почти идентичен вызовам С

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

хорошо, по моему мнению вот у вас написано на скрине n.sqrt()

и такого просто не должно быть

вобщем. попрошу прощения! кароче было просто интересно

есть такая реализация в лоб

mat4 Mulm4(const mat4 a,const mat4 b)

{

mat4 result;

// Column 0

result.v[0] = a.v[0] * b.v[0] + a.v[4] * b.v[1] + a.v[8] * b.v[2] + a.v[12] * b.v[3];

result.v[1] = a.v[1] * b.v[0] + a.v[5] * b.v[1] + a.v[9] * b.v[2] + a.v[13] * b.v[3];

result.v[2] = a.v[2] * b.v[0] + a.v[6] * b.v[1] + a.v[10] * b.v[2] + a.v[14] * b.v[3];

result.v[3] = a.v[3] * b.v[0] + a.v[7] * b.v[1] + a.v[11] * b.v[2] + a.v[15] * b.v[3];

// Column 1

result.v[4] = a.v[0] * b.v[4] + a.v[4] * b.v[5] + a.v[8] * b.v[6] + a.v[12] * b.v[7];

result.v[5] = a.v[1] * b.v[4] + a.v[5] * b.v[5] + a.v[9] * b.v[6] + a.v[13] * b.v[7];

result.v[6] = a.v[2] * b.v[4] + a.v[6] * b.v[5] + a.v[10] * b.v[6] + a.v[14] * b.v[7];

result.v[7] = a.v[3] * b.v[4] + a.v[7] * b.v[5] + a.v[11] * b.v[6] + a.v[15] * b.v[7];

// Column 2

result.v[8] = a.v[0] * b.v[8] + a.v[4] * b.v[9] + a.v[8] * b.v[10] + a.v[12] * b.v[11];

result.v[9] = a.v[1] * b.v[8] + a.v[5] * b.v[9] + a.v[9] * b.v[10] + a.v[13] * b.v[11];

result.v[10] = a.v[2] * b.v[8] + a.v[6] * b.v[9] + a.v[10] * b.v[10] + a.v[14] * b.v[11];

result.v[11] = a.v[3] * b.v[8] + a.v[7] * b.v[9] + a.v[11] * b.v[10] + a.v[15] * b.v[11];

// Column 3

result.v[12] = a.v[0] * b.v[12] + a.v[4] * b.v[13] + a.v[8] * b.v[14] + a.v[12] * b.v[15];

result.v[13] = a.v[1] * b.v[12] + a.v[5] * b.v[13] + a.v[9] * b.v[14] + a.v[13] * b.v[15];

result.v[14] = a.v[2] * b.v[12] + a.v[6] * b.v[13] + a.v[10] * b.v[14] + a.v[14] * b.v[15];

result.v[15] = a.v[3] * b.v[12] + a.v[7] * b.v[13] + a.v[11] * b.v[14] + a.v[15] * b.v[15];

return result;

}

положим на 1 точку или на 3 мы решаем sqrt всё что я хотел сказать прошу прощения если я не в тему, просто заметилось, в годболте поставил таймер и чутка прикинул к 3д

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

Используйте специальные инструменты для проведения бенчмарка. Или на худой конец запустите 100 000 раз на компе с прогревом в 1000 запусков, и усредните.

P.S. Похоже что 348 и 1535 это время работы компиляторов, а не программы.

И что вы хотите доказать этим? Занимаетесь полной чепухой. Я вам тоже могу выдать вот такой результат, где якобы rust с оптимизацией быстрее скомпилировал. И что? Эти цифры зависят только от загруженности VM в конкретную секунду на которой выполняется компиляция, не более.

Лучше бы делом занимались, честное слово

я предпологаю что в основе реализации раста лежит темплейт , если проверить именно те операции о которых я выше написал и принципиально соблюдать такие расчеты какие близки в 3д мы попадаем на тест https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/fannkuchredux.html#fannkuchredux

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

тоесть матрицы-память-расчеты-(практически с любым нажатием в игре происходят трансформации матриц 4х4 и не 1 раз а около (на 1 модель 1)(на модель с костной анимацией на CPU примерно сколько костей)(соотв концы результирующие решает GPU))

хотя согласшусь такое себе

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

Тестируйте хотя бы как-нибудь так, коли вам так понравилось :)

#include <math.h>
#include <stdio.h>
#include <time.h>

const float square(const float num) {
    return sqrt(num);
}

int main() {
    struct timespec start, end;
    volatile float result;
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
    for (int i = 0; i < 1000000000; i++) {
        result = square(4.0);
    };
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
    double elapsed_secs = (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1e9;
    printf("Elapsed CPU time: %.9f seconds\n", elapsed_secs);
    return 0;
}
use libc::{clock_gettime, timespec, CLOCK_PROCESS_CPUTIME_ID};
use std::hint::black_box;

fn get_time() -> timespec {
    let mut ts = unsafe { std::mem::zeroed::<timespec>() };
    if unsafe { clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &mut ts) } == -1 {
        panic!("Failed to get time");
    }
    ts
}

pub fn main() {
    let start = get_time();
    for _ in 0..1_000_000_000 {
        black_box(4.0f32.sqrt());
    }
    let end = get_time();
    let elapsed_secs = (end.tv_sec - start.tv_sec) as f64 + (end.tv_nsec - start.tv_nsec) as f64 / 1e9;
    println!("Elapsed CPU time: {} seconds", elapsed_secs);
}

раст С не конкурент по производительности

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

Вот бенчи со всякими веб-фреймворками, там раст обычно в топе, вместе с C и Java и иногда с го

Вот бенчи с числодробилками, там раст +- на уровне с С и С++

fn main() {
    let iter = (1..100).map(|x| {
        println!("{x}");
        x
    });
    let v: Vec<u64> = iter.collect();
    // Что-нибудь делаем с v
}

Так конечно можно, но это отвратительно.

Map нужен для конвертации одного объекта в другой, а inspect для инспекции первого объекта, они выглядит похоже, но решают концептуально разные вещи

Антон Волков вместе со своей командой пилят игры и движки на Rust

Ну, как тут не вспомнить Пола Грэхэма :)

Programming languages teach you not to want what they don't provide.

А разве так не проще работать с PartialOrd, PartialEq? И ведь код работает правильно, как и должен.

use std::cmp::PartialOrd;

#[derive(PartialEq, PartialOrd)]
struct NamedNumber(i64, char);

fn main() {
    let a = NamedNumber(12, 'A');
    let b = NamedNumber(12, 'B');

    match () {
        () if a > b => println!("a is more than b"),
        () if a == b => println!("a is equal to b"),
        () if a < b => println!("a is less than b"),
        _ => println!("it is impossible..."),
    }
}

Так ведь вместо match можно легко заменить на if - else if - else. И всё отлично работает.

use std::cmp::PartialOrd;

#[derive(PartialEq, PartialOrd)]
struct NamedNumber(i64, char);

fn compare(a: NamedNumber, b: NamedNumber) -> String {
    if a > b {
        String::from("a is more than b")
    } else if a == b {
        String::from("a is equal to b")
    } else if a < b {
        String::from("a is less than b")
    } else {
        String::from("it is impossible...")
    }
}

fn main() {
    let a = NamedNumber(12, 'A');
    let b = NamedNumber(12, 'B');

    let result = compare(a, b);
    println!("{}", result);
}

ПС. Но ведь лучше использовать код короче и понятнее, чем ту простыню что написал автор статьи.

Sign up to leave a comment.

Articles