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