Как найти значение в массиве структур в 1С и почему не работает стандартный метод Найти()?

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

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

Выясним причину: почему метод Найти() возвращает Неопределено

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


стр = Строка(ЗначениеИтератора);
ИскомоеЗначение = МассивПоиска.Найти(стр);

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

Для того чтобы понять, почему это происходит, нам необходимо разобраться с типами данных, которые находятся внутри коллекции МассивПоиска. В процессе обсуждения выяснилось, что элементами массива являются не простые строки или числа, а объекты типа Структура, внутри которых лежат пары ключ-значение (например, «Номенклатура» со ссылкой на справочник и «Цена» с числовым значением).

Когда мы вызываем метод Найти() объекта Массив, платформа 1С выполняет последовательное сравнение переданного параметра с каждым элементом массива. При этом действуют строгие правила сравнения типов в платформе:

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

Решение №1: Преобразование массива структур в Соответствие (Map)

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

Объект Соответствие — это специализированная коллекция «Ключ и Значение». В отличие от структуры, где ключом может выступать только строка, в объекте Соответствие ключом может быть значение абсолютно любого типа, включая объектные ссылки (например, СправочникСсылка.Номенклатура). Поиск элемента в соответствиях реализован на уровне платформы крайне эффективно и выполняется за константное время, что избавляет нас от необходимости писать обходы коллекции в цикле.

Разберем по шагам, как преобразовать исходный массив структур в соответствие и выполнить быстрый поиск:


// Шаг 1: Создаем новую коллекцию "Соответствие" на стороне клиента
ТаблицаЦен = Новый Соответствие;

// Шаг 2: Обходим исходный массив структур и заполняем Соответствие
Для Каждого ТекущаяСтруктура Из МассивСтруктурССервера Цикл
    // Ключом делаем саму ссылку на номенклатуру, значением - цену
    ТаблицаЦен.Вставить(ТекущаяСтруктура.Номенклатура, ТекущаяСтруктура.Цена);
КонецЦикла;

// Шаг 3: Теперь поиск нужного значения на клиенте выполняется мгновенно и без циклов
ИскомаяНоменклатура = ТекущаяСтрокаТЧ.Номенклатура;
ЦенаТовара = ТаблицаЦен.Получить(ИскомаяНоменклатура);

Если ЦенаТовара <> Неопределено Тогда
    // Обрабатываем полученный результат
    ТекущаяСтрокаТЧ.Цена = ЦенаТовара;
КонецЕсли;

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

Решение №2: Использование Таблицы Значений на сервере

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

Посмотрим на пример использования метода Найти() объекта ТаблицаЗначений. В отличие от массива, в таблице значений поиск можно выполнять с ограничением по конкретной колонке:


// Инициализируем таблицу значений на сервере
ТаблицаЦен = Новый ТаблицаЗначений;
ТаблицаЦен.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаЦен.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));

// Представим, что таблица уже заполнена данными...

// Поиск конкретной строки по ссылке номенклатуры
СтрокаПоиска = ТаблицаЦен.Найти(ИскомаяНоменклатура, "Номенклатура");

Если СтрокаПоиска <> Неопределено Тогда
    НайденнаяЦена = СтрокаПоиска.Цена;
КонецЕсли;

Если нам нужно найти несколько совпадений или выполнить сложный многокритериальный отбор, мы можем прибегнуть к встроенному методу НайтиСтроки(), передавая в него структуру отбора:


ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("Номенклатура", ИскомаяНоменклатура);

НайденныеСтроки = ТаблицаЦен.НайтиСтроки(ПараметрыОтбора);

Если НайденныеСтроки.Количество() > 0 Тогда
    НайденнаяЦена = НайденныеСтроки[0].Цена;
КонецЕсли;

Архитектурный анализ: почему «Массив не нужен» для табличной части документа

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

Выясним, как эту задачу решают профессиональные разработчики 1С. Существует два основных паттерна проектирования.

Паттерн №1: Пакетный запрос при изменении реквизитов шапки (Контекстный серверный вызов)

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

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


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

Этот паттерн минимизирует количество обращений к базе данных и выполняет всю тяжелую работу на сервере СУБД, возвращая на форму уже полностью заполненный и готовый результат.

Паттерн №2: Бесконтекстный вызов при интерактивном изменении отдельной строки

Что делать, если пользователь просто добавляет новую строку в табличную часть или меняет товар в текущей строке? В этом случае передавать весь контекст документа на сервер (а это может быть огромный документ с сотнями строк) слишком накладно для производительности системы. Нужно использовать легкий вызов с директивой компиляции &НаСервереБезКонтекста.

Рассмотрим подробнее, как правильно настроить этот процесс:


&НаКлиенте
Процедура ТоварыНоменклатураПриИзменении(Элемент)
    
    ТекущаяСтрока = Элементы.Товары.ТекущиеДанные;
    
    Если ТекущаяСтрока <> Неопределено И ЗначениеЗаполнено(ТекущаяСтрока.Номенклатура) Тогда
        
        // Быстро получаем цену с сервера без передачи тяжелого контекста формы
        НайденнаяЦена = ПолучитьЦенуНаСервереБезКонтекста(ТекущаяСтрока.Номенклатура, Объект.ВидЦен, Объект.Дата);
        
        ТекущаяСтрока.Цена = НайденнаяЦена;
        ТекущаяСтрока.Сумма = ТекущаяСтрока.Количество * ТекущаяСтрока.Цена;
        
    КонецЕсли;
    
КонецПроцедуры

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

Метод ПолучитьПоследнее() у менеджера периодического регистра сведений работает чрезвычайно быстро и не требует написания сложных текстов запросов. Это идеальный вариант для работы "в моменте" при интерактивных действиях пользователя в интерфейсе.

Резюме для разработчика

Подводя итоги нашего разбора, сформулируем три важных правила разработки на платформе «1С:Предприятие 8»:

  1. Никогда не используйте метод Найти() для поиска сложных объектов (таких как структуры или соответствия) в массиве. Этот метод разработан только для поиска простых типов данных или точных объектных ссылок, хранящихся непосредственно в памяти.
  2. Если вам необходимо сопоставлять пары «Ключ — Значение», всегда используйте встроенный объект Соответствие вместо самодельных массивов структур. Поиск по ключу в Соответствии работает практически мгновенно на уровне движка платформы.
  3. Для автоматического заполнения цен в табличных частях отдавайте предпочтение пакетным запросам или специализированным методам менеджеров периодических регистров сведений, выполняемых на сервере.
← На главную