Приветствуем вас, уважаемые коллеги! Сегодня мы детально разберем одну из весьма специфических, но в то же время классических проблем, с которой рано или поздно сталкивается практически каждый разработчик на платформе «1С:Предприятие 8». Речь пойдет о коллекции Соответствие и о том, как достоверно определить, присутствует ли в ней конкретный ключ, если значением этого ключа может являться Неопределено.
На первый взгляд задача кажется тривиальной. Однако те, кто давно работает с платформой, знают, что у объекта Соответствие отсутствует удобный встроенный метод вроде Свойство(), который есть у объекта Структура. Давайте вместе проанализируем эту проблему, выясним причины ее возникновения и рассмотрим практические способы ее решения, включая высокоэффективные алгоритмы со сложностью выполнения O(1).
Для начала давайте вспомним базовое поведение платформы. В 1С есть две основные универсальные коллекции для хранения пар "ключ-значение": это Структура и Соответствие. У них есть важное различие. Ключом структуры может быть только строго типизированная строка, соответствующая правилам идентификатора. В то время как ключом соответствия может выступать абсолютно любой тип данных — от простых чисел и строк до ссылок на элементы справочников и сложных объектов базы данных.
Именно из-за этой гибкости Соответствие крайне популярно при кэшировании данных, сопоставлении различных объектов при интеграциях и обмене данными. Но здесь разработчиков подстерегает коварная архитектурная особенность платформы.
Представим стандартную ситуацию. Нам нужно получить значение по ключу. Мы пишем следующий код:
Значение = МоеСоответствие.Получить(ИскомыйКлюч);
Или используем более лаконичное обращение через квадратные скобки:
Значение = МоеСоответствие[ИскомыйКлюч];
В обоих случаях, если ИскомыйКлюч отсутствует в коллекции, платформа «1С:Предприятие» не выдаст ошибку времени выполнения. Она просто вернет значение Неопределено. Но что произойдет, если ключ в коллекции присутствует, но мы намеренно записали в него значение Неопределено? Мы получим абсолютно такой же результат! Нам вернется Неопределено, и мы никак не сможем стандартным путем понять: ключа вообще не было в коллекции, или же он там есть, но хранит в себе пустое значение.
Давайте смоделируем эту неоднозначность на простом примере кода:
ПримерСоответствия = Новый Соответствие;
ПримерСоответствия.Вставить("СуществующийКлюч", Неопределено);
// Попытка получить значение существующего ключа
Значение1 = ПримерСоответствия.Получить("СуществующийКлюч"); // Возвращает Неопределено
// Попытка получить значение несуществующего ключа
Значение2 = ПримерСоответствия.Получить("НесуществующийКлюч"); // Тоже возвращает Неопределено
Как мы видим, переменные Значение1 и Значение2 абсолютно идентичны. В реальной разработке это может привести к трудноуловимым логическим ошибкам. Например, когда мы строим кэш запросов к базе данных и хотим отличить ситуацию "данные еще не запрашивались" (ключа нет в соответствии) от ситуации "данные запрашивались, но в базе ничего не найдено" (ключ есть, значение равно Неопределено или Null).
Первое решение, которое приходит в голову при обсуждении данной проблемы на форумах, — это написать вспомогательную функцию, которая будет последовательно обходить все элементы коллекции в цикле и сравнивать ключи. Давайте подробно рассмотрим этот вариант, так как он наиболее очевиден.
Код такой функции выглядит следующим образом:
Функция ЕстьКлючВСоответствииЦиклом(Соответствие, ИскомыйКлюч) Экспорт
Для Каждого Элемент Из Соответствие Цикл
Если Элемент.Ключ = ИскомыйКлюч Тогда
Возврат Истина;
КонецЕсли;
КонецЦикла;
Возврат Ложь;
КонецФункции
Давайте проанализируем этот алгоритм по шагам. Мы используем цикл Для каждого, который последовательно извлекает элементы соответствия в виде пар "Ключ-Значение". На каждой итерации мы проверяем свойство Ключ на равенство нашему искомому ключу. Если совпадение найдено, функция немедленно прекращает работу и возвращает Истина. Если же цикл завершился полностью, а совпадений не обнаружено, мы возвращаем Ложь.
Преимущества данного способа:
Недостатки данного способа:
Главный и самый критичный минус — это катастрофически низкая производительность на больших объемах данных. В теории алгоритмов сложность такого поиска оценивается как O(N), где N — количество элементов в соответствии. Если в вашей коллекции содержатся десятки тысяч элементов, вызов такой функции в цикле приведет к заметному зависанию системы. Вся прелесть хэш-таблиц (которыми под капотом и являются соответствия) с их мгновенным доступом к данным по ключу за O(1) полностью нивелируется полным перебором.
А теперь давайте перейдем к более изящным инженерным решениям, которые работают практически мгновенно независимо от того, сколько элементов находится в вашей коллекции — десять штук или миллион. Первый такой метод основан на базовом свойстве поведения хэш-таблиц.
Когда мы вызываем метод Вставить(Ключ, Значение) для соответствия, платформа ведет себя следующим образом:
Мы можем использовать эту особенность для нашей проверки. Давайте разберем алгоритм по шагам:
Неопределено, значит, ключ точно существует в коллекции. Мы сразу возвращаем Истина и завершаем функцию. Это экономит ресурсы в большинстве стандартных сценариев.Неопределено, мы запоминаем текущее количество элементов в соответствии с помощью метода Количество().Неопределено.Удалить(Ключ), чтобы вернуть коллекцию в первозданный вид, и возвращаем Ложь.Неопределено. Мы возвращаем Истина. При этом нам даже не нужно восстанавливать состояние, ведь значение Неопределено просто перезаписало само себя.Давайте посмотрим на программную реализацию этого алгоритма:
Функция ЕстьКлючВСоответствииБыстро(Соответствие, Ключ) Экспорт
// Быстрый выход: если значение заполнено, ключ точно есть
Если Соответствие[Ключ] <> Неопределено Тогда
Возврат Истина;
КонецЕсли;
// Замеряем размер коллекции до вставки
РазмерДо = Соответствие.Количество();
// Пытаемся вставить ключ (если он был, размер не изменится)
Соответствие.Вставить(Ключ, Неопределено);
РазмерПосле = Соответствие.Количество();
Если РазмерДо <> РазмерПосле Тогда
// Ключа не было, мы его добавили. Нужно удалить временную запись!
Соответствие.Удалить(Ключ);
Возврат Ложь;
КонецЕсли;
// Ключ был в коллекции со значением Неопределено
Возврат Истина;
КонецФункции
Преимущества данного способа:
Невероятная скорость работы. Сложность алгоритма составляет O(1). Операции получения размера коллекции, вставки по хэш-ключу и удаления элемента выполняются на уровне платформы мгновенно.
Недостатки данного способа:
Этот метод модифицирует исходную коллекцию. Хотя мы сразу же удаляем временно добавленный элемент, в многопоточных сценариях или при наличии каких-то внешних обработчиков, следящих за изменением коллекции, это теоретически может вызвать нежелательные побочные эффекты. Также этот способ неприменим, если переданное нам соответствие защищено от записи (например, если это фиксированный объект).
Существует еще один очень интересный и крайне изящный способ, который позволяет обойти ограничение платформы без изменения исходной коллекции. Этот способ базируется на поведении специального объекта платформы — ФиксированноеСоответствие.
Давайте проанализируем важный нюанс работы с объектом ФиксированноеСоответствие. Если мы обращаемся к нему по несуществующему ключу с помощью метода Получить(Ключ), система ведет себя стандартно и возвращает Неопределено. Но если мы попытаемся обратиться к несуществующему ключу напрямую через квадратные скобки ФиксСоотв[Ключ], платформа неожиданно вызовет исключение (выдаст ошибку выполнения)! При этом, если ключ существует (даже если в нем записано Неопределено), обращение через квадратные скобки отработает успешно и вернет значение без каких-либо ошибок.
Используя эту недокументированную особенность поведения платформы, мы можем построить максимально надежный и быстрый алгоритм с использованием блока Попытка — Исключение.
Давайте пошагово разберем, как это реализовать на практике:
ФиксированноеСоответствие. Это делается моментально одной строкой кода.Попытка обращаемся к созданному фиксированному соответствию через квадратные скобки по нашему ключу.Истина.Исключение. Это однозначно сигнализирует о том, что ключа в коллекции не было. Мы перехватываем ошибку и возвращаем Ложь.Посмотрим на пример реализации данного метода:
Функция ЕстьКлючВСоответствииБезИзменения(Соответствие, Ключ) Экспорт
// Быстрый выход для заполненных значений
Если Соответствие[Ключ] <> Неопределено Тогда
Возврат Истина;
КонецЕсли;
// Создаем фиксированную копию соответствия
ФиксСоответствие = Новый ФиксированноеСоответствие(Соответствие);
Попытка
// Обращение через [] к несуществующему ключу у фиксированного соответствия вызывает ошибку
ВременноеЗначение = ФиксСоответствие[Ключ];
Возврат Истина;
Исключение
// Ошибка перехвачена, значит ключа не существовало
Возврат Ложь;
КонецПопытки;
КонецФункции
Преимущества данного способа:
Соответствие никак не изменяется, не добавляются и не удаляются элементы.Недостатки данного способа:
Использование конструкции Попытка — Исключение в 1С имеет небольшие накладные расходы по производительности в тот момент, когда происходит реальный переход в ветку Исключение. Если ваша логика построена так, что в большинстве случаев ключа действительно не будет в коллекции, постоянное возникновение внутренних ошибок платформы может незначительно снизить общую скорость работы при массовой обработке данных (миллионы итераций). Тем не менее, это все равно будет в разы быстрее, чем полный перебор коллекции в цикле.
Мы подробно проанализировали три кардинально отличающихся подхода. Давайте систематизируем полученные знания, чтобы вы могли легко принять решение при проектировании вашей системы:
Платформа «1С:Предприятие 8» развивается уже много лет, и некоторые архитектурные решения "тянут" за собой обратную совместимость, из-за чего разработчики вынуждены применять подобные обходные приемы. Однако фирма 1С активно разрабатывает технологию нового поколения — «1С:Элемент».
Создатели новой платформы учли многолетний опыт сообщества разработчиков. В новом типе данных Соответствие в «1С:Элемент» был добавлен встроенный метод ВставитьЕслиОтсутствует(Ключ, Значение). Этот метод позволяет решить проблему проверки на уровне самой платформы, работая максимально быстро и безопасно без написания вспомогательных конструкций. Будем надеяться, что подобные удобные механизмы со временем появятся и в классической платформе «1С:Предприятие 8»!
Надеемся, что этот подробный разбор поможет вам сделать ваш код более быстрым, надежным и профессиональным. До новых встреч на страницах нашего ресурса!