Обновить

Комментарии 125

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

*шёпотом* ...Rust... *подготовка к уворачиванию от тапков*

Имхо киллер фича зиг, это то что он очень старается быть похожим на Typescript.

Вторая киллер фича сImport, настолько легко писать под тот же winapi не было никогда.

Я слышал что в rust тоже не сложно интегрировать С библиотеки, но не пробовал, в Go это сущий ад.

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

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

При компиляции в режиме ReleaseFast программа НЕ останавливает работу, в результате чего полностью ломается и начинает творить дичь

Из этого можно сделать неутешительный вывод, что сам автор оригинала не знает даже Zig (не говоря уже о R*кхе-кхе*)

Никакой явной проверки того, что значение c находится в интервале от 1 до 9, не требуется, потому что при выполнении операции сдвига произойдёт переполнение.

И здесь то же самое: в режиме ReleaseFast переполнение НЕ проверяется, из-за чего sudoku.zig падает с сегфолтом, а example.zig пишет «All 1 tests passed» и не заморачиваясь печатает мусор вместо цифр в Input Grid

Статья топ! Спасибо за отличную антирекламу!

Не думаю, что за мою 45-летнюю карьеру какой-то другой язык удивлял меня сильнее, чем Zig. Могу с уверенностью сказать, что Zig — это не только новый язык программирования, но и, на мой взгляд, совершенно новый способ написания программ.

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

Откровенно не понимаю, почему эта статья собрала так много голосов на хакер ньюсе.

В зиге действительно есть интересные концепты, хотелось бы сфокусироваться на них.

Я тоже ждал, когда же о чудесах начнется, но все было скучно. Словно ещё один из тысяч языков.

Вот название у этого языка действительно интересное.

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

yay zig

и оно мне

5 extra/cargo-zigbuild 0.20.1-1 (1.1 MiB 3.5 MiB)
    Compile Cargo project with zig as linker
4 extra/zigbee2mqtt 2.6.3-1 (39.4 MiB 196.8 MiB)
    A Zigbee to MQTT bridge
3 extra/zls 0.15.0-2 (1.2 MiB 3.5 MiB)
    A language server for Zig
2 extra/zig0.14 0.14.1-2 (20.1 MiB 159.2 MiB)
    a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software
1 extra/zig 0.15.2-2 (23.8 MiB 186.4 MiB)
    a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software
==> Packages to install (eg: 1 2 3, 1-3 or ^4)
==> 1 3

И тут обращает на себя внимание пакет 5 - я не знаю зачем менять в Cargo линковщик, но Zig точно оказался как химия - широко простёр руки свои в дела человеческие. Поэтому я сразу за телефон и в Termux - многократно уже убедился, что всё интересненькое в пакетах Termux есть, а постылого ничего и нету, рекомендую лайфхак. И там zig да zls как с куста

Так что

$ zig version
0.15.2
$

Продано, можно поздравлять автора и переводчика. В Termux, правда, 0.15.1.

я не знаю зачем менять в Cargo линковщик,

ну, PR с build.zig во всякие проекты уже достиг уровня меметичности. С учётом того, что zig имеет свой компилятор C видимо удобнее иметь собственный линковщик и наверняка ещё и сборку ускоряет в средах с FFI. Ну, а резонность появления такая же как и у ld-gold, mold, wild - они просто эффективнее занимаются линковкой. Тем более что этот линкощик будет как сабкоманда для карго и никому не мешает.

Любой новый язык программирования это всегда интересно. А в случае с zig интересно зачем они придумали нестандартный синтаксис составных литералов .{ } , чем общепринятый последнее время JSON-like синтаксис не устроил?

По стилю почти тоже самое, что и designated initializers, которые есть в C/C++

речь видимо за

std.debug.print(
  "Current digit {}\nposition in string {}\n" ++
  "line {}\ncolumn {}\ncode {b}\n", 
  .{c, k, i, j, code}
);

что-то старого типа питоньего "{} {}".format(x,y). Видимо до нормальных f-string в zig не добрались - не зря ж оно ещё не 1.0

Имхо это наименьшая из проблем зига. Раст прожил несколько лет без захвата внешних переменных в println! и в целом всем было +- всё равно.

Собственно, потому оно до сих пор и существует в таком виде - лучше чем prinf синтаксис и ладно.

Не понял, про какой "вид" речь? Растовый println поддерживает захват переменных с определённой версии

let local_variable = "some";
println!("format {local_variable} arguments");

Но это всё вкусовщина, что в питоне, что в расте, что в зиге.

Я про Zig говорил. И таки все же не вкусовщина, а сахар.

