Сравнение значений во встроенном языке и запросах 1С 8.3: где ловушки
Содержание
Два значения в 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 Цикл
Если Найденные.Получить(Строка.Номенклатура) = Неопределено Тогда
// такой номенклатуры во второй таблице нет
КонецЕсли;
КонецЦикла;
Для таблицы значений есть ещё вариант — добавить индекс (Таблица.Индексы.Добавить("Номенклатура")) и искать методом НайтиСтроки() со структурой отбора.
Частые вопросы
Как сравнить значения произвольного типа на больше-меньше, например при сортировке?
СравнениеЗначений: его метод Сравнить(Значение1, Значение2) возвращает −1, 0 или 1 и не падает на разных типах. Этот же объект передают в сортировку таблицы значений.Чем отличается сравнение в отборе списка от сравнения в коде?
ВидСравнения: Равно, ВСписке, Содержит, ВИерархии и другими. Отбор выполняется запросом к базе, поэтому действуют правила запроса — в том числе нечувствительность к регистру.Почему сравнение двух ссылок на клиенте работает, а реквизит ссылки недоступен?
Как сравнить значения типа ХранилищеЗначения?
= содержимое не сравнит. Получите данные методом Получить() и сравнивайте уже их. Для картинок и файлов сравнивают двоичные данные или их хеш.