Как организовать поиск в массиве структур на клиенте в 1С?

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

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

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

Особенности работы метода Найти() в массивах

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

Когда мы вызываем метод Массив.Найти(ИскомоеЗначение), платформа выполняет поиск только по элементам первого уровня. Это означает, что переданное значение сравнивается непосредственно с элементами массива. Если элемент массива — это объект Структура, то метод будет искать точное совпадение с ссылкой на эту структуру, а не проверять поля внутри нее. Встроенного функционала для глубокого поиска по ключам структур в объекте Массив нет.

Посмотрим на типовую функцию передачи таблицы значений на клиент из типовых конфигураций:


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

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

Вариант 1. Линейный перебор элементов массива в цикле

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

Если коллекция небольшая, поиск перебором на клиенте является абсолютно рабочим и простым решением:


ИскомыйОстаток = 0;
Для Каждого Стр Из МассивДанных Цикл
    Если Стр.Номенклатура = ТекущаяНоменклатура Тогда
        ИскомыйОстаток = Стр.Остаток;
        Прервать; // Прерываем цикл, как только нашли соответствие
    КонецЕсли;
КонецЦикла;

Использование оператора Прервать завершает цикл сразу после первого совпадения, сокращая время выполнения при поиске уникальных значений.

Вариант 2. Использование объекта Соответствие вместо массива

Рассмотрим ситуацию, когда требуется найти остаток или цену по ключевому полю (например, по ссылке на номенклатуру) среди сотен или тысяч позиций. Вложенный цикл в таком случае приведет к квадратичной сложности O(N*M), что существенно замедлит работу интерфейса.

В таких задачах идеальной заменой массиву структур служит объект Соответствие:

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

Разберем пример формирования соответствия на сервере:


&НаСервере
Функция ПолучитьДанныеПоНоменклатуре()
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   Остатки.Номенклатура КАК Номенклатура,
        |   Остатки.ВНаличииОстаток КАК ВНаличии,
        |   Остатки.ВРезервеОстаток КАК Резерв
        |ИЗ
        |   РегистрНакопления.Запасы.Остатки КАК Остатки";
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    ДанныеОстатков = Новый Соответствие;
    Пока Выборка.Следующий() Цикл
        // Сохраняем структуру показателей под ключом номенклатуры
        Показатели = Новый Структура("ВНаличии, Резерв", Выборка.ВНаличии, Выборка.Резерв);
        ДанныеОстатков.Вставить(Выборка.Номенклатура, Показатели);
    КонецЦикла;
    
    Возврат ДанныеОстатков;
КонецФункции

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


&НаКлиенте
Процедура ОбработатьСтроку(ТекущаяНоменклатура)
    
    ДанныеПозиции = ДанныеОстатков[ТекущаяНоменклатура];
    Если ДанныеПозиции <> Неопределено Тогда
        Остаток = ДанныеПозиции.ВНаличии;
        Резерв  = ДанныеПозиции.Резерв;
    КонецЕсли;
    
КонецПроцедуры

Вариант 3. Использование коллекции формы и метода НайтиСтроки()

Если данные необходимо отображать в таблице на форме или если требуется сложный поиск по нескольким колонкам, данные можно передать в реквизит формы типа ТаблицаЗначений. На клиенте такой реквизит автоматически становится объектом типа ДанныеФормыКоллекция.

У объекта ДанныеФормыКоллекция доступен метод НайтиСтроки(), который принимает структуру с отбором:


&НаКлиенте
Процедура НайтиВДанныхФормы()
    
    ПараметрыОтбора = Новый Структура("Номенклатура", ТекущаяНоменклатура);
    НайденныеСтроки = ЭлементыДанных.НайтиСтроки(ПараметрыОтбора);
    
    Если НайденныеСтроки.Количество() > 0 Тогда
        СтрокаРезультата = НайденныеСтроки[0];
        // Обработка полученных данных
    КонецЕсли;
    
КонецПроцедуры

Архитектурная оптимизация: перенос обработки на сервер

Проанализируем задачу автора темы: сбор больших объемов данных по продажам, остаткам и резервам для последующей выгрузки в сторонние системы (например, Microsoft Access или Excel). Передача таких данных на тонкий клиент через массив структур — грубая архитектурная ошибка по нескольким причинам:

  1. Расход оперативной памяти. На каждую строку создается отдельный объект Структура, что многократно увеличивает объем занимаемой памяти и создает избыточную нагрузку на сборщик мусора.
  2. Трафик между клиентом и сервером. Передача массивных структур по сети приводит к зависанию интерфейса тонкого клиента во время сериализации и десериализации.
  3. Проблемы с внешними интерфейсами. Если на сервере и клиенте используются разные архитектуры (например, 64-битный сервер и COM-соединения к Access), выгрузка через клиент кажется вынужденной мерой, однако сопоставление данных все равно должно выполняться на стороне СУБД.

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


&НаСервере
Процедура СформироватьДанныеДляВыгрузки()
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ПродажиОбороты.Номенклатура КАК Номенклатура,
        |   ПродажиОбороты.КоличествоОборот КАК Продано,
        |   ЕСТЬNULL(ТоварыНаСкладахОстатки.ВНаличииОстаток, 0) КАК Остаток
        |ИЗ
        |   РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , ) КАК ПродажиОбороты
        |   ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(&КонецПериода, ) КАК ТоварыНаСкладахОстатки
        |       ПО ПродажиОбороты.Номенклатура = ТоварыНаСкладахОстатки.Номенклатура";
    
    ТаблицаИтог = Запрос.Выполнить().Выгрузить();
    
    // Если на ТаблицеЗначений на сервере требуется частый поиск,
    // используем индексацию колонок:
    ТаблицаИтог.Индексы.Добавить("Номенклатура");
    
    // Выполняем экспорт в Access или текстовый файл прямо на сервере,
    // не передавая тяжелые данные по сети на клиент.
    
КонецПроцедуры

Если выгрузка в стороннюю базу (MS Access) не может быть выполнена на сервере из-за операционной системы (например, сервер работает на Linux, где недоступны OLE DB/COM-драйверы Microsoft Jet/ACE), оптимальным решением будет сформировать на сервере универсальный файл обмена (CSV, JSON или XML), поместить его во временное хранилище, передать адрес на клиент и сохранить итоговый файл штатными методами.

← На главную