Как исправить ошибку «Значение не является значением объектного типа» при работе с датами в 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Работа с датами — одна из самых частых задач при разработке отчетов, обработок и механизмов интеграции в платформе «1С:Предприятие 8». На первый взгляд, сравнение двух дат кажется тривиальным действием. Однако начинающие разработчики часто сталкиваются с коварными ошибками, которые приводят к аварийному завершению работы программы. Самая популярная из них — «Значение не является значением объектного типа».

В этой статье мы подробно разберем, почему возникает эта ошибка, как правильно сравнивать даты в системе 1С, избегая неявного преобразования типов, почему нельзя называть переменные системными именами и как сделать ваш код безопасным и быстрым с помощью механизмов Библиотеки стандартных подсистем (БСП) и оптимизации запросов.

Анализ исходной проблемы: почему падает код?

Рассмотрим ситуацию, с которой столкнулся автор темы на форуме. В цикле обхода данных выполняется следующий код:


Дата = Дата(2019, 12, 31);
ДатаД1 = ДобавитьМесяц(Стр.Документ.Дата, 36);

Если ДатаД1 < Дата Тогда
    // Какое-то действие
КонецЕсли;

При выполнении этого фрагмента платформа возвращает ошибку: Значение не является значением объектного типа (Дата). Давайте выясним ее истинную причину. Ошибка указывает на то, что программа пытается обратиться через точку к свойству Дата у объекта, который на самом деле объектом не является.

В выражении Стр.Документ.Дата мы обращаемся к реквизиту Дата через объект Стр.Документ. Ошибка возникает тогда, когда в переменной Стр.Документ находится не ссылка на документ, а нечто иное:

Важный нюанс платформы 1С: Если бы в переменной Стр.Документ находилась обычная пустая ссылка (например, Документы.РеализацияТоваровУслуг.ПустаяСсылка()), то обращение через точку не вызвало бы ошибку. Платформа 1С отработала бы корректно и просто вернула пустую дату по умолчанию — 01.01.0001 0:00:00. Но в случае с NULL или Неопределено система немедленно прекращает выполнение кода с критической ошибкой.

Проблема «трех точек» и производительность системы

Даже если мы гарантируем, что переменная Стр.Документ всегда заполнена, обращение вида Стр.Документ.Дата внутри цикла является грубейшей архитектурной ошибкой. В кругах разработчиков 1С это называют «проблемой трех точек» (или неявным разыменованием ссылок).

Когда вы пишете Стр.Документ.Дата в цикле, платформа 1С:Предприятие вынуждена на каждой итерации совершать отдельный запрос к базе данных. При этом СУБД считывает в память сервера весь объект целиком — со всеми его многочисленными реквизитами, табличными частями и реквизитами адресации, хотя вам нужна всего лишь одна дата. Если в вашей таблице будет 10 000 строк, сервер выполнит 10 000 тяжелых SQL-запросов, что приведет к катастрофическому падению производительности.

Решение 1. Безопасное получение реквизитов через БСП

Для решения проблемы производительности и одновременного исключения ошибки объектного типа в любой современной конфигурации на базе БСП применяется специализированный инструмент. Мы можем использовать функцию общего модуля ОбщегоНазначения.ЗначениеРеквизитаОбъекта().

Давайте посмотрим, как будет выглядеть безопасный код с применением этого метода:


// Безопасно получаем только дату документа, не считывая сам документ целиком
ДатаДокумента = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Стр.Документ, "Дата");

// Проверяем, удалось ли получить дату (если Стр.Документ был NULL или Неопределено, вернется Неопределено)
Если ЗначениеЗаполнено(ДатаДокумента) Тогда
    ДатаД1 = ДобавитьМесяц(ДатаДокумента, 36);
Иначе
    // Обрабатываем ситуацию, когда документ или его дата отсутствуют
    ДатаД1 = '00010101'; 
КонецЕсли;

В чем преимущества данного подхода?

  1. Отказоустойчивость: Если в Стр.Документ передано значение Неопределено, NULL или битая ссылка, функция не вызовет аварийного завершения работы программы. Она вернет значение Неопределено, которое мы легко можем обработать в коде.
  2. Высокая скорость работы: Платформа сгенерирует быстрый и легкий SQL-запрос, который выберет из базы данных исключительно поле даты для конкретной ссылки, минимизируя нагрузку на сервер СУБД и сеть.

Решение 2. Обработка пустых значений на уровне запроса

Если данные для нашего отчета или обработки выбираются с помощью запроса (а это лучший стандарт разработки в 1С), то решать проблему с пустыми значениями и отсутствующими датами правильнее всего непосредственно на уровне СУБД.

Рассмотрим пример построения правильного запроса. Мы можем использовать конструкцию ЕСТЬNULL, чтобы гарантировать получение даты даже при левых соединениях, а также отсечь незаполненные документы на этапе выборки:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Продажи.Документ КАК Документ,
    |	ЕСТЬNULL(Продажи.Документ.Дата, ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаДокумента
    |ИЗ
    |	РегистрНакопления.Продажи КАК Продажи
    |ГДЕ
    |	Продажи.Документ <> ЗНАЧЕНИЕ(Документ.РеализацияТоваровУслуг.ПустаяСсылка)
    |	И Продажи.Документ НЕ ЕСТЬ NULL";

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // Здесь мы гарантированно получаем заполненную дату без риска получить ошибку объектного типа
    ДатаД1 = ДобавитьМесяц(Выборка.ДатаДокумента, 36);
КонецЦикла;

Обрабатывая данные таким образом, мы полностью исключаем попадание неопределенных значений в наш цикл обхода.

Опасность строкового сравнения дат и функция «Формат»

Вернемся к первому сообщению автора темы, где он пытался выполнить сравнение следующим образом:


Дата = Формат(Дата(2019, 12, 31), "ДФ=dd.MM.yyyy"); 
ДатаД1 = Формат(ДобавитьМесяц(Стр.Документ.Дата, 36), "ДФ=dd.MM.yyyy"); 

Если ДатаД1 < Дата Тогда
    // Не работает!
КонецЕсли;

Давайте разберем, почему этот код ошибочен с точки зрения логики сравнения. Функция Формат() всегда возвращает текстовое представление переданного в нее значения. То есть в переменные попадают обычные строки вида "31.12.2019" и "16.12.2019".

Когда платформа выполняет сравнение строк с помощью операторов сравнения (<, >, <=, >=), она производит посимвольное (лексикографическое) сопоставление слева направо по таблице кодов символов:

  1. Первый символ первой строки сравнивается с первым символом второй строки.
  2. Если они равны, сравниваются вторые символы, и так далее.

Давайте попробуем сравнить две строки: "31.12.2019" и "01.01.2020". Хронологически первая дата меньше второй. Но при строковом сравнении система сопоставит первый символ "3" с символом "0". Поскольку код символа "3" больше кода символа "0", система вернет результат Ложь (то есть строка "31.12.2019" распознается системой как "большая", чем "01.01.2020").

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

Интересный факт: если вам все же крайне необходимо сравнить даты в строковом формате, строку нужно приводить к международному формату ISO (год-месяц-день): "yyyyMMdd" (например, "20191231" и "20200101"). В таком формате лексикографическое сравнение строк будет совпадать с хронологическим, однако делать так в коде 1С без крайней необходимости (например, при специфических обменах данными) не рекомендуется.

Конфликт имен и перекрытие глобального контекста

Обратим внимание на еще одну важную деталь в коде автора:

Дата = Дата(2019, 12, 31);

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

После объявления такой переменной внутри этой же процедуры или функции вы больше не сможете воспользоваться системным конструктором. Если ниже по коду вы попытаетесь написать ДатаСлед = Дата(2020, 1, 1);, платформа выдаст ошибку времени выполнения, поскольку попытается вызвать ваше локальное значение (которое уже является конкретным значением типа Дата) как функцию. Это недопустимо.

Всегда называйте переменные понятными именами, которые не пересекаются с ключевыми словами и функциями платформы: ДатаОтсечки, ГраничнаяДата, ДатаСравнения, ТекущийПериод.

Итоговый рефакторинг: пишем качественный и безопасный код

С учетом всех разобранных выше правил и механизмов платформы, давайте напишем правильный вариант обработки и сравнения дат. Мы избавимся от конфликтов имен, исключим неявное считывание объектов из базы данных, обезопасим программу от падения при пустых значениях и будем сравнивать строго типизированные даты.

Рассмотрим готовый к использованию шаблон кода:


// Определяем граничную дату. Используем понятное имя переменной
ДатаСравнения = Дата(2019, 12, 31); 

// Безопасное получение даты документа через общий модуль БСП
ДатаДокумента = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Стр.Документ, "Дата");

// Проверяем, что дата успешно получена и заполнена
Если ЗначениеЗаполнено(ДатаДокумента) Тогда
    
    // Прибавляем к исходной дате 36 месяцев (3 года)
    ДатаД1 = ДобавитьМесяц(ДатаДокумента, 36);
    
    // Сравниваем переменные в исходном типе "Дата"
    Если ДатаД1 < ДатаСравнения Тогда
        // Выводим информацию пользователю, только здесь используя форматирование в строку
        ТекстСообщения = СтрШаблон("Документ от %1 устарел более чем на 3 года относительно даты отсечки.", 
            Формат(ДатаДокумента, "ДФ=dd.MM.yyyy"));
        ОбщегоНазначенияКлиентСервер.СообщитьПользователю(ТекстСообщения);
    КонецЕсли;
    
Иначе
    // Действия на случай, если документ не заполнен или удален
    ВызватьИсключение "В обрабатываемой строке отсутствует ссылка на корректный документ!";
КонецЕсли;

Как быстро локализовать подобные ошибки с помощью отладки

Чтобы оперативно находить строки кода, вызывающие сбои в циклах, настоятельно рекомендуем отказаться от использования метода Сообщить() в качестве инструмента отладки. Вместо этого освойте штатные инструменты конфигуратора:

  1. Остановка по ошибке: В меню конфигуратора выберите пункт Отладка -> Настройка отладки. На вкладке «Автоматическая остановка» установите флажок напротив пункта «Останавливаться по ошибке». При возникновении сбоя платформа сама остановит выполнение кода прямо на той строке, где значение переменной оказалось Неопределено или NULL.
  2. Табло (Ctrl + Alt + W): Добавьте проверяемые переменные (например, Стр.Документ) в окно Табло во время отладки. Вы сможете в реальном времени отслеживать тип значения и его содержимое на каждой итерации цикла.
  3. Вычислить выражение (Shift + F9): Выделите интересующий вас фрагмент кода (например, Стр.Документ.Дата) и нажмите горячие клавиши. Вы сможете детально проанализировать структуру объекта, проверить типы его свойств и даже выполнить произвольные проверочные функции «на лету», не перезапуская сеанс отладки.

Используя безопасные методы работы с реквизитами и грамотно подходя к типизации данных, вы сможете писать производительный, надежный и легко поддерживаемый код на платформе 1С:Предприятие 8!

← На главную