При разработке в среде 1С:Предприятие одной из самых распространенных задач является извлечение данных из регистров сведений. Однако начинающие программисты часто сталкиваются с неочевидным поведением платформы, пытаясь применить методы периодических регистров к непериодическим. Это приводит к досадным ошибкам на этапе выполнения кода, наиболее известная из которых — «Значение не является значением объектного типа».
В рамках этой статьи мы подробно разберем, почему возникает эта ошибка, проанализируем внутренние механизмы работы платформы 1С с различными типами регистров сведений, а также изучим несколько надежных и эффективных способов решения этой проблемы.
Рассмотрим ситуацию, с которой столкнулся автор темы. В общем модуле была объявлена экспортная функция, предназначенная для получения кода печати ярлыка по конкретной номенклатуре:
Функция КодПечати(Номенклатура, ЭлементНоменклатуры) Экспорт
Отбор = новый Структура ("Номенклатура", ЭлементНоменклатуры);
ЗначениеРесурсов = РегистрыСведений.КодПечатиЯрлыка.ПолучитьПоследнее(Номенклатура, Отбор);
Возврат ЗначениеРесурсов.КодПечати;
КонецФункции
При вызове этой функции система экстренно завершает работу с ошибкой:
{ОбщийМодуль.РаботаСоСправочниками.Модуль(5)}: Значение не является значением объектного типа (КодПечати)
Выясним причину такого поведения. Как указано в описании структуры метаданных, регистр сведений КодПечатиЯрлыка является непериодическим. Он содержит единственное измерение Номенклатура и ресурс КодПечати.
Основная ошибка кроется в вызове метода ПолучитьПоследнее(). Согласно официальной справке по встроенному языку 1С, данный метод предназначен исключительно для периодических регистров сведений. Когда мы вызываем этот метод у менеджера непериодического регистра, платформа ведет себя следующим образом:
Неопределено вместо ожидаемой структуры с ресурсами.КодПечати у переменной ЗначениеРесурсов, которая на данный момент равна Неопределено. Поскольку у примитивного типа Неопределено нет и не может быть никаких свойств, платформа генерирует исключение «Значение не является значением объектного типа».Попытка автора исправить ситуацию в Сообщении 5 путем передачи даты в качестве первого параметра (ПолучитьПоследнее(АктуальнаяДата, Отбор)) закономерно привела к другой ошибке: Несоответствие типов (Параметр номер "1"). Это произошло по той же причине — менеджер непериодического регистра не ожидает на вход параметры, связанные со временем.
Ниже мы подробно разберем три различных и корректных способа решения этой задачи в зависимости от архитектурных требований вашего приложения.
---
Если нам необходимо получить данные из непериодического регистра сведений в одну строку кода без написания тяжелых запросов, в платформе 1С предусмотрен специализированный метод Получить(). В отличие от своего «периодического» собрата ПолучитьПоследнее(), этот метод разработан специально для работы с непериодическими регистрами (а также для получения записей на конкретную дату без среза).
Давайте перепишем исходную функцию автора, используя этот метод:
Функция КодПечати(ЭлементНоменклатуры) Экспорт
// Формируем структуру отбора по ключевым измерениям регистра
Отбор = Новый Структура("Номенклатура", ЭлементНоменклатуры);
// Получаем структуру ресурсов напрямую через менеджер регистра сведений
ЗначениеРесурсов = РегистрыСведений.КодПечатиЯрлыка.Получить(Отбор);
// Возвращаем полученное значение ресурса
Возврат ЗначениеРесурсов.КодПечати;
КонецФункции
Этот способ обладает важными преимуществами:
Получить() не вернет Неопределено. Платформа вернет полноценную структуру, где все ключи будут соответствовать именам ресурсов регистра, а значения будут заполнены дефолтными значениями типов (например, пустая строка для строк, 0 для чисел, Ложь для Булево). Таким образом, обращение к ЗначениеРесурсов.КодПечати никогда не вызовет ошибку «Значение не является значением объектного типа».Важное предупреждение при использовании метода Получить(): инструмент имеет жесткое ограничение платформы. В структуре отбора обязательно должны быть указаны значения для всех измерений регистра сведений. Если в будущем вы решите доработать регистр и добавите туда, например, измерение Характеристика или Организация, данный код выбросит ошибку времени выполнения. Помните об этом при проектировании масштабируемых систем!
---
Как совершенно справедливо отметили участники обсуждения в Сообщениях 6, 8 и 9, использование запросов — это самый прозрачный и надежный метод получения информации в 1С. Запросы не зависят от количества измерений в регистре (мы можем делать отбор по любому подмножеству измерений) и легко поддаются оптимизации.
Посмотрим на корректный текст запроса для непериодического регистра сведений:
Функция КодПечати(ЭлементНоменклатуры) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КодПечатиЯрлыка.КодПечати КАК КодПечати
|ИЗ
| РегистрСведений.КодПечатиЯрлыка КАК КодПечатиЯрлыка
|ГДЕ
| КодПечатиЯрлыка.Номенклатура = &Номенклатура";
Запрос.УстановитьПараметр("Номенклатура", ЭлементНоменклатуры);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.КодПечати;
Иначе
// Если запись в регистре отсутствует, возвращаем пустое значение или Неопределено
Возврат "";
КонецЕсли;
КонецФункции
Обратите внимание на критическую деталь, упомянутую в Сообщении 9. В Сообщении 8 разработчик ошибочно попытался построить запрос к виртуальной таблице СрезПоследних для непериодического регистра. У непериодических регистров сведений нет виртуальных таблиц срезов. Запрос нужно делать непосредственно к физической таблице регистра (РегистрСведений.КодПечатиЯрлыка).
---
Иногда перед разработчиком стоит задача не просто получить дефолтное пустое значение (как это делает метод Получить()), а четко определить: существует ли физически запись в базе данных?
Для решения этой задачи идеально подходит объектный метод работы с данными через МенеджерЗаписи. Рассмотрим пример реализации:
Функция КодПечати(ЭлементНоменклатуры) Экспорт
// Создаем пустой менеджер записи для нашего регистра
МенеджерЗаписи = РегистрыСведений.КодПечатиЯрлыка.СоздатьМенеджерЗаписи();
// Заполняем ключевые поля (измерения) для поиска
МенеджерЗаписи.Номенклатура = ЭлементНоменклатуры;
// Выполняем чтение записи из базы данных
МенеджерЗаписи.Прочитать();
// Проверяем, удалось ли прочитать существующую запись
Если МенеджерЗаписи.Выбран() Тогда
Возврат МенеджерЗаписи.КодПечати;
Иначе
// Записи с такой номенклатурой физически нет в базе данных
Возврат Неопределено;
КонецЕсли;
КонецФункции
Метод Выбран() возвращает Истина только в том случае, если запись действительно была найдена в СУБД. Это позволяет гибко ветвить логику программы в зависимости от наличия настроек в регистре.
---
Выбор конкретного метода получения данных во многом зависит от контекста вызова нашей функции КодПечати. Проанализируем два сценария использования этой функции:
Получить() или МенеджерЗаписи полностью оправдано. Код отрабатывает мгновенно, а разработка занимает минимум времени.Каждое обращение к методам Получить() или Прочитать() внутри цикла генерирует отдельный одиночный запрос к системе управления базами данных (СУБД). Если в вашем документе 1000 строк, сервер 1С выполнит 1000 мелких транзакций к СУБД, что спровоцирует колоссальные задержки и зависание интерфейса у пользователей.
Как правильно поступать в таких случаях? Мы должны отказаться от поочередного вызова функции для каждого элемента. Вместо этого следует передать весь список (массив) номенклатуры в один единственный запрос, выбрать все соответствующие коды печати пакетным запросом, выгрузить результат во временную таблицу или в объект Соответствие, а затем быстро распределить полученные значения по строкам в оперативной памяти.
Пример оптимизированного кода для обработки массива номенклатуры:
Функция ПолучитьКодыПечатиДляСписка(МассивНоменклатуры) Экспорт
РезультатСоответствие = Новый Соответствие;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КодПечатиЯрлыка.Номенклатура КАК Номенклатура,
| КодПечатиЯрлыка.КодПечати КАК КодПечати
|ИЗ
| РегистрСведений.КодПечатиЯрлыка КАК КодПечатиЯрлыка
|ГДЕ
| КодПечатиЯрлыка.Номенклатура В (&МассивНоменклатуры)";
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
РезультатСоответствие.Вставить(Выборка.Номенклатура, Выборка.КодПечати);
КонецЦикла;
// Возвращаем коллекцию "Соответствие" для быстрого поиска в памяти без обращений к БД
Возврат РезультатСоответствие;
КонецФункции
Применяя этот подход, мы выполняем ровно один запрос к базе данных вместо сотен и тысяч, гарантируя максимальную отзывчивость информационной системы даже под высокими нагрузками.