Хабр, привет! Меня зовут Никита, я Rust‑разработчик.
Я давно хотел написать про bool и Option, но последнее время сомневаюсь, насколько кому‑то нужны такие статьи. Когда код пишет агент, happy path работает и задачу уже можно закрывать, обсуждение типов кажется необязательным. Да и код руками пишет всё меньше людей, я, в том числе. Кто‑то даже уже почти не смотрит на то, что сгенерировал агент. Я пока так не «преисполнился», мне всё ещё важны читаемость, тестируемость и корректность, хотя бы на каком‑то минимальном уровне паршивости.
Когда в программе есть bool и Option, я и болванчик не сразу понимаем, что именно они означают. Приходится читать окружающий код и выяснять, о каком режиме или состоянии идет речь. Я хочу видеть эти названия прямо в типах, а недопустимые сочетания значений исключить по построению.
Коротко о типах
Примеры будут на Rust. bool принимает два значения: true или false. Option<T> содержит значение типа T в варианте Some(value) или обозначает его отсутствие вариантом None.
bool в аргументах
Смысл true и false приходится узнавать из сигнатуры. Несколько булевых аргументов можно перепутать местами, и никто ничего про это не скажет.
Сходить в определение функции я, конечно, могу. Но при чтении диффа хочется проверять логику задачи. bool в этом не помогает:
fn start_job(path: &Path, refresh_cache: bool, notify: bool) { /* ... */ } start_job(path, true, false);
Назовём режимы отдельными типами:
enum CachePolicy { Reuse, Refresh } enum NotificationPolicy { Silent, OnCompletion } fn start_job(path: &Path, cache: CachePolicy, notify: NotificationPolicy) { /* ... */ } start_job(path, CachePolicy::Refresh, NotificationPolicy::Silent);
Теперь видно: запускаем задачу с обновлением кеша и без уведомления. Перестановка аргументов даст ошибку типов. Оба enum имеют по два варианта: предметные имена полезны уже здесь.
У этой проблемы есть название boolean trap. Для Rust такую же рекомендацию дают Rust API Guidelines.
Состояние описано флагами
Флаги могут просто кодировать состояние. Два bool дают четыре сочетания. Если все четыре допустимы, по набору состояний модель ничего не теряет. У настроек кеша и уведомлений выше как раз допустимы все сочетания. Проигрыш когнитивный: нужно помнить смысл каждой комбинации. Но с флагами есть ещё две проблемы.
1. Лишние сочетания флагов
Проблема появляется, когда состояний меньше, чем сочетаний флагов. Если для трёх состояний взяли два флага, одно сочетание уже лишнее. Пока флаги можно произвольно создавать и менять, обработчики должны проверять сочетание или полагаться на проверку до вызова.
Например, запущенная попытка обработки файла либо ещё выполняется, либо завершилась успехом, либо ошибкой. Одновременные успех и ошибка не предусмотрены, но такая структура позволяет их записать:
struct AttemptState { succeeded: bool, failed: bool, } fn show_status(state: &AttemptState) -> Result<(), InvalidAttemptState> { if state.succeeded && state.failed { return Err(InvalidAttemptState); } /* ... */ Ok(()) }
Можно закрыть поля и проверять флаги в конструкторе. Тогда методы изменения тоже должны сохранять это правило. Лучше перечислять допустимые состояния явно:
enum AttemptState { Running, Succeeded, Failed, }
Из четырёх сочетаний флагов остались три нужных состояния. Вместо двух ложных флагов теперь Running, а одновременных успеха и ошибки в типе нет.
У Option<bool> три значения: None, Some(false), Some(true). Но смысл каждого исхода всё равно придётся объяснять отдельно.
2. Данные конкретного состояния
Если конкретному состоянию нужны свои данные, одни флаги тоже не задают эту связь. Придётся отдельно проверять, что данные есть в нужном состоянии.
Добавим путь к готовому файлу, обязательный при успехе. Если валидацию не дополнить проверкой нового поля, она продолжит пропускать успех без пути:
struct AttemptState { succeeded: bool, failed: bool, output: Option<PathBuf>, } fn validate(state: &AttemptState) -> Result<(), InvalidAttemptState> { if state.succeeded && state.failed { return Err(InvalidAttemptState); } Ok(()) } let state = AttemptState { succeeded: true, failed: false, output: None }; assert!(validate(&state).is_ok()); // проверка прошла, хотя пути нет
Свяжем путь с успешным исходом:
enum AttemptState { Running, Succeeded { output: PathBuf }, Failed, } let state = AttemptState::Succeeded { output: PathBuf::from("report.pdf"), };
При создании Succeeded путь теперь обязателен: забыть поле не даст компилятор.
let state = AttemptState::Succeeded {}; // ошибка: отсутствует поле `output`
Но использовать эти данные по назначению он не заставляет.
Когда одного None недостаточно
None обозначает отсутствие значения. Но по нему не всегда понятно, что программа должна сделать. None должен иметь однозначный смысл в контексте использования.
Например, оформляем заказ. У покупателя есть сохранённый адрес, но при оформлении можно передать другой:
// Типы Cart и Address здесь опущены. fn checkout(cart: Cart, delivery_address: Option<Address>) { /* ... */ } checkout(cart, None);
None здесь можно прочитать как «самовывоз» или «доставка на сохранённый адрес». По одному вызову не совсем понятно, какая трактовка выбрана.
В этой функции None означает самовывоз. Назовём этот режим:
enum Delivery { Pickup, ToAddress(Address), } fn checkout(cart: Cart, delivery: Delivery) { /* ... */ } checkout(cart, Delivery::Pickup);
У типа те же два варианта, но в вызове теперь явно выбран самовывоз. Компилятор не помешает передать Pickup, когда нужна доставка, но при чтении диффа или теста я вижу выбранный режим.
Где я оставляю bool и Option
bool подходит для вопроса с однозначным ответом «да» или «нет». Например, имя is_empty() уже объясняет, что означает результат. Дополнительный тип здесь ничего не добавляет.
В языках с именованными аргументами они тоже могут прояснить вызов. Иногда достаточно отдельного метода с понятным именем; такой вариант для флагов предлагает Мартин Фаулер. Если режим нужно хранить и передавать между функциями, лучше предпочитать отдельный тип.
Option оставляю, когда значение может отсутствовать и смысл отсутствия однозначен для вызывающего кода. В результате поиска по ключу None означает «ключ не найден». В обобщённом коде, которому важно только наличие значения, Option тоже достаточно.
В сгенерированном коде я постоянно встречаю true, false и None, которые приходится расшифровывать. Я всё ещё читаю код своих и чужих болванчиков и хочу, чтобы выбранные режимы и состояния были названы явно.
Комментарии (7)

Dart-Veider11
07.11.2026 17:58Хороший пример с AttemptState. Но не кажется, что если увлекаться такими enum для каждого bool, то сложность просто переедет из условий в количество типов? Особенно в больших проектах, где ради двух значений приходится постоянно прыгать между определениями.

NikTimf Автор
07.11.2026 17:58Да скорее всего так и будет, это неизбежная сложность, вам в любом случае надо отобразить состояния, я почти всегда делаю это енумом

ImagineTables
07.11.2026 17:58Так-то оно так, но без инлайновых
enumов уж больно накладно заменять все-всеboolи. И ещё есть легаси.
ImagineTables
07.11.2026 17:58Даже не в накладности дело, а в том, что
bool Initialize();тупо выразительнее, чемInitializationResult Initialize();. Было бы можно писать:{ OK, Failed } Initialize();, который разворачивался бы в
enumсо скоупом в виде функции и дефолтным именем типаResult(для возвращаемого значения) и именами, совпадающими с параметрами (для параметров) — ещё куда ни шло, а самому тип городить на каждый чих это перебор. Я ограничиваюсь тем, что строго не использую булевские параметры (по очевидным причинам читать вызовы функций типаFoo(true, false, false, true, true)нереально), и очень страдаю от необходимости явного описания требуемыхenumов, да ещё и не вложенных.Лично я думаю, что чтобы эта идея взлетела, нужен специально спроектированный язык, где с одной стороны были бы все средства, чтобы комфортно писать без
bool/Option, а с другой — чтобы они были запрещены или, хотя бы, дискриминированы (сделаны гражданами не первого класса).
eee
Хорошие мысли. В целом язык мог бы помочь со случаями, где два bool идут рядом с помощью именованных аргументов. А в остальном соглашусь, паттерн матчинг в расте сильный, грех им не пользоваться. С помощью энумов очень легко переделать логику в конечную стейт-машину.
NikTimf Автор
Согласен, переходил на раст с питона и именованных аргументов по началу оч не хватало, но потом привык без них.