А почему Zig, а не V? Zig сложнее и местами смахивает на JS.

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

что мешает?

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

не ухудшает производительность

Крестовые shared pointer'ы все таки имеют оверхед.

почти не ухудшает эргономику использования

А рустовые ошибки проверки заимствования возникают в самый неподходящий момент.

При чём тут shared_ptr? Он к безопасности памяти отношения не имеет.

ошибки проверки заимствования возникают в самый неподходящий момент

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

нам то не гони (с)

let s = String::from("hello"); my_func(s); println! ("{}", s) ; // нельзя, АПАСНА

Это пример ущербного дизайна Rust, из-за которого некоторые типы молча перемещаются. К безопасности памяти это решение дизайна языка не имеет никакого отношения.

Чинится это, впрочем, весьма тривиально. Надо или s.clone() добавить, или сигнатуру функции my_func изменить.

Не соглашусь про ущербность дизайна. Этот пример как раз таки понятен для любого плюсовика и заставляет определиться забирает ли владение my_func или нет. По сути защищает от использования после того, как из объекта помували данные. И в C++ это было бы полезно, но не завезли

std::string s{"hello"};
my_func(std::move(s));  // `my_func` забирает владение
std::print("{}", s);    // здесь компилятор C++ молчит, а зря

поэтому здесь самое правильное определиться и сделать так, чтобы my_func владение не забирала

std::string s{"hello"};
my_func(s);  // `my_func` принимает const std::string&
std::print("{}", s);  // всё ok

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

В C++ просто перемещения в строгом понимании вообще нету. std::move ничего не перемещает и только преобразует один вид ссылки в другой. Вся логика перемещения реализована через конструкторы перемещения или операторы перемещающего присваивания. При этом объект, подвергнутый "перемещению" все ещё существует, деструктор его будет вызван и ряд методов для него будут работать (но не все, там всё хитро с этим).

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

ну я вот привёл тривиальный пример. там действительно НАДО clone()?

а в реальном коде такой подход превращает простые алгоритмы в гейпорно с импотентами (слабыми ссылками в тч).

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

там действительно НАДО clone()?

Да, потому что если строка окажется чуть длиннее чем 5 байт (в моей личной практике строки по несколько мегабайт это норма), её копирование будет ОЧЕНЬ дорогим

так не надо копировать. все просто.

работа же идёт с константами - компилятору это видно, и более того явно прописано согласно синтаксису раста

Вы целенаправленно сказали компилятору выделить память в куче и скопировали в неё строковый литерал, управление кучей работает естественно в рантайме, никакими константами здесь и не пахнет

продолжайте мысль, что может случиться?

вызовы константные

String::from никогда не был константным

и что дальше, я вот не могу добиться

там не написано my_func(mut s), и в вызове принт тоже

Там написано my_func(s), что означает полную передачу владения строкой (и связанным с ней куском памяти в куче). Мне нужно пересказывать учебник по основам Rust или о чём мы тут вообще?

мы об

ущербном дизайне Rust

Как верно сказал @Panzerschrek

И ответа "зачем", я доже проблеска не вижу

Зачем что? Про перемещение вместо копирования я уже ответил, про то, что в вашем коде нет констант, я уже тоже разъяснил. Что ещё вам непонятно?

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

в общем я понимаю что это разговор бесполезный

панцер как разработчик компилятора больше в теме

Ещё раз — String::from никогда не был константным. Выделение памяти в куче подразумевает системный вызов к ОС, который не может быть константным даже теоретически. Что вам здесь непонятно?

Ещё раз — String::from никогда не был константным. Выделение памяти в куче подразумевает системный вызов к ОС, который не может быть константным даже теоретически.

я совсем забыл, что в Расте let дает константу по умолчанию

так что s ещё и константа

прекрасный уровень понимания =)

Константу даёт const, let никогда не был константой. Вам серьёзно стоит прочитать учебник по основам Rust

let (без mut) - дает неизменяемую переменную, что с т.з. плюсовика вполне себе константа.

с т.з. плюсовика

То есть человека, не читавшего учебник по основам Rust?)

let не константа в любом случае, независимо от наличия или отсутствия mut

И где здесь хотя бы одно упоминание константы?

Откройте определение константы в том же Rust reference

В трактовке Раста константные переменные не существуют. п 6.9

Так что единственный вариант константных переменных, как в большинстве других языков это объявленные let, именуемые тут иммутабельными. Сути не меняет.

Не надо заниматься софистикой.

Иммутабельные переменные не являются константными

работа же идёт с константами

А что должно произойти, когда вызываемая функция положит эту "константу" куда-нибудь и вернётся, а вызвавшая решит, что строка больше не нужна и освободит память?

