Инвертирование равенства. Как реже стрелять себе в ногу в C-подобных языках
- вторник, 10 февраля 2026 г. в 00:00:04
Встав утром и посмотрев в профиль, отметил: на Хабре много лет, писал статьи, писал код. И ни разу не писал о коде на Хабре. А вообще-то разработчик. Поззорище! Пора исправляться.
Поговорим о классической (и болезненной) проблеме кодирования "присваивание вместо равенства" которая в любой момент может создать очень много проблем. О логическом источнике этой ошибке, и о способах решения.
Ну и ещё слегка вспомним "Звездные войны" :)
Когда я учился разработке, одним из главных способов в большом коде получить "мартышку с гранатой" была типичная ошибка - внутри условного оператора вместо равенства произвести присваивание (то есть вместо == написать просто =).
<?php $q=1; //Do something big & wise //Continue to do smth really cool //Have a breakfast if($q == 2 || $q = "foot bar"){ // Now we need to plant a tree // }
Несмотря на то, что ошибка известна почти всем - она всё равно появляется. И хуже всего то, как она себя ведёт в языках с динамической типизацией. По итогу тут мы получили неправильное ветвление: операция присваивания прошла успешно, а условие ($q=2) возвращает булево true, приведение к которому требуется внутри if().
А ещё, если не повезло - мы сбили счетчик. При этом заметить сразу-же такое получается далеко не всегда.
Попробую привести пример (погонял ИИ с просьбой сделать более-менее реалистичный код), где ошибка уже не так видна и очевидна:
<?php // Имитируем получение настроек безопасности из Redis/API $security_policy = ["min_delay" => 3600, "check_ip" => true, "alert_level" => "high"]; $raw_logs = ["last_attempt" => "2023-11-20 12:00:05", "source" => "internal"]; // Вычисляем интервалы в разных форматах для "шума" $now = new DateTime(); $last_hit = DateTime::createFromFormat('Y-m-d H:i:s', $raw_logs['last_attempt']); $since_last_hit = $now->getTimestamp() - $last_hit->getTimestamp(); $is_suspicious = ($since_last_hit < $security_policy['min_delay']); $days_passed = floor($since_last_hit / (24 * 3600)); $formatted_diff = $now->diff($last_hit)->format('%R%a days %H:%I:%S'); // Данные пользователя $user = [ 'id' => 777, 'role' => 'guest', 'is_verified' => false, 'flags' => ['warned' => false, 'locked' => false] ]; if ($days_passed < 7 && $user['role'] == 'guest' || $user['is_verified'] = true) { // БАГ: $user['is_verified'] ТЕПЕРЬ ВСЕГДА TRUE. $report = "Security scan: " . $formatted_diff . " | Level: " . $security_policy['alert_level']; file_put_contents('security.log', $report . PHP_EOL, FILE_APPEND); echo "Action executed for user " . $user['id']; } // Проверяем последствия: var_dump($user['is_verified']); // Выведет: bool(true) — мы только что сломали логику прав доступа
Поиск же подобного счастья (особенно когда это не первое условие) на том же JavaScript - может быть тем ещё развлечением. А особенно весело будет, если второе-третье условие окажется ещё и редко применяемым:
В JavaScript этот баг выглядит ещё «невиннее», потому что мы часто используем объекты и асинхронность. Когда код перегружен промисами, деструктуризацией и манипуляциями с датами, одиночное равно в конце длинной цепочки условий превращается в идеального диверсанта.
Вот пример с имитацией загрузки данных и проверкой прав доступа, где ошибка задвинута в самый край:
// Имитируем получение данных с сервера const fetchLogData = async () => ({ lastLogin: "2023-11-15T10:30:00Z", attempts: 3, metadata: { ip: "192.168.1.1", location: "MSK" } }); const user = { id: 101, role: "guest", isPremium: false, permissions: ["read"] }; async function checkAccess() { const log = await fetchLogData(); // Работаем с датами: вычисляем, сколько часов назад был вход const now = new Date(); const lastLoginDate = new Date(log.lastLogin); const hoursSinceLogin = Math.abs(now - lastLoginDate) / 36e5; // 36e5 = 60 * 60 * 1000 const gracePeriod = 24 * 7; // неделя в часах const isLoginFresh = hoursSinceLogin < gracePeriod; // --- ЛОВУШКА --- if (isLoginFresh && log.attempts < 5 && user.role === "guest" || user.isPremium = true) { // КАТАСТРОФА: // 1. user.isPremium теперь ВСЕГДА true. // 2. Весь if теперь ВСЕГДА true (так как результат присваивания — true). console.log(`[AUTH] Access granted for ID ${user.id}.`); console.log(`[DEBUG] Session hours: ${hoursSinceLogin.toFixed(2)}`); grantTemporaryAccess(user); } } function grantTemporaryAccess(u) { console.log("Status check:", u.isPremium ? "PREMIUM" : "REGULAR"); } checkAccess();
Ситуация может совсем выходить из под контроля, если мы попадаем во множественную вложенность, да ещё и с побочными эффектами в виде тернарных выражений.
Найдете без подсказки?
<?php $session = [ 'user_id' => 1024, 'permissions' => ['guest'], 'is_admin' => false, 'cache_ttl' => 3600 ]; $auth_log = [ ['ip' => '127.0.0.1', 'status' => 'ok'], ['ip' => '10.0.0.5', 'status' => 'fail'] ]; // Генерируем коллбэк для "ленивой" проверки доступа $check_access = function($u) use ($auth_log) { // Много шума: считаем количество неудачных попыток в логе $fails = array_reduce($auth_log, fn($acc, $item) => $acc + ($item['status'] == 'fail' ? 1 : 0), 0); return ($fails < 3); }; // --- ВОТ ОНО, КЛАДБИЩЕ ЛОГИКИ --- $is_allowed = ($session['user_id'] > 0) ? (function() use (&$session, $check_access) { // Вложенный тернарный оператор внутри замыкания... // ...перемешанный с вызовом функции... // ...и ГДЕ-ТО В КОНЦЕ МЫ СТРЕЛЯЕМ СЕБЕ В НОГУ: return $check_access($session) ? (is_array($session['permissions']) && count($session['permissions']) > 0 && in_array($session['permissions'], $config_permissions)&& check_is_loagavaible($auth_log) && $session['is_admin'] = true ) : false; })() : false; // Результат: echo $is_allowed ? "Доступ открыт" : "Доступ закрыт"; echo "\nСтатус админа теперь: " . ($session['is_admin'] ? "ДА" : "НЕТ");
Ошибка - в самом конце 32й строки.
Да, я понимаю что "хорошо написанное Unit-тестирование выловит это счастье". Но... даже не будем говорить о "плохом стиле" когда тестирования нет. Вы всегда пишите тесты строго параллельно самому коду?
Самое гадкое тут - каждый раз это проблема там, где вы её вообще не ожидаете. Что может быть проще условия?
И всегда проверяете буквально все возможные ответвления и приколы? Даже если второе (для экстремистов - 5е) условие будет выполняться в 1% случаев?
Этот феномен логично рассмотреть через концепции, созвучные «Критике чистого разума» Иммануила Канта.
Согласно Канту, мы и так заперты между двумя мирами: феноменальным (миром явлений, где всё зыбко, полно компромиссов множество шума) и ноуменальным («миром чистого разума» или «вещей в себе»), где царят абсолютные идеи и единство. Обычный человек постоянно балансирует между ними, пытаясь навязать хаосу реальности логические категории и свой порядок.
Однако разработчик, как заметил Фредерик Брукс в книге «Мифический человеко-месяц» (и позже развивали другие теоретики Computer Science), вынужден конструировать код, параллельно пребывая ещё и в третьем мире — мир машинной логики.
Конфликт миров и «ошибка присваивания»
Реальный мир (Мир компромиссов): Здесь мы оперируем контекстом. Мы знаем, что «пользователь активен» — это состояние, которое уже есть. В этом мире мы редко «присваиваем» значения сознательно; мы лишь констатируем факты.
Мир чистого разума (Мир абстракций): Здесь мы строим идеальные алгоритмы. Тут всё четко: если A = B то B = A. Здесь живет математическая гармония. К слову потому, что математика реального мира для оператора равенства работает в обе стороны одинаково.
Мир машинной логики (Мир формальных систем): Это мир предельного буквализма. Здесь символ = не означает «равенство» (как в чистом разуме), он означает «действие по изменению реальности». Также как и бинарные операторы - работают слева-направо.
Диссонанс: В реальном мире вопрос «Это яблоко — красное» является фиксацией факта. В машинном мире
apple = red— это принудительная перекраска любого яблока в красный цвет.
Когда разработчик погружается в код, границы между мирами размываются. Возникает тот самый «когнитивный шум»: мозг начинает путать константы миров. Мы начинаем подсознательно ожидать, что машина «поймет» наше намерение из мира чистого разума, в то время как она тупо исполняет инструкцию из мира логики. С опытом мы, безусловно, допускаем эти ошибки реже. Однако с усталостью и перегрузкой - они всё равно проявляются.
В фильме "Начало", где люди путешествовали между реальным миром и уровнями сна, и путешествовали по множеству различных уровней была сформулирована проблема путаницы между мирами.
А также продемонстрирован интересный способ быстро их интуитивно различать. Ну и "приводить себя в соответствие". Использовался некий "тотем", который в зависимости от мира - подчинялся разным правилам.
Запущенный волчок крутился во сне не переставая, чего не происходило в реальном мире. Это помогало вспомнить в каком из миров они находятся.
В общем, однажды дико "застрелившись" на пару дней с одним (дико разросшимся условием) - мне удалось подсмотреть интересное решение, которое теперь применяю. И хочу поделиться с вами. Также, уже при подготовке этой статьи узнал (век живи, век - учись) что этот метод называется Условия Йоды (Yoda Conditions ). В общем - призываем на помощь гранд-мастера ордена Джедаев:

Вместо if ($q = 2) мы пишем условие в инвертированном виде: if (2 = $q), и тем самым создаем логический парадокс машинного кода. В мире "реальной математики" - ничего не меняется. А вот в коде - меняется всё.
Сравнивать в любом порядке - можно.
Присвоить нечто числу, строке, константе - нельзя.
<?php define("WORLD_WELCOME", "Hello, world!"); $q = 1; /** * Если мы случайно напишем (2 = $q), PHP сразу выдаст Fatal Error, * потому что нельзя присвоить значение литералу (числу). */ if (2 == $q || "foot bar" == $q || WORLD_WELCOME = $q) { // Теперь мы в безопасности. // Если бы мы забыли второе "=", код бы просто не отвалился по ошибке. echo "Time to plant a tree."; }
В таком случае, при нашей ошибке - мы сразу же получаем напутствие от интерпретатора, и идём исправлять это. Подобный стиль - хороший способ защититься от опечатки, либо от ошибки по усталости.
Из минусов такого подхода:
Поначалу вызывает диссонанс. Это немного непривычно читать.
При написании поначалу - совсем непривычно писать. Но быстро привыкаешь
Несколько нарушается единый стиль сравнения. if ('active' = var1 && var2 > 100500) - ну такое себе.
Пытался это победить методом полной инверсии типа: if (3 <= $a) (подразумевая что $a >= 3). Но от таких вот форм при неравенстве довольно быстро вскипает голова. Ибо сложно сообразить как написать: >= либо => . Ибо в голове сразу начинает инвертироваться и путаться почти всё.
Немного погонял ИИ. Он утверждает что на множестве других более современных языков - это стало куда как меньшей проблемой. Ибо на Python либо на Go запрещены присваивания внутри условий. А в Java либо C# - применим лишь частично.
И поэтому данный способ написания if("active" = valiable){} начал уходить в историю в конце 2010х, и сегодня - уже слабоактуален.
Что вы думаете об этом? Есть ли смысл в инверсном стиле (Yoda style) написания подобных выражений? Или этим лишь артефактом сегодня - лишь новичков на собеседованиях пугать?