При разработке отчетов и обработок в среде 1С:Предприятие одной из самых частых задач является обход коллекции данных (таблицы значений, результата запроса или массива) с целью вычисления общего итога. Начинающие разработчики часто сталкиваются с техническими трудностями при попытке сохранить промежуточное число из одной итерации цикла, чтобы сложить его со значением из следующей итерации.
В этой статье мы подробно разберем, как правильно реализовать накопительный итог внутри цикла, выясним истинные причины возникновения популярной ошибки "Преобразование к числу не может быть выполнено", а также проведем глубокий аудит кода, исправив скрытые уязвимости и оптимизировав производительность алгоритма.
Представим ситуацию: у нас есть цикл, который обрабатывает строки некоторой таблицы (например, список контрагентов с суммами взаиморасчетов) и выводит их в табличный документ. Нам необходимо не просто вывести строки, но и посчитать общую сумму по всем выводимым строкам, чтобы затем записать этот итог в шапку (Секция1) или подвал отчета.
Посмотрим на классическую ошибку, которую совершают программисты при попытке решить эту задачу:
Для Каждого СтрокаТаблицы Из СписокСтрок Цикл
// Какая-то логика вычислений...
СуммаВРублях = СтрокаТаблицы.Сумма * КурсВалюты;
// Попытка сложить значения
ВсегоСумма = ВсегоСумма + СуммаВРублях;
КонецЦикла;
При выполнении такого кода программа моментально аварийно завершится с ошибкой преобразования типов. Выясним, почему это происходит.
В платформе 1С:Предприятие используется динамическая типизация. Если переменная не была предварительно объявлена и ей не было присвоено начальное значение, то при первом обращении к ней система считает её тип равным Неопределено.
Когда цикл начинает свою работу на первой итерации, строчка кода ВсегоСумма = ВсегоСумма + СуммаВРублях пытается выполнить математическое сложение значения типа Неопределено (которое хранится в еще не инициализированной переменной ВсегоСумма) и числа из переменной СуммаВРублях. Платформа 1С не умеет складывать пустоту с числом, из-за чего и генерирует исключение.
Для решения этой проблемы необходимо строго соблюдать золотое правило программирования: переменная-накопитель всегда должна быть инициализирована нулем до начала работы цикла.
Рассмотрим правильный базовый шаблон:
// 1. Инициализируем переменную-накопитель числом 0 ДО входа в цикл
ВсегоСумма = 0;
Для Каждого СтрокаТаблицы Из СписокСтрок Цикл
СуммаВРублях = СтрокаТаблицы.Сумма * КурсВалюты;
// 2. Теперь сложение отработает корректно на любой итерации
ВсегоСумма = ВсегоСумма + СуммаВРублях;
КонецЦикла;
Проанализируем исходный код, предоставленный автором на форуме. Хотя после переноса инициализации переменной сумма3 = 0 за пределы цикла его сиюминутная проблема решилась, данный код содержит ряд критических ошибок, опечаток и уязвимостей, которые обязательно приведут к сбоям при изменении данных в базе.
Выделим ключевые проблемные места:
ттс: Для Каждого ттс из ИНН_ Цикл. Однако внутри тела цикла автор обращается к переменной тс: СуммаВРублях = тс.Сумма2; и Если тс.ДоговорКонтрагентаВалютаВзаиморасчетов.... Если переменная тс случайно была определена где-то выше во внешнем контексте, код выполнится, но выдаст абсолютно неверные математические расчеты. Если же она не определена, программа упадет с ошибкой.тс.ДоговорКонтрагентаВалютаВзаиморасчетов.Код = "978" — это потенциальная "мина замедленного действия". Если у контрагента не выбран договор или у договора не заполнена валюта, свойство вернет значение Неопределено или пустую ссылку. Попытка получить свойство .Код у пустого объекта вызовет фатальную ошибку: "Поле объекта не обнаружено (Код)".Остатки.НайтиСтроки(...) с одинаковыми параметрами — в начале цикла и в самом конце. Это избыточная операция, которая сильно замедляет работу системы, особенно если коллекция Остатки содержит большое количество записей.Секция2.Параметры..., а в ветке Иначе внезапно появляется Секция.Параметры.Сумма1 = 0;. Отсутствие единого именования переменных неизбежно приведет к ошибкам компиляции.Давайте проведем совместный рефакторинг этого алгоритма. Мы сделаем его полностью безопасным, устойчивым к пустым значениям в базе данных и оптимизируем с точки зрения производительности.
Шаг 1. Инициализация переменных и защита от деления/умножения на ноль. Перед циклом определим переменную для накопления итога и подготовим значение курса валюты, защитив код от возможных пустых ссылок.
ИтоговаяСумма = 0;
// Безопасно получаем курс евро. Если курс не задан или равен нулю, используем 1 во избежание логических ошибок
КурсЕвроЗначение = ?(КурсЕвро <> Неопределено И КурсЕвро.Курс <> 0, КурсЕвро.Курс, 1);
Шаг 2. Оптимизация поиска строк. Чтобы не перегружать процессор постоянным созданием структуры поиска и вызовом метода НайтиСтроки на каждой итерации, мы вынесем создание структуры и выполним поиск строк ровно один раз за одну итерацию в самом начале цикла.
Для Каждого ттс Из ИНН_ Цикл
// Создаем структуру поиска один раз за итерацию
ПараметрыПоиска = Новый Структура("Контрагент, ИНН, Сумма1, Сумма2", ттс.Контрагент, ттс.ИНН, ттс.Сумма1, ттс.Сумма2);
Стр_Осн = Остатки.НайтиСтроки(ПараметрыПоиска);
Для Каждого стр Из Стр_Осн Цикл
стр.ВыводитьСтроку = Ложь;
КонецЦикла;
Шаг 3. Обработка условий обхода и защита от пустых договоров. Реализуем проверку заполнения договора контрагента перед тем, как обращаться к его коду валюты.
Если ттс.Сумма1 = 0 И ттс.Сумма2 = 0 Тогда
Продолжить;
КонецЕсли;
Секция2.Параметры.Контрагент = ттс.Contragent; // используем корректную переменную ттс
Секция2.Параметры.Сумма1 = ттс.Сумма1;
// Защита от пустых значений договоров:
КодВалюты = "";
Если ЗначениеЗаполнено(ттс.ДоговорКонтрагентаВалютаВзаиморасчетов) Тогда
КодВалюты = ттс.ДоговорКонтрагентаВалютаВзаиморасчетов.Код;
КонецЕсли;
Шаг 4. Расчет суммы в рублях и безопасное суммирование. Заменим потенциально опасные значения типа Null на числовое выражение и аккуратно прибавим результат к нашей итоговой переменной.
Если КодВалюты = "978" Тогда
СуммаВРублях = ттс.Сумма1 * КурсЕвроЗначение;
Иначе
Секция2.Параметры.Сумма1 = 0; // Исправлена опечатка (было Секция)
СуммаВРублях = ттс.Сумма1;
КонецЕсли;
Секция2.Параметры.Сумма2 = СуммаВРублях;
// Предотвращаем падение при сложении, если СуммаВРублях вернула Null или Неопределено
ЧисловоеЗначениеСуммы = ?(СуммаВРублях = Неопределено Или СуммаВРублях = Null, 0, Число(СуммаВРублях));
// Накапливаем итоговое значение
ИтоговаяСумма = ИтоговаяСумма + ЧисловоеЗначениеСуммы;
// Выводим готовую секцию в результирующий табличный документ
// ТабличныйДокумент.Вывести(Секция2);
КонецЦикла;
Рассмотренный метод отлично решает задачу "в лоб", однако в профессиональной разработке на платформе 1С 8.3 (особенно в конфигурациях уровня "Управление торговлей 10.3") подобные расчеты, конвертации валют и сопоставление остатков крайне не рекомендуется выполнять непосредственно в цикле обхода табличной части.
Гораздо более правильным, производительным и профессиональным подходом является проведение всех расчетов на уровне СУБД с помощью механизма запросов 1С. Вы можете соединить таблицу остатков с курсами валют (используя виртуальную таблицу РегистрСведений.КурсыВалют.СрезПоследних) прямо в запросе. В этом случае на выходе вы получите уже готовую, рассчитанную в рублях таблицу данных, по которой останется лишь быстро пройтись простым циклом вывода без лишних проверок и дополнительных транзакций к базе данных.