это уже хороший пример

сразу конечно ничего не будет, а в "куда-нибудь" будет невалидная [константная] ссылка.

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

Ну вот Rust и следит, выполняя перемещение

Ссылка на кучу по определению не может быть константной, прекращайте уже

в "куда-нибудь" будет невалидная [константная] ссылка

Почему ссылка, мы же саму строку туда сохраняем?

надо компилятору следить за её лайфтаймом.

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

Но вообще, всё проще: если функции нужно владение строкой, то лучше принимать String, если владение не нужно, то &str. А там уже вызывающий код пусть сам определит как ему оптимально поступать: клонировать или отдавать владение готовой строки.

Почему ссылка, мы же саму строку туда сохраняем?

Мы сохраняем объект типа строка, размером в 24 байта. А вот текстовые данные строки лежат в динамической памяти, на которые ссылка может и принести потом проблем

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

Вообще-то не даёт 100% гарантии. Хотя контроль хороший.

Чтобы этот тонкий момент обойти, утечка памяти в Расте проблемой нарушения памяти (memory safety) не считается =)

Приведите пример, когда гарантия нарушится или произойдёт утечка.

Но вообще, всё проще: если функции нужно владение строкой, то лучше принимать String, если владение не нужно, то &str. А там уже вызывающий код пусть сам определит как ему оптимально поступать: клонировать или отдавать владение готовой строки.

Дело не столько в строках, сколько в любых ссылочных типах.

Обычно есть владеющая ссылка, полученная при создании объекта, и потом этот объект кому то передается во владение, хранение итп. И потом нужно с этим объектом работать - хотя бы читать его св-ва. Для этого нужна ещё одна [читающая] ссылка, [а может и больше] , что Растом запрещено. И начинаются пляски либо с клоном в простых случаях, либо же с явными прописываниями Rc::, Arc

И то, что эти пляски нажинаются с примитивного вызова println, удобства разработки не добавляют.

Производительности, кстати, тоже. Rc и/или Arc аллоцируют в куче.

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

ещё одна [читающая] ссылка, [а может и больше] , что Растом запрещено

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

Несколько читающих ссылок очень даже разрешены, пока одновременно с ними нет mut-ссылки

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

Термина «владеющая ссылка», кстати, не существует

владелец объекта

Это кто? Пример

На примере вашего же кода:

let s = String::from("hello");

Функция String::from выделяет память в куче, создаёт объект с указателем на кучу (вообще-то в Rust нет объектов, а есть структуры, но да ладно), автоматически становится владельцем этого объекта, но дальше передаёт владение тому, кто вызвал функцию — видимо, владельцем будет функция main() или куда там вы пихаете свой код

Имейте в виду, что владение означает получение полных прав над объектом, то есть владелец имеет полное право, например, переместить объект из немутабельной переменной в мутабельную let mut s = s; и менять строку на здоровье (ещё раз, немутабельные переменные это НЕ константы)

my_func(s);

Функция main передаёт владение объектом в функцию my_func, которая и становится владельцем и получает право делать с объектом что угодно (в том числе пихать в let mut как я показал выше)

Дальше у функции my_func есть три варианта: или передать владение кому-то ещё (видимо в глобальную переменную, потому что больше вроде некуда), или вернуть владение обратно вызывающему (тогда можно будет написать let s = my_func(s); и владеть строкой снова будет функция main), или ничего никуда не передавать — тогда после завершения функции my_func объект останется без владельца и будет вызван деструктор (функция drop), которая освободит память в куче и сделает хранящийся в объекте указатель на кучу невалидным

println! ("{}", s) ;

К этому моменту функция main уже перестала быть владельцем значения, хранившегося в старой переменной s, а значит хранящиеся в ней данные могут быть уже невалидны (например, указатель может указывать на освобождённую память в куче) — поэтому Rust абсолютно правильно запрещает использовать старую переменную s, предотвращая use-after-free и прочие страшные штуки

видимо, владельцем будет функция main() или куда там вы пихаете свой код..

Так владелец переменной функция?

видимо в глобальную переменную, потому что больше вроде некуда

А теперь какая функция?

переместить объект из немутабельной переменной в мутабельную let mut s = s; и менять строку на здоровье (ещё раз, немутабельные переменные это НЕ константы)

Нет, это всего лишь shadowing

Для такого поведения есть целая концепция interior mutability

Но все это все равно не относится к делу о криволапости владения.

Так владелец переменной функция?

Переменная s в функции main, если конкретнее

Нет, это всего лишь shadowing

