Сравнение значений во встроенном языке и запросах 1С 8.3: где ловушки

· 9 мин

Содержание

Два значения в 1С сравнивают операторами =, <>, <, >, <=, >=, а результат всегда имеет тип Булево. Частое заблуждение — что = сравнивает «по смыслу». Нет: 5 = "5" даёт Ложь, "Иванов" = "иванов" — тоже Ложь, а два одинаковых массива не равны друг другу.

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

Операторы сравнения в 1С и что они возвращают

Оператор равенства = и неравенства <> работают с любыми типами. Если типы разные, ошибки не будет — просто Ложь для = и Истина для <>.

Операторы «больше-меньше» строже. Они допустимы только для значений одного примитивного типа: Число, Строка, Дата, Булево. Попытка сравнить 5 < "7" или две ссылки через < завершится ошибкой выполнения: Операции сравнения на больше-меньше допустимы только для значений совпадающих примитивных типов (Булево, Число, Строка, Дата).

ВыражениеРезультат
5 = 5.0Истина
5 = "5"Ложь, без ошибки
Истина = 1Ложь: Булево и Число — разные типы
Ложь < ИстинаИстина
5 < "7"ошибка выполнения
Неопределено = НеопределеноИстина

💡 Логические операции И, ИЛИ в 1С вычисляются сокращённо: если левая часть уже определила результат, правая не вычисляется. Поэтому безопасна проверка такого вида:

Если Значение <> Неопределено И Значение.Количество() > 0 Тогда
    // при Значение = Неопределено вызова Количество() не будет
КонецЕсли;

Для выбора одного из двух значений по условию есть тернарный оператор: Максимум = ?(А > Б, А, Б);. Для чисел и дат удобнее встроенные функции Макс(А, Б) и Мин(А, Б).

Числа: точность и округление

Числа в 1С хранятся в десятичном виде, поэтому классической проблемы 0.1 + 0.2 <> 0.3 здесь нет — это равенство истинно. Проблемы начинаются после деления: результат 1 / 3 содержит длинный хвост знаков, и умножение обратно на 3 не обязано дать ровно 1.

Правило простое: денежные суммы и количества перед сравнением приводите к нужной точности.

Если Окр(СуммаДокумента, 2) = Окр(СуммаОплаты, 2) Тогда
    // суммы совпадают с точностью до копейки
КонецЕсли;

// Допуск вместо строгого равенства
Если Макс(Сумма1 - Сумма2, Сумма2 - Сумма1) <= 0.01 Тогда
    // расхождение не больше копейки
КонецЕсли;

Как сравнить типы двух значений

Прежде чем сравнивать значения, часто нужно убедиться, что они одного типа. Для этого берут тип через ТипЗнч() и сравнивают его с Тип():

Если ТипЗнч(Значение) = Тип("Число") Тогда
    // работаем как с числом
ИначеЕсли ТипЗнч(Значение) = Тип("СправочникСсылка.Контрагенты") Тогда
    // ссылка на контрагента
КонецЕсли;

// Совпадают ли типы двух значений
ТипыСовпадают = (ТипЗнч(Значение1) = ТипЗнч(Значение2));

Строку в Тип() пишут точно так, как тип называется в синтаксис-помощнике. Опечатка в имени («СправочникСсылка.Контрагент» без «ы») даёт ошибку выполнения, а не Ложь.

Если значение может быть одним из нескольких типов, удобнее описание типов:

ДопустимыеТипы = Новый ОписаниеТипов("Число, Строка");
Если ДопустимыеТипы.СодержитТип(ТипЗнч(Значение)) Тогда
    // значение числовое или строковое
КонецЕсли;

Сравнение ссылок, объектов и коллекций

Ссылки сравниваются по идентификатору и типу. Документ1.Ссылка = Документ2.Ссылка — быстрая операция, к базе данных она не обращается. Две ссылки на один и тот же элемент справочника, полученные разными путями, равны.

С объектами иначе. ПолучитьОбъект() каждый раз создаёт новый экземпляр в памяти. Два объекта одного документа, полученные по очереди, оператор = считает разными:

Объект1 = Ссылка.ПолучитьОбъект();
Объект2 = Ссылка.ПолучитьОбъект();
Сообщить(Объект1 = Объект2);               // Нет
Сообщить(Объект1.Ссылка = Объект2.Ссылка); // Да

Вывод: сравнивайте ссылки, а не объекты.

Пустая ссылка

Проверить, что ссылка не заполнена, можно тремя способами:

  • 🔹 Ссылка.Пустая() — метод самой ссылки, нужен известный тип;
  • 🔹 Ссылка = Справочники.Контрагенты.ПустаяСсылка() — сравнение с пустой ссылкой конкретного вида;
  • 🔹 НЕ ЗначениеЗаполнено(Ссылка) — универсально, работает и для Неопределено, и для ссылок любого вида.

В реквизитах составного типа надёжнее всего ЗначениеЗаполнено(): там может лежать и пустая ссылка, и Неопределено.

Массивы, структуры, таблицы

Коллекции сравниваются по ссылке на экземпляр, а не по содержимому. Два массива [1, 2, 3], собранные по отдельности, не равны. Для сравнения содержимого нужен свой код: сравнить количество элементов, затем элементы по порядку.

Функция МассивыРавны(Массив1, Массив2)
    Если Массив1.Количество() <> Массив2.Количество() Тогда
        Возврат Ложь;
    КонецЕсли;
    Для Индекс = 0 По Массив1.ВГраница() Цикл
        Если Массив1[Индекс] <> Массив2[Индекс] Тогда
            Возврат Ложь;
        КонецЕсли;
    КонецЦикла;
    Возврат Истина;
КонецФункции

В типовых конфигурациях на Библиотеке стандартных подсистем есть серверная функция ОбщегоНазначения.ДанныеСовпадают(Данные1, Данные2). Она сравнивает структуры, массивы, соответствия и таблицы значений по содержимому, рекурсивно. Своё сравнение писать не придётся.

Сравнение строк: регистр, пробелы, порядок

Сравнение строк во встроенном языке чувствительно к регистру. "ООО Ромашка" = "ООО ромашка" даёт Ложь. Это главная причина, по которой поиск контрагента «не находит» существующую запись.

Способы сравнить строки без учёта регистра:

СпособЧто делаетКогда брать
ВРег(С1) = ВРег(С2)приводит обе строки к верхнему региструработает на любой версии 8.3
НРег(С1) = НРег(С2)то же, к нижнему региструаналогично
СтрСравнить(С1, С2) = 0возвращает −1, 0 или 1 без учёта регистра8.3.6 и новее, когда нужен ещё и порядок

Вторая ловушка — пробелы. Строка из реквизита фиксированной длины или из файла может прийти с пробелами по краям. "Ромашка" = "Ромашка " — это Ложь. Перед сравнением чистите строки функцией СокрЛП():

Функция СтрокиРавны(Строка1, Строка2)
    Возврат ВРег(СокрЛП(Строка1)) = ВРег(СокрЛП(Строка2));
КонецФункции

Операторы < и > сравнивают строки посимвольно, в лексикографическом порядке. Отсюда частый сюрприз: "10" < "9" — это Истина, потому что символ «1» меньше символа «9». Номера и коды, которые выглядят как числа, перед сравнением на больше-меньше переводите в число: Число(Код1) < Число(Код2). Если в строке могут быть буквы, Число() упадёт — оберните его в Попытка.

Для проверки, что строка начинается с подстроки или содержит её, есть СтрНачинаетсяС(), СтрЗаканчиваетсяНа() и СтрНайти(). Они точнее, чем вырезка через Лев() с последующим =.

Как сравнить две даты в 1С

Тип Дата в 1С всегда содержит и дату, и время. Поэтому ДатаДокумента = ТекущаяДата() почти никогда не истинно: совпасть должны секунды.

Чтобы сравнить только календарный день, отрежьте время:

Если НачалоДня(Дата1) = НачалоДня(Дата2) Тогда
    // один и тот же день
КонецЕсли;

Аналогично работают НачалоМесяца(), НачалоКвартала(), НачалоГода() — так проверяют, что два документа попали в один период.

Разность двух дат — число секунд. Отсюда разница в днях:

РазницаСекунд = Дата2 - Дата1;
РазницаДней = (НачалоДня(Дата2) - НачалоДня(Дата1)) / 86400;

Деление на 86 400 (24 × 60 × 60) даёт целое число дней только после НачалоДня() у обеих дат. Без этого 15 марта 23:00 и 16 марта 01:00 дадут 0,083 дня вместо одного.

Пустая дата — это 1 января 0001 года, 00:00:00. Сравнивают с ней так: Дата = '00010101' или Дата = Дата(1, 1, 1). Короче всего НЕ ЗначениеЗаполнено(Дата).

ТекущаяДата() возвращает время компьютера, на котором выполняется код. На клиенте и на сервере оно может различаться. Для сравнения с датами документов в типовых конфигурациях на БСП берут ТекущаяДатаСеанса() — она учитывает часовой пояс сеанса.

Неопределено, Null и пустые значения

Эти три вещи путают чаще всего, хотя ведут они себя по-разному:

  • 🔸 Неопределено — значение «ничего не задано». Его возвращают пустая переменная, Найти() без результата, Свойство() для отсутствующего ключа.
  • 🔸 Null — значение из базы данных. Появляется в результате запроса, когда в левом соединении нет пары, или при обращении через точку к реквизиту пустой ссылки составного типа.
  • 🔸 Пустое значение типа — 0, "", пустая дата, пустая ссылка. Это не Null и не Неопределено.

Во встроенном языке Null = Null и Неопределено = Неопределено — это Истина. Но Null = Неопределено — Ложь, и 0 = Null — тоже Ложь. А вот Null > 0 приведёт к ошибке, потому что Null не примитивный тип из списка Число/Строка/Дата/Булево.

// Сумма из запроса может прийти как Null
Сумма = ?(Выборка.Сумма = Null, 0, Выборка.Сумма);
Если Сумма > 0 Тогда
    // безопасно
КонецЕсли;

Универсальная проверка — ЗначениеЗаполнено(). Для Неопределено, Null, нуля, пустой строки, пустой даты и пустой ссылки она возвращает Ложь. Учтите: для Булево Ложь функция тоже вернёт Ложь, а для числа 0 — «не заполнено». Если ноль — законное значение, проверяйте тип явно.

В запросе Null не равен ничему, даже другому Null. Условие ГДЕ Поле = NULL не отберёт ни одной строки. Правильно писать ГДЕ Поле ЕСТЬ NULL или подменять значение функцией ЕСТЬNULL(Поле, 0).

Сравнение значений в запросе и скорость работы

Сравнение в запросе подчиняется правилам СУБД, а не встроенного языка. Отличия, которые бьют по результату и по скорости:

Регистр и строки неограниченной длины

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

Строку неограниченной длины (например, реквизит «Комментарий») сравнивать в условии нельзя — запрос упадёт с ошибкой. Её приводят к строке фиксированной длины:

ВЫБРАТЬ
    Документ.Ссылка
ИЗ
    Документ.ПоступлениеТоваровУслуг КАК Документ
ГДЕ
    ВЫРАЗИТЬ(Документ.Комментарий КАК СТРОКА(200)) = &ТекстКомментария

Не оборачивайте поле в функцию

Условие ГДЕ НАЧАЛОПЕРИОДА(Документ.Дата, ДЕНЬ) = &Дата заставляет СУБД вычислить функцию для каждой строки таблицы. Индекс по дате при этом не работает . Тот же отбор через диапазон использует индекс:

ГДЕ
    Документ.Дата МЕЖДУ &НачалоДня И &КонецДня

Параметры заполняются из кода: Запрос.УстановитьПараметр("НачалоДня", НачалоДня(Дата)); и Запрос.УстановитьПараметр("КонецДня", КонецДня(Дата));.

Составной тип и проверка типа

Если поле может ссылаться на разные справочники, сравнивайте тип оператором ССЫЛКА:

ГДЕ
    Расчеты.Контрагент ССЫЛКА Справочник.Контрагенты

Обращение через точку к реквизиту поля составного типа порождает соединение с каждой таблицей из состава типа. Если нужен реквизит одного вида, сначала сузьте тип через ВЫРАЗИТЬ(Поле КАК Справочник.Контрагенты).ИНН.

Список вместо цепочки ИЛИ

Пять условий Поле = &А ИЛИ Поле = &Б ИЛИ … заменяются одним Поле В (&Список), где параметр — массив. Запрос короче, а СУБД проще построить план.

Сравнение в цикле: ищите по соответствию

Вложенный цикл «для каждой строки первой таблицы перебрать вторую» на 10 000 × 10 000 строк — это сто миллионов сравнений. Быстрее собрать Соответствие по ключу и проверять наличие через Получить():

Найденные = Новый Соответствие;
Для Каждого Строка Из Таблица2 Цикл
    Найденные.Вставить(Строка.Номенклатура, Истина);
КонецЦикла;

Для Каждого Строка Из Таблица1 Цикл
    Если Найденные.Получить(Строка.Номенклатура) = Неопределено Тогда
        // такой номенклатуры во второй таблице нет
    КонецЕсли;
КонецЦикла;

Для таблицы значений есть ещё вариант — добавить индекс (Таблица.Индексы.Добавить("Номенклатура")) и искать методом НайтиСтроки() со структурой отбора.

Проверьте себя
Чему равно выражение "10" < "9" во встроенном языке?
Что вернёт условие ГДЕ Поле = NULL в запросе?
Два объекта одного документа получены через ПолучитьОбъект(). Чему равно Объект1 = Объект2?
Чему равно выражение 5 = "5"?

Частые вопросы

Как сравнить значения произвольного типа на больше-меньше, например при сортировке?
Используйте объект СравнениеЗначений: его метод Сравнить(Значение1, Значение2) возвращает −1, 0 или 1 и не падает на разных типах. Этот же объект передают в сортировку таблицы значений.
Чем отличается сравнение в отборе списка от сравнения в коде?
В отборах и в СКД вид сравнения задаётся системным перечислением ВидСравнения: Равно, ВСписке, Содержит, ВИерархии и другими. Отбор выполняется запросом к базе, поэтому действуют правила запроса — в том числе нечувствительность к регистру.
Почему сравнение двух ссылок на клиенте работает, а реквизит ссылки недоступен?
Сравнить ссылки на клиенте можно — это операция над идентификаторами. А обращение через точку к реквизиту ссылки требует чтения базы, на клиенте в управляемом приложении оно недоступно. Нужное значение получайте на сервере.
Как сравнить значения типа ХранилищеЗначения?
Напрямую оператор = содержимое не сравнит. Получите данные методом Получить() и сравнивайте уже их. Для картинок и файлов сравнивают двоичные данные или их хеш.