Об авторе. Федерико Мена-Кинтеро — мексиканский программист, один из основателей проекта GNOME

Элегия C

malloc()/free()

Мои приятные моменты работы с C

Неприятные моменты

free()

Автоматическое управление ресурсами

Автоматическое: не нужно вручную расставлять free() . Память освобождается, файлы закрываются, мьютексы разблокируются вне зоны видимости. Если нужно обернуть внешний ресурс, просто реализуете трейт Drop, и это в основном всё. Такой ресурс становится как будто частью языка, потому что потом не нужно вручную нянчиться с ним.

. Память освобождается, файлы закрываются, мьютексы разблокируются вне зоны видимости. Если нужно обернуть внешний ресурс, просто реализуете трейт Drop, и это в основном всё. Такой ресурс становится как будто частью языка, потому что потом не нужно вручную нянчиться с ним. Детерминировано: ресурсы создаются (память выделяется, происходит инициализация, файлы открываются и т.д.) и уничтожаются вне зоны видимости. Здесь нет сборки мусора: всё действительно уничтожается с закрытием скобки. Вы начинаете рассматривать потоки данных в программе как дерево вызовов функций.

Дженерики (обобщённые типы)

Vec<T>

T

T

Типажи (трейты) — не просто интерфейсы

Drawable

draw ()

Связанные типы

Iterator

pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }

Iterator

Item

next()

Some(YourElementType)

None

for

IntoIterator

pub trait IntoIterator { /// The type of the elements being iterated over. type Item; /// Which kind of iterator are we turning this into? type IntoIter: Iterator<Item=Self::Item>; fn into_iter(self) -> Self::IntoIter; }

Item

IntoIter

Iterator

Срезы

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

Вызывать pkg-config вручную или с помощью макросов Autotools.

вручную или с помощью макросов Autotools. Разбираться с путями в include для заголовочных файлов...

… и файлов библиотек.

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

Cargo.toml

cargo build

Тесты

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

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

Следует выбрать фреймворк для тестирования. Зарегистрировать там тесты. Изучить фреймворк.

#[test] fn test_that_foo_works() { assert!(foo() == expected_result); }

cargo test

Документация с тестами

/// Multiples the specified number by two /// /// ``` /// assert_eq!(multiply_by_two(5), 10); /// ``` fn multiply_by_two(x: i32) -> i32 { x * 2 }

Гигиенические макросы

max (5 + 3, 4)

Нет автоматических преобразований

int

short

char

Нет целочисленного переполнения

В целом, в безопасном Rust отсутствует неопределённое поведение

unsafe {}

>>

Сопоставление шаблонов

gcc

switch()

match()

impl f64 { pub fn sin_cos(self) -> (f64, f64); } let angle: f64 = 42.0; let (sin_angle, cos_angle) = angle.sin_cos();

match()

let color = "green"; match color { "red" => println!("it's red"), "green" => println!("it's green"), _ => println!("it's something else"), }

my_func(true, false, false)

pub struct Fubarize(pub bool); pub struct Frobnify(pub bool); pub struct Bazificate(pub bool); fn my_func(Fubarize(fub): Fubarize, Frobnify(frob): Frobnify, Bazificate(baz): Bazificate) { if fub { ...; } if frob && baz { ...; } } ... my_func(Fubarize(true), Frobnify(false), Bazificate(true));

Стандартная, полезная обработка ошибок

#[derive(Debug)]

#[derive(Debug)]

Замыкания

user_data

Заключение

Librsvg достиг переломного момента: внезапно выясняется, что легче портировать некоторые основные части из C на Rust, чем просто добавить аксессоры. Кроме того, всё больше «мяса» библиотеки сейчас написано на Rust.Сейчас мне приходится часто переключаться между двумя языками, и C теперь выглядит очень, очень примитивным.Я влюбился в C около 24 лет назад. Выучил азы по второму изданию “The C Programming Language by K&R” в переводе на испанский. До этого я использовал достаточно низкоуровневый Turbo Pascal, с указателями и ручным распределением памяти, так что C казался приятным и придающим сил.K&R — отличная книга для выработкии лаконичности. Эта маленькая книжка даже научит вас реализовать простой, что поистине просветляет. Даже низкоуровневые конструкции можно вставлять в самом языке!В последующие годы я хорошо освоил C. Это небольшой язык с маленькой стандартной библиотекой. Вероятно, идеальный язык для реализации ядра Unix на 20 000 строк кода или около того.GIMP и GTK+ научили меня причудливой объектной ориентации в С. GNOME научил, как поддерживать крупные программные проекты на С. Начало казаться, что первый проект в 20 000 строк кода C можно более или менее полно понять за несколько недель.Но кодовые базы уже не такие маленькие. Наш софт теперь выдвигаеттребования к функциям стандартной библиотеки языка.Первый раз прочитал исходники POV-Ray и научился объектной ориентации и наследованию в С.Прочитал исходники GTK+ и изучил стиль программирования с удобочитаемым, поддерживаемым и чистым кодом.Прочитал исходники SIOD, затем первые исходники Guile — и понял, как интерпретатор Scheme можно написать на C.Написал первые версии Eye of GNOME и настроил рендеринг микротайлов.Когда в команде Evolution всё шло наперекосяк. Нам тогда пришлось купить машину Solaris просто чтобы иметь возможность купить Purify; тогда не было Valgrind.Отладка взаимных блокировок gnome-vfs.Безрезультатная отладка Mesa.Принял первоначальную версию Nautilus-share и обнаружил, что там ни разу не используетсяПытался рефакторить код, где я понятия не имел о стратегии управления памятью.Попытался сделать библиотеку из кода, набитого глобальными переменными без единой статической функции.Но ладно, давайте всё-таки поговорим о тех вещах Rust, которых мне не хватает в C.Одной из первых статей, которые я прочитал о Rust, была статья «В Rust никогда не придётся закрывать сокет» . Rust позаимствовал идеи C++: это идиома «получение ресурса есть инициализация» (RAII) , умные указатели, он добавил принцип единственной ответственности для величин и автоматическое, детерминированное управление ресурсами в очень аккуратном виде.Когда постоянно забываешь освободить/закрыть/уничтожить объекты C или, ещё хуже, пытаешься найти в чужом коде места, где забыли это сделать (или сделали, что неправильно)… Нет, я не хочу снова этим заниматься.действительно представляет собой вектор, элементы которого имеют размер. Это не массив указателей к отдельно выделенным объектам. Он компилируетсядля кода, который может обрабатывать только объекты типаНаписав кучу мусорных макросов на C для подобных вещей… не хочу снова этим заниматься. Rust — это не Java-подобный объектно-ориентированный язык . Вместо этого у него есть типажи, которые на первый взгляд кажутся интерфейсами Java — простой способ динамической диспетчеризации: типа если объект реализует, то вы можете предположить наличие методаНо типажи способны на гораздо большее. У типажей могут быть ассоциированные (связанные) типы . Например, типажможет выполняться таким образом:Это значит, что каждый раз при выполнениидля какого-то итерируемого объекта нужно также определить типдля перебираемых элементов. Если выполняетсяи элементы ещё не закончились, то вы получите. Если у итератора закончились элементы, то он вернётСвязанные типы могут ссылаться натипажи.Например, в Rust вы можете использовать циклывезде, где выполняется типажПри выполнении такого типажа следует указать и типв итераторе, и тип— фактический тип для, который определяет состояние итератора.Таким образом можно создавать паутину типов, которые ссылаются друг на друга. У вас может быть типаж «Я могу выполнить foo и bar, но только если предоставите тип, способный делать то и то».Я уже писал об отсутствии срезов строк в C и о том, как это неудобно, когда к ним привыкнешь.Вместо этого:Вы просто пишете файл, в котором перечисляете названия и версии своих зависимостей. Они загружаются c известного адреса или из другого указанного места.Больше не нужно бороться с зависимостями. Всё сразу работает по командеИспользовать юнит-тесты в C очень трудно по нескольким причинам:В Rust вы пишете в любом месте программы или библиотеки:… и когда набираете, ОН БЛИН ПРОСТО РАБОТАЕТ. Этот код связывается только с тестовым бинарником. Не нужно ничего компилировать дважды вручную или химичить с Makefile, или думать, как извлечь внутренние функции для тестирования.Для меня это вообще киллер-фича.Rust генерирует документацию из комментариев в синтаксисе Markdown. Код из документации. Вы можете описать назначение функцииодновременно её протестировать:Код вашего примераи гарантирует, что документация соответствует фактическому коду.: QuietMisdreavus описал, как rustdoc преобразует doctests в исполняемый код . Это магия высокого уровня и чрезвычайно интересно.У Rust есть гигиенические макросы, лишённые всех проблем с неумышленным скрытием идентификаторов в коде C. Вам не нужно писать каждый символ в круглых скобках, чтобы макросправильно работал.Все эти баги в C, из-за которыхслучайно преобразуется вили что-то ещё — Rust так не поступает. Здесь необходимо явное преобразование.Сказано достаточно.В Rust считается багом в языке, если нечто в «безопасном Rust» (то есть всё, что разрешено писать вне блоков) приводит к неопределённому поведению. Можете поставитьперед отрицательным целым числом — и всё будет предсказуемо работать.Знаете эти предупреждения, когда операторприменяется для enum, не обрабатывая все значения? Ну словно маленький ребёнок.В различных местах Rust используется сопоставление шаблонов . Он способен на такой фокус для перечислений внутри. Он может выполнить деконструкцию, так что функция возвращает несколько значений:Можно запуститьна строках. ВЫ МОЖЕТЕ СРАВНИТЬ ГРЁБАНЫЕ СТРОКИ.Показать непонятную строчку?А что если вместо этого сопоставить шаблоны, соответствующие аргументам функции:Я много говорил об этом. Больше никакого boolean без объяснения ошибки, никаких случайных пропусков ошибок, никакой обработки исключений с нелокальными прыжками.Если пишете новый тип (скажем, структуру с кучей полей), можете указать— и Rust будет знать, как автоматически вывести содержимое этого типа для отладки. Больше не придётся писать специальную функцию, которую нужно вызывать вручную в gdb чтобы просто проверить кастомный тип.Больше не придётся вручную передавать указатели функций иЯ ещё не разобрался с главой о «бесстрашном параллелизме» , где компилятор может предотвратить гонку данных в потоковом коде. Предполагаю, что она станет поворотным моментом для тех, кто ежедневно пишет параллельный код.C — это старый язык с примитивными конструкциями и примитивными инструментами. Это был хороший язык для маленького однопроцессорного ядра Unix, которое запускалось в доверенной академической среде. Для современного ПО он уже не подходит.Rust трудно выучить, но это того стоит. Трудно, потому что требуется хорошее понимание кода, который хотите написать. Думаю, это один из тех языков, которые развивают вас как программиста и позволяют решать более амбициозные задачи.