shadowing старой переменной, а новая переменная становится новым владельцем и получает право мутировать объект, да и shadowing здесь ни при чём, напишите let mut s2 = s; и получите то же самое (вместе с невозможностью использовать старую переменную s, так как она перестала быть владельцем)

Для такого поведения есть целая концепция interior mutability

Ну раз вы в курсе даже про interior mutability, то к чему были все эти свистопляски с якобы константными вызовами

криволапости владения

Я вроде только что продемонстрировал, что владение — ни разу не криволапая штука, успешно защищающая недостаточно внимательного программиста от случайных ошибок при работе с памятью (и немного даже от логических ошибок, хотя не от всех)

Я вроде только что продемонстрировал, что владение — ни разу не криволапая штука, успешно защищающая недостаточно внимательного программиста от случайных ошибок при работе с памятью (и немного даже от логических ошибок, хотя не от всех)

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

Можно программисту отрубить обе руки, и он вообще не будет писать программы с ошибками.

Раст, условно, лишает одной руки.

С обеих рук очень удобно отстреливать себе обе ноги, это да)

владеющая ссылка

Ссылка - это как раз просто ссылка, она не даёт владение объектом. Если владение объектом передаётся куда-то, то ссылок на сам объект в этот момент не может существовать, иначе бы они становились невалидными и нарушали гарантии языка.

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

Нет, не запрещено.

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

Нельзя подменить доступ к объекту доступом к клону объекта, потому что клон - это уже другой объект, семантика операции получается совсем другой.

Ссылка не в смысле амперсенд, а имя ссылочной переменной.

let x = Box::new(42); это тоже ссылка в общепринятой терминологии

Вроде как проблема имеет решение.

Передавать владение только пишушим [ссылкам] , т.е при вызове fn(mut s), fn (&mut s) .

При вызовах fn(s), fn(&s) владение не передавать.

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

Короче, ждём Руст 2.0

Т.е. ещё один тип ссылок добавить в язык, внутрипоточные ссылки? И зачем владение передавать в fn (&mut s)? Как мне теперь передать в функцию объект, чтобы он в ней модифицировался?

Вообще то TLS (thread local storage) переменные уже существуют, это не фантастика.

fn(mut s), fn (&mut s) и есть передача владения, чтобы в функции делать с ним что хочешь. Во втором случае правда, снаружи останется висячая ссылка - функция же при выходе переменную прибьет, отменяем вариант. Т.е только fn(mut s)

ослабить требование на читающие ссылки

Я не очень понял что конкретно вы пытаетесь сделать — разрешить передавать владение не ломая читающие ссылки? Это технически невозможно, потому что любое изменение объекта рискует сделать связанные с ним ссылки невалидными

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

потому что любое изменение объекта рискует сделать связанные с ним ссылки невалидными

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

не передавать владение без необходимости и неявно

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

либо статическим анализом посложнее

Думаю, если бы такое было возможно, создатели Rust сделали бы это с самого начала

Как я уже говорил

К сожалению, из того что ты говорил, верно оказалось только 50% =)

Думаю, если бы такое было возможно, создатели Rust сделали бы это с самого начала

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

Результат пока меня не устраивает - см соседнюю статью - код на Расте длиннее и запутаннее других ЯП.

Так что, возвращаясь к теме статьи - Zig выглядит лучше. Особенно в эмбеде

Zig выглядит лучше

С двумя UB на ровном месте он ну вообще никак не может быть лучше. Некрасивый, но рабочий код бесконечно лучше сломанного кода, сколь бы красивым он ни был

  1. Я не вижу там UB, есть специально сделанный ReleaseSafe

  2. Если вести себя зеркально, то в Zig нет UB по определению (как константных переменных в расте)

Скрытый текст

В нем называется IB (illegal behavior=)

ReleaseSafe

И в этом режиме Zig оказывается примерно на 5% медленнее аналогичного кода на Rust, при этом safe Rust продолжает оставаться safe при любых параметрах компиляции. В общем, Zig не нужен

Это абсолютно не факт, с учётом количества оберток [подорожника] которыми приходится обкладывать BC.

Без тестов, короче, не верю.

Например, прошлогодние тесты 20 яп

safe Rust продолжает оставаться safe при любых параметрах компиляции. 

И, увы, опять неверно. В release профиле отключаются ассерты и проверки переполнений.

Ассерты отключаются не все, а только отладочные, которые не влияют на безопасность, а поведение переполнения не является UB и тоже не влияет на безопасность. Сколько раз я уже советовал вам прочитать учебник по основам Rust?

При чём тут shared_ptr? Он к безопасности памяти отношения не имеет.

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

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

как небезопасный язык

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

Вон выше в комментах я всего за полчаса тыкания наткнулся на целых два undefined behavior, чего в принципе достаточно, чтобы опровергнуть ваш комментарий

Чтобы иметь UB нужно иметь какую-то спеку. Учитывая что языка ещё не 1.0 там половина языка фактически undefined.

Ну и второй момент - ReleaseFast видимо выключает какой-то набор проверок для того чтобы работалось быстрее - аналогично флагу -ffast-math в С++ отключающий комплаенс для floating point значений. Логично, что если не знаешь чем это сулит, то и результат получается соотвествующий. Так что незнаю чего вы там конкретно наопровергали.

Сразу оговорюсь, что не спец по Zig и могу ошибаться в поведении флагов компиляции и моё знание о стабильности различных компонентов языка довольно ограничено.

Ну из всего этого можно сделать вывод, что Zig как минимум пока что не готов для использования

Rust в режиме release (точнее, cargo, но не суть) тоже отключает некоторые проверки, но это не приводит к UB

Зависит от того, что вы требуете от языка. От первого компилятора до первого стандарта Си тоже немало времени прошло и компиляторы языка под разные платформы в своё время тоже содержали кучу нюансов и багов, пока наконец не оформили ANSI C стандарт, к которому уже начали стремиться. Кода на С в проде к моменту появления стандарта было уже немало. Так чем ситуация с Zig принципиально иная? Кому-то важна стабильность языка и возможность делать формальную аттестацию по спеке и наворачивать разную верификацию, кому-то и эргономной и эффективной разработки достаточно.

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

Очень дурной подход - сначала отказаться сознательно от деструкторов,

А когда их в Zig завозили вообще? Там либо писали собственную функцию удаления как и в Си и при помощи defer организовывали себе RAII - определённо чище чем goto cleanup, либо использовали bump аллокатор, который в конце области видимости очищали - ни тебе фрагментации, ни опасности, что что-то потечёт. Какие проблемы с сырыми указателями у вас в таком формате возникают?

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

В описанном вами подходе граблей всё ещё полно и назвать его "безопасным" - язык не поворачивается. Можно забыть написать удаление ресурсов в defer. Можно дважды удалить ресурс (если он куда-то в другую функцию передаётся, и та тоже вызывает освобождение в своём defer). Можно сохранить указатель ну некую область памяти и освободить её, забыв про этот указатель и разименовав его позже.

defer - вообще костыль, который противоречит базовым принципам структурного программирования. RAII его назвать нельзя, ибо RAII - это управление ресурсами на уровне типов.

Вообще, безопасность, это не "ну вроде норм выглядит", а формальное свойство, гарантируемое дизайном языка. В Zig её нету и авторы даже не пытаются её реализовать.

граблей всё ещё полно и назвать его "безопасным"

Они себе и не позиционируют как безопасный. Более безопасная альтернатива Си - да, но это скорее следствие попыток внедриться в часть ниши Си.

Zig is a general-purpose programming language and toolchain for maintaining robust, optimal and reusable software.

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

Можно забыть написать удаление ресурсов в defer.

Также как и позвать delete в с++ или позвать его на адресе из стека и double-free всё ещё проблема. Параболичность рук в среднем везде одинаково мешает.

Просто статья плохая. Даже обзор фич на родном сайте лучше.

Особой небезопасности не заметил. Ну ручное управление ресурсами с вариациями defer. Но с указателями уже огорожено

Это просто статья неудачная. Там автор зачем-то делает всё как привык в C. Если же делать правильно, то он безопасный по памяти. При чем на этапе компиляции критичные места будут подсвечены и он просто не скомпилируется. Автор просто не разобрался в средствах языка и создал ложное мнение о нём у читателей.

Тут сразу проблема Zig в том, что он позволил автору сделать что-то неправильно. А делать правильно можно хоть на Си, хоть на языке ассемблера, изобретать новый язык для этого не надо. Язык, претендующий на звание безопасного по памяти, должен заставлять делать правильно

Тоже верно. Хотя... Скажем так, Zig поощряет делать правильно, но если ты хочешь делать неправильно — твое право. С другой стороны, практически любой язык такой. Тот же Rust, обернул unsafe и твори что душе угодно. Но почему-то никто не говорит, что язык плохой. ))

В коде реализации стека какая-то дичь написана. Зачем там matrix? Который к тому же никак не используется.

Наверно, самое невероятное достоинство компилятора Zig — способность компиляции кода на C.

Когда вместо этого можно написать GCC или LLVM фронтенд? Так себе достоинство. Скорее говорит, что авторы языка выбирают свой особый путь, либо не особо в теме компиляторов.

А они умеют и в LLVM бэкэнд, но хотят сделать лучше.

Как-то странно автор стурктурировал всё. Про улучшенние рефакторинга не рассказал, про обработку ошибок и работу с опционалами тоже не упомянул.

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

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

Все эти fn и двоеточия даром не нужны. Особенно сильно бесят сокращения pub и fn. Это какая жалкая экономия на спичках. Так и представляю себе, как автор языка уделал всех остальных программистов мира в скорости разработки за счет экономии на буквах. Только вот есть один большой просчет. Как же это так, что у нас есть красивый и компактный fn, и при этом остался омерзительный жирный return? Должно быть ret, а ещё лучше просто r. Экономия должна быть экономной. Ну и const нужно сократить до cn.

Спасибо, но я остаюсь на Си, ваш синтаксический сахар меня не усластил.

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

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

Особенно сильно бесят сокращения pub и fn.

Надо было вообще как паскале сделать procedure и begin end

а разве спиральные объявления не лаконичны? =)

читаются плохо, это да

А как сложилосью, что вы не стали писать на APL?

Я не настолько старый

Ну если typedef писать лениво, то почему и нет-то?

char (signal(int, bool (*callback)(char *, int))); можно развернуть например так:

typedef bool (*Callback)(char *, int);

typedef char (SignalFunc)(int, Callback);

В итоге получаем: SignalFunc signal; Давно забыл Си, за точность не ручаюсь, но typedef это про оно самое.

Главное не запутаться в скобках и кто на кого указатель крошит, ага. И не забыть потом что за void* прячется.

Ретурны вообще надо было убрать. Чтоб как в расте, какая последняя строчка, такой и ретурн.

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

fn, fun, func, function, def, sub, procedure


лаконичнее уже ничего быть не может

А как же отсутствие круглых скобок у условий? Здорово разгружает визуальную нагрузку, как по мне.

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

Визуальную нагрузку — вы серьёзно? Дело попахивает СДВГ, если пара лишних скобочек мешает вам работать и адекватно воспринимать код.

К тому же, есть общая логика в том, что все control structure имеет унифицированный синтаксис keyword(...) statementsblock

Если убирать скобочки у if, то тогда по общей логике нужно убирать и у while, for, switch, а это не совместимо с тем, что у for внутри скобочек не просто выражение, а несколько выражений.

К тому же, скобочки у обычного if прекрасно контрастируют с отсутствием скобочек у #if, а устранение скобочек сближает по степени схожести.

Желание убрать скобочки — это когда делать больше нечего, а что-то сделать хочется.

если пара лишних скобочек

Сами придумали - сами возмутились. Нередко внутри скобок ещё несколько вложенных.

тогда по общей логике нужно убирать и у while, for, switch

Разве у новых языков это не так?

Нередко внутри скобок ещё несколько вложенных.

И что? Они там не просто так, наверное. И всегда существует масса способов оформить внешнее восприятие кода так, чтобы не путаться в скобочках. А если кого-то пара ВНЕШНИХ скобочек огорачает своей избыточностей, то может надо чем-нибудь другим заниматься?

Разве у новых языков это не так?

Плевать, как там хипстеры выделываются. У них design decisions делаются по принципу «лишь бы выпрендриться и отличиться от классики».

45 лет за компами .. сколько же на моей памяти было этих "убийц Си" и позже С++ .. сбился со счету. И самое смешное где их обльшая часть? )

А мне даже интересно сколько тех убийц вы застали? Из относительно старых знаю только D бодался с плюсами. С# бодался с Java и даже преуспел во многом. Ну а молодые убицы типа Rust только набирают обороты. Всякие "промежуточные" убийцы типа cppfront и carbon ещё моложе, так что им ещё расти и расти до уровня хотя бы Rust 1.0. Остальное всё в экспериментах. Из убийц си можно вспомнить V lang, Zig и Odin, Но опять же, они очень молодые и нестабильные, чтобы получить более широкое распространение.

Самым первым убивцем Си вообще-то была Ада (1979). )

Давайте дополню:

Ада (1979) - позиционировался как язык системного программирования, обладал кучей достоинств (писал компилятор, бросил), кмк, его время ещё не пришло;

Модула, Модула2 -- также позиционировался как язык системного программирования (ОС Лилит?). Не взлетело. Наследник Оберон, Джава. Последняя также позиционировалась как замена С++, но увы, только смогла отвоевать свою (не малую) нишу и только. Где-то тут можно вспомнить и про Дельфи и Модула3..

Cyclone (1990-е где-то там) - прямая попытка создания безопасного Си. :) Из него вырос .. Rust (ещё один претендент).

Chapel (Cray) .. 2006? не помню уже, только знакомился слегка очень.. тоже "призван заменить С/С++" (ещё и Фортран)

.. можно ещё наверное вспомнить с десяток, не меньше.. ) Аз да, как же Julia (2012?).. Clay какже.. BitC, C2..

Ада (1979) - позиционировался как язык системного программирования

быть ЯП для системного программирования и быть конкурентом си не одно и то же. Собственно, он и сейчас является ЯП для сиспрога, но в средах с требованием повышенной надёжности. Насколько я понимаю, Rust во многом унаследовал его строгость типов, пусть и не на прямую. Одновременно удивлён, что кажется язык никогда не был популярнее Fortran. Хотя может в то время особо не думали про построение безопасных приложений и API и вся эта строгость выглядела как излишество. Что там с портируемостью компилятора тоже отдельный вопрос. Хотя в современном GCC это вроде один из самых популярных компиляторов после сиплюсов и фортрана.

Java в 90х позиционировалась как конкурент плюсов, но там обычно посмеивались над теми адептами и показывали джавистам, то что нынче рекламируют как zero cost abstractions, плюс первый стандарт. Поэтому джависты перешли к рекламе installed on over billion devices в начале нулевых и плотно засели на мобильных девайсах. Да и сейчас дела у них тоже неплохо идут.

Cyclone

Могу представить какие части Rust мог подсмотреть, но кажется в условном OCaml языковых фишек к тому моменту было больше, включая тот же null safety. Либо циклоньи части достались unsafe Rust. Когда-то о нём слышал, но не видел, чтобы его продвигали именно как убийцу си. Хотя попытка была неплоха.

Chapel

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

Julia

Вот уж точно кого не ожидал увидеть в списке. Afaik его никогда не позиционировали как убийцу плюсов/си, т.к. у него была ниша научных вычислений, где доминировал Python с оптимизированными сишными либами типа numpy. В некотором смысле Жюлиа справился - план покрытия функционала всяких уже выполнен и перевыполнен и вроде даже может соперничать с Wolfram/Matlab на предмет специализированных средств для мат.моделирования и рассчётов. Но там ниша довольно узкая пока.

BitC/C2/C3 - это скорее любительские эксперименты (как и v-lang) без какой-либо серьёзной адаптации сообществом, нежели серьёзные заявки на убийц. Но в среднем они все довольно молодые и нестабильные, как и степень активности в соответствующих репозиториях в сравнении с тем же Zig/Odin.

Не понял из статьи какие же все таки плюсы у Zig в сранении с другими языками?

Синтакиси не является преимуществом, т.к. у всех языков свой синтаксис. Может безопаснее чем другие языки? Нет. Быстрее и меньше потребляет ресурсов, чем другие языки? нет. Может на нем быстрее писать чем на других языках? Нет. Стандартная библиотека лучше чем у других языков? Нет. Большое сообщество и большое количество сторонних библиотек? Нет. Компиляция быстрее чем в других языках? Нет.

Так в чем плюсы Zig то?

Как минимум на три вопроса ответ-да

Синтаксис преимуществом является, повышает читаемость. Но в этом Циг середнячок

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

И да, он безопаснее, чем C. Стандартная библиотека на порядок лучше, чем в Си. На счёт того, кто быстрее... Тут, наверное, Си немного выиграет, потому что в нём есть оптимизации на основе UB и отсутствуют многие проверки. Зато в Zig гораздо проще работать с SIMD! Но в любом случае, Zig - это очень быстрый язык. Хотя правильно написанный код на Julia может оказаться быстрее неправильно написанного кода на Zig...

Самое любимое преимущество Zig - это алгебраические типы данных. А именно, маркированное объединение (tagged union) - тип сумма. В C есть union, но он не хранит информации о том, какой именно вариант сейчас активен. В Zig union(enum) хранит тег, и его можно красиво сочетать со switch
```

const Res = union(enum) {
    node: *Node,
    fail,

    pub fn success(node: *Node) Res {
        return .{ .node = node };
    }
};
pub fn next(self: *Parser) anyerror!*Node {
        switch (try self.readInteger()) {
            .node => |node| return node,
            .fail => {},
        }
        switch (try self.readSymbol()) {
            .node => |node| return node,
            .fail => {},
        }
        switch (try self.readList()) {
            .node => |node| return node,
            .fail => {},
        }
        return ParsingError.CantParse;
    }

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

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

А ведь в comptime можно производить вычисления над типами... Так реализованы обобщённые типы:

fn Gen(ty: type) type {
    return struct {
        first: ty,
        second: i32,
    };
}

const GenUsize = Gen(usize);

Если сравнивать безопасность Си и Zig, то побеждает конечно же Zig.
В нём все указатели, которые могут содержать Null обязательно помечаются, и если мы хотим из разыменовать, нам нужно будет их проверить, что они не Null.


fn foo(a: ?*GenUsize) void {
    if (a) |gen| { // Вот тут мы проверяем, что указатель не null, и кладём его значение в gen, если он таки не null.
        std.debug.print("First: {}, Second: {}\n", .{ gen.first, gen.second });
    } else {
        std.debug.print("No value\n", .{});
    }
}

Работа с памятью заметно улучшена, по сравнению с Си. Для выделения памяти используется специальный объект - аллокатор. Если функция решила выделить память, она должна принимать аллокатор в качестве параметра:

const std = @import("std");

/// Создаёт динамический массив целых чисел от 0 до n-1,
/// возвращает срез (slice). Вызывающий отвечает за освобождение памяти.
fn createRange(allocator: std.mem.Allocator, n: usize) ![]u64 {
    // Выделяем память под n элементов
    const slice = try allocator.alloc(u64, n);

    // Заполняем значениями
    for (slice, 0..) |*item, i| {
        item.* = @intCast(i);
    }

    return slice;
}

pub fn main() !void {
    // Используем GeneralPurposeAllocator для отслеживания утечек
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            std.debug.print("Обнаружена утечка памяти!\n", .{});
        }
    }

    const allocator = gpa.allocator();

    // Вызываем нашу функцию
    const numbers = try createRange(allocator, 10);
    defer allocator.free(numbers); // освобождаем при выходе из scope

    // Печатаем результат
    for (numbers) |num| {
        std.debug.print("{} ", .{num});
    }
    std.debug.print("\n", .{});
}


Этих аллокаторов есть несколько видов, самые полезные - это GeneralPurposeAllocator и аллокатор арена.

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

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

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

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

Ещё одно прекрасное преимущество Zig - это обработка ошибок. Начнём с того, что в отличие от Си она тут хотя бы есть! То, что есть в Си - не считается, это костыли и прошлый век.

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

const MyError = error {
    Ploho, // Возможноые варианты ошибки
    Ujasno,
};

fn baz(a: usize) MyError!void { // Помечаем, что функция вернёт либо ошибку типа MyError, либо void
    if (a % 2 == 0) {
        return error.Ploho;
    } if (a % 3 == 0) {
        return error.Ujasno;
    }
}

pub fn main() void {
    try baz(1); // Просто выполнится, так-как baz не возвращает ошибку. try ничего не сделает
    baz(2) catch |err| { // Перехватываем ошибку, и что-то с ней делаем
        std.debug.print("Error: {}\n", .{err});
    };
    try baz(4); // Так-как baz вернёт ошибку, оператор try просто вернёт её из функции.


Короче говоря, Zig побеждает C по многим параметрам. Это я ещё не все перечислил. Составить ему конкуренцию могут только Rust и C++. Что же он противопоставит им? Простоту. Zig очень простой язык. Говорят, его реально, без шуток можно выучить за 21 день, если вы уже умеете программировать. Я слышал отзыв одной компании, которая использовала Zig, что ей не нужно было нанимать тех, кто его уже знает. Они могли просто научить ему программиста сами.

C++ он побеждает ещё и тем, что в Zig нет такого адского количества неопределённого поведения. А ещё в C++ куча легаси, оставшегося за пятьдесят лет развития, и он унаследовал множество недостатков Си! Его очень трудно знать хорошо.

Rust, наверное, самый сильный противник. Думаю, что многим он подойдёт больше. С другой стороны, в Zig управление памятью полностью ручное, а в Rust оно автоматическое. Часто это плюс, но я могу представить ситуацию, когда это минус. Хотя не знаю, что это за ситуацию.

И вообще, тут много вкусовщины. Мне Zig нравится больше всего. Rust я не осилил. C++ мне категорически не нравится.

Очень советую попробовать написать на Zig достаточно большую программу, чтобы почувствовать, ваш это язык, или не ваш. Если вы любите C, то скорее всего полюбите и Zig.

Серьёзным нюансом Zig есть отсутствие метапрограммирования. И это не позволит ему в части областей конкурировать с более сложными языками.

И синтаксис читается сложнее.

Comptime, ну я не знаю фича ли это - то что просто выкидывается оптимизатором.

ADT в С23 эмулируется именованными компонентами структур, и безопасность норм подтянули.

Так что норм драка будет.

Хотя Zig можно просто совмещать с С в одном проекте.

Случайно не хайлем?...

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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
ruvds.com
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
ruvds