javascript

Инвертирование равенства. Как реже стрелять себе в ногу в C-подобных языках

  • вторник, 10 февраля 2026 г. в 00:00:04
https://habr.com/ru/articles/994488/

Встав утром и посмотрев в профиль, отметил: на Хабре много лет, писал статьи, писал код. И ни разу не писал о коде на Хабре. А вообще-то разработчик. Поззорище! Пора исправляться.

Поговорим о классической (и болезненной) проблеме кодирования "присваивание вместо равенства" которая в любой момент может создать очень много проблем. О логическом источнике этой ошибке, и о способах решения.

Ну и ещё слегка вспомним "Звездные войны" :)


Проблема "диверсанта" внутри условия

Когда я учился разработке, одним из главных способов в большом коде получить "мартышку с гранатой" была типичная ошибка - внутри условного оператора вместо равенства произвести присваивание (то есть вместо == написать просто =).

<?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 - может быть тем ещё развлечением. А особенно весело будет, если второе-третье условие окажется ещё и редко применяемым:

Пример подвоха на JS. Более тяжелый вариант

В 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 подвоха
<?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) написания подобных выражений? Или этим лишь артефактом сегодня - лишь новичков на собеседованиях пугать?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Стоит ли писать о подобных приемах?
0%Да, это полезно0
37.5%Нет, это элементарщина3
62.5%Это устаревший подход5
Проголосовали 8 пользователей. Воздержались 2 пользователя.