Как проверить наличие свойства в соответствии в 1С?

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

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

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

Почему возникает сложность при работе с Соответствием?

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

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

Представим стандартную ситуацию. Нам нужно получить значение по ключу. Мы пишем следующий код:

Значение = МоеСоответствие.Получить(ИскомыйКлюч);

Или используем более лаконичное обращение через квадратные скобки:

Значение = МоеСоответствие[ИскомыйКлюч];

В обоих случаях, если ИскомыйКлюч отсутствует в коллекции, платформа «1С:Предприятие» не выдаст ошибку времени выполнения. Она просто вернет значение Неопределено. Но что произойдет, если ключ в коллекции присутствует, но мы намеренно записали в него значение Неопределено? Мы получим абсолютно такой же результат! Нам вернется Неопределено, и мы никак не сможем стандартным путем понять: ключа вообще не было в коллекции, или же он там есть, но хранит в себе пустое значение.

Давайте смоделируем эту неоднозначность на простом примере кода:


ПримерСоответствия = Новый Соответствие;
ПримерСоответствия.Вставить("СуществующийКлюч", Неопределено);

// Попытка получить значение существующего ключа
Значение1 = ПримерСоответствия.Получить("СуществующийКлюч"); // Возвращает Неопределено

// Попытка получить значение несуществующего ключа
Значение2 = ПримерСоответствия.Получить("НесуществующийКлюч"); // Тоже возвращает Неопределено

Как мы видим, переменные Значение1 и Значение2 абсолютно идентичны. В реальной разработке это может привести к трудноуловимым логическим ошибкам. Например, когда мы строим кэш запросов к базе данных и хотим отличить ситуацию "данные еще не запрашивались" (ключа нет в соответствии) от ситуации "данные запрашивались, но в базе ничего не найдено" (ключ есть, значение равно Неопределено или Null).

Способ 1. Классический перебор в цикле (подход "в лоб")

Первое решение, которое приходит в голову при обсуждении данной проблемы на форумах, — это написать вспомогательную функцию, которая будет последовательно обходить все элементы коллекции в цикле и сравнивать ключи. Давайте подробно рассмотрим этот вариант, так как он наиболее очевиден.

Код такой функции выглядит следующим образом:


Функция ЕстьКлючВСоответствииЦиклом(Соответствие, ИскомыйКлюч) Экспорт
    Для Каждого Элемент Из Соответствие Цикл
        Если Элемент.Ключ = ИскомыйКлюч Тогда
            Возврат Истина;
        КонецЕсли;
    КонецЦикла;
    Возврат Ложь;
КонецФункции

Давайте проанализируем этот алгоритм по шагам. Мы используем цикл Для каждого, который последовательно извлекает элементы соответствия в виде пар "Ключ-Значение". На каждой итерации мы проверяем свойство Ключ на равенство нашему искомому ключу. Если совпадение найдено, функция немедленно прекращает работу и возвращает Истина. Если же цикл завершился полностью, а совпадений не обнаружено, мы возвращаем Ложь.

Преимущества данного способа:

  1. Простота понимания кода. Логика работы понятна даже начинающему разработчику.
  2. Безопасность для исходного объекта. Соответствие никак не изменяется в процессе проверки.
  3. Универсальность. Метод гарантированно работает на любых версиях платформы 1С v8.

Недостатки данного способа:

Главный и самый критичный минус — это катастрофически низкая производительность на больших объемах данных. В теории алгоритмов сложность такого поиска оценивается как O(N), где N — количество элементов в соответствии. Если в вашей коллекции содержатся десятки тысяч элементов, вызов такой функции в цикле приведет к заметному зависанию системы. Вся прелесть хэш-таблиц (которыми под капотом и являются соответствия) с их мгновенным доступом к данным по ключу за O(1) полностью нивелируется полным перебором.

Способ 2. Временная вставка и замер количества элементов (быстрое решение O(1))

А теперь давайте перейдем к более изящным инженерным решениям, которые работают практически мгновенно независимо от того, сколько элементов находится в вашей коллекции — десять штук или миллион. Первый такой метод основан на базовом свойстве поведения хэш-таблиц.

Когда мы вызываем метод Вставить(Ключ, Значение) для соответствия, платформа ведет себя следующим образом:

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

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

  1. Сначала делаем "быструю" проверку: если значение по ключу не равно Неопределено, значит, ключ точно существует в коллекции. Мы сразу возвращаем Истина и завершаем функцию. Это экономит ресурсы в большинстве стандартных сценариев.
  2. Если же там вернулось Неопределено, мы запоминаем текущее количество элементов в соответствии с помощью метода Количество().
  3. Затем мы принудительно вставляем наш ключ в соответствие с пустым значением Неопределено.
  4. Снова измеряем количество элементов и сравниваем его со значением, которое мы сохранили до вставки.
  5. Если количество изменилось (увеличилось на 1) — значит, ключа в соответствии раньше не было. Мы аккуратно удаляем только что добавленный элемент методом Удалить(Ключ), чтобы вернуть коллекцию в первозданный вид, и возвращаем Ложь.
  6. Если количество осталось прежним — значит, ключ существовал в коллекции со значением Неопределено. Мы возвращаем Истина. При этом нам даже не нужно восстанавливать состояние, ведь значение Неопределено просто перезаписало само себя.

Давайте посмотрим на программную реализацию этого алгоритма:


Функция ЕстьКлючВСоответствииБыстро(Соответствие, Ключ) Экспорт
    // Быстрый выход: если значение заполнено, ключ точно есть
    Если Соответствие[Ключ] <> Неопределено Тогда
        Возврат Истина;
    КонецЕсли;
    
    // Замеряем размер коллекции до вставки
    РазмерДо = Соответствие.Количество();
    
    // Пытаемся вставить ключ (если он был, размер не изменится)
    Соответствие.Вставить(Ключ, Неопределено);
    
    РазмерПосле = Соответствие.Количество();
    
    Если РазмерДо <> РазмерПосле Тогда
        // Ключа не было, мы его добавили. Нужно удалить временную запись!
        Соответствие.Удалить(Ключ);
        Возврат Ложь;
    КонецЕсли;
    
    // Ключ был в коллекции со значением Неопределено
    Возврат Истина;
КонецФункции

Преимущества данного способа:

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

Недостатки данного способа:

Этот метод модифицирует исходную коллекцию. Хотя мы сразу же удаляем временно добавленный элемент, в многопоточных сценариях или при наличии каких-то внешних обработчиков, следящих за изменением коллекции, это теоретически может вызвать нежелательные побочные эффекты. Также этот способ неприменим, если переданное нам соответствие защищено от записи (например, если это фиксированный объект).

Способ 3. Использование "ФиксированногоСоответствия" (безопасное решение O(1))

Существует еще один очень интересный и крайне изящный способ, который позволяет обойти ограничение платформы без изменения исходной коллекции. Этот способ базируется на поведении специального объекта платформы — ФиксированноеСоответствие.

Давайте проанализируем важный нюанс работы с объектом ФиксированноеСоответствие. Если мы обращаемся к нему по несуществующему ключу с помощью метода Получить(Ключ), система ведет себя стандартно и возвращает Неопределено. Но если мы попытаемся обратиться к несуществующему ключу напрямую через квадратные скобки ФиксСоотв[Ключ], платформа неожиданно вызовет исключение (выдаст ошибку выполнения)! При этом, если ключ существует (даже если в нем записано Неопределено), обращение через квадратные скобки отработает успешно и вернет значение без каких-либо ошибок.

Используя эту недокументированную особенность поведения платформы, мы можем построить максимально надежный и быстрый алгоритм с использованием блока Попытка — Исключение.

Давайте пошагово разберем, как это реализовать на практике:

  1. Создаем на основе нашего исходного соответствия новый объект типа ФиксированноеСоответствие. Это делается моментально одной строкой кода.
  2. В блоке Попытка обращаемся к созданному фиксированному соответствию через квадратные скобки по нашему ключу.
  3. Если обращение прошло успешно — значит, ключ гарантированно присутствует в коллекции. Возвращаем Истина.
  4. Если платформа выбросила исключение — управление переходит в блок Исключение. Это однозначно сигнализирует о том, что ключа в коллекции не было. Мы перехватываем ошибку и возвращаем Ложь.

Посмотрим на пример реализации данного метода:


Функция ЕстьКлючВСоответствииБезИзменения(Соответствие, Ключ) Экспорт
    // Быстрый выход для заполненных значений
    Если Соответствие[Ключ] <> Неопределено Тогда
        Возврат Истина;
    КонецЕсли;
    
    // Создаем фиксированную копию соответствия
    ФиксСоответствие = Новый ФиксированноеСоответствие(Соответствие);
    
    Попытка
        // Обращение через [] к несуществующему ключу у фиксированного соответствия вызывает ошибку
        ВременноеЗначение = ФиксСоответствие[Ключ];
        Возврат Истина;
    Исключение
        // Ошибка перехвачена, значит ключа не существовало
        Возврат Ложь;
    КонецПопытки;
КонецФункции

Преимущества данного способа:

  1. Полная безопасность. Исходный объект Соответствие никак не изменяется, не добавляются и не удаляются элементы.
  2. Высокая скорость работы O(1). Создание фиксированного соответствия из обычного выполняется платформой на C++ чрезвычайно быстро.
  3. Подходит для работы с коллекциями, переданными "только для чтения".

Недостатки данного способа:

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

Какое решение выбрать в реальном проекте?

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

  1. Используйте Способ 2 (Временная вставка), если вам критически важна максимальная скорость работы алгоритма (например, при расчете себестоимости, проведении тяжелых документов или массовом импорте данных), и вы точно уверены, что исходное соответствие можно временно модифицировать.
  2. Используйте Способ 3 (ФиксированноеСоответствие), если вы пишете универсальную библиотечную функцию (например, для общего модуля общего назначения), куда могут передаваться различные объекты, включая защищенные от записи, или если вы хотите гарантированно избежать рисков изменения исходной коллекции.
  3. Избегайте Способа 1 (Цикл) на любых промышленных базах данных, за исключением учебных задач или коллекций, размер которых заведомо никогда не превысит 5-10 элементов.

Взгляд в будущее: Как это решено в технологии «1С:Элемент»

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

Создатели новой платформы учли многолетний опыт сообщества разработчиков. В новом типе данных Соответствие в «1С:Элемент» был добавлен встроенный метод ВставитьЕслиОтсутствует(Ключ, Значение). Этот метод позволяет решить проблему проверки на уровне самой платформы, работая максимально быстро и безопасно без написания вспомогательных конструкций. Будем надеяться, что подобные удобные механизмы со временем появятся и в классической платформе «1С:Предприятие 8»!

Надеемся, что этот подробный разбор поможет вам сделать ваш код более быстрым, надежным и профессиональным. До новых встреч на страницах нашего ресурса!

← На главную