Как изменить контекст выполнения формулы или функции Вычислить() в 1С?

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

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

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

Проблема: ограничение стандартного вычисления

Представим ситуацию: у нас есть строка формулы "Функ(СубконтоДт2.Подразделение)" и объект Запись, содержащий этот реквизит. Если мы попытаемся выполнить код напрямую:

Результат = Вычислить("Функ(СубконтоДт2.Подразделение)");

Платформа выдаст ошибку, так как в текущем контексте модуля нет переменной СубконтоДт2. Простое добавление префикса в начало строки (например, превращение ее в "Запись.СубконтоДт2.Подразделение") не решит проблему, если формула содержит сложные арифметические операции, вызовы других функций или несколько разных переменных. Давайте разберем, как решить эту задачу правильно.

Решение 1. Динамическое объявление локальных переменных через Выполнить()

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

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

Рассмотрим пример реализации этой логики:


Функция ВычислитьВКонтекстеОбъекта(Знач Формула, Знач ОбъектКонтекста) Экспорт
    
    // Шаг 1. Формируем текст объявления переменных
    ТекстИнициализации = "";
    МетаданныеОбъекта = ОбъектКонтекста.Метаданные();
    
    // Перебираем все реквизиты объекта
    Для Каждого Реквизит Из МетаданныеОбъекта.Реквизиты Цикл
        ИмяРеквизита = Реквизит.Имя;
        ТекстИнициализации = ТекстИнициализации + ИмяРеквизита + " = ОбъектКонтекста." + ИмяРеквизита + ";" + Символы.ПС;
    КонецЦикла;
    
    // Если это документ или справочник, также можем добавить стандартные реквизиты
    Для Каждого СтандартныйРеквизит Из МетаданныеОбъекта.СтандартныеРеквизиты Цикл
        ИмяРеквизита = СтандартныйРеквизит.Имя;
        ТекстИнициализации = ТекстИнициализации + ИмяРеквизита + " = ОбъектКонтекста." + ИмяРеквизита + ";" + Символы.ПС;
    КонецЦикла;
    
    // Шаг 2. Добавляем переменную для получения результата
    Перем РезультатВычисления;
    ПолныйТекстКода = ТекстИнициализации + "РезультатВычисления = " + Формула + ";";
    
    // Шаг 3. Динамически выполняем код
    Попытка
        Выполнить(ПолныйТекстКода);
    Исключение
        ВызватьИсключение "Ошибка при вычислении формулы: " + ОписаниеОшибки();
    КонецПопытки;
    
    Возврат РезультатВычисления;
    
КонецФункции

Преимущества метода:

Недостатки метода:

Решение 2. Оптимизированный контекст через Структуру

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

Проанализируем пример такого подхода:


Функция ВычислитьЧерезСтруктуру(Знач Формула, Знач СтруктураДанных) Экспорт
    
    КодИнициализации = "";
    Для Каждого Элемент Из СтруктураДанных Цикл
        КодИнициализации = КодИнициализации + Элемент.Ключ + " = СтруктураДанных." + Элемент.Ключ + ";" + Символы.ПС;
    КонецЦикла;
    
    Перем РезультатВычисления;
    КодВыполнения = КодИнициализации + "РезультатВычисления = " + Формула + ";";
    
    Выполнить(КодВыполнения);
    
    Возврат РезультатВычисления;
    
КонецФункции

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

Решение 3. Использование Системы компоновки данных (СКД)

Если безопасность системы является для вас приоритетом, или вы хотите избежать использования потенциально опасной функции Выполнить(), идеальным решением будет использование Системы компоновки данных (СКД) в качестве «вычислительного движка» прямо в памяти.

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

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


Функция ВычислитьФормулуЧерезСКД(Знач Формула, Знач ОбъектКонтекста) Экспорт
    
    // Создаем схему компоновки программно
    СхемаКомпоновки = Новый СхемаКомпоновкиДанных;
    
    // Создаем источник данных
    ИсточникДанных = СхемаКомпоновки.ИсточникиДанных.Добавить("Источник");
    ИсточникДанных.ТипИсточникаДанных = "Local";
    
    // Создаем набор данных - объект (таблицу значений)
    НаборДанных = СхемаКомпоновки.НаборыДанных.Добавить(Тип("НаборДанныхОбъектСхемыКомпоновкиДанных"));
    НаборДанных.Имя = "НаборДанных";
    НаборДанных.ИмяОбъекта = "ТаблицаКонтекста";
    НаборДанных.ИсточникДанных = "Источник";
    
    // Создаем таблицу значений с одной строкой для передачи контекста
    ТаблицаКонтекста = Новый ТаблицаЗначений;
    МетаданныеОбъекта = ОбъектКонтекста.Metadane(); // или свойства структуры
    
    // Заполняем колонки таблицы на основе реквизитов контекста
    Для Каждого Реквизит Из МетаданныеОбъекта.Реквизиты Цикл
        ИмяПоля = Реквизит.Имя;
        ТаблицаКонтекста.Колонки.Добавить(ИмяПоля);
        
        // Регистрируем поле в СКД
        ПолеНабора = НаборДанных.Поля.Добавить(Тип("ПолеНабораДанныхСхемыКомпоновкиДанных"));
        ПолеНабора.Поле = ИмяПоля;
        ПолеНабора.ПутьКДанным = ИмяПоля;
    КонецЦикла;
    
    // Добавляем вычисляемое поле, где выражение - это наша формула
    ВычисляемоеПоле = СхемаКомпоновки.ВычисляемыеПоля.Добавить();
    ВычисляемоеПоле.ПутьКДанным = "Результат";
    ВычисляемоеПоле.Выражение = Формула;
    
    // Заполняем строку таблицы контекста значениями из нашего объекта
    НоваяСтрока = ТаблицаКонтекста.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяСтрока, ОбъектКонтекста);
    
    // Инициализируем процессор компоновки для расчета
    Настройки = СхемаКомпоновки.НастройкиПоУмолчанию;
    
    // Добавляем поле Результат в выбранные поля
    ВыбранноеПоле = Настройки.Выбор.Элементы.Добавить(Тип("АвтоВыбранноеПолеКомпоновкиДанных"));
    
    КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
    МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновки, Настройки);
    
    // Передаем таблицу в процессор компоновки
    ВнешниеНаборыДанных = Новый Структура;
    ВнешниеНаборыДанных.Вставить("ТаблицаКонтекста", ТаблицаКонтекста);
    
    ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
    ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, ВнешниеНаборыДанных);
    
    // Выводим результат в коллекцию значений
    ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
    ИтоговаяТаблица = Новый ТаблицаЗначений;
    ПроцессорВывода.УстановитьОбъект(ИтоговаяТаблица);
    ПроцессорВывода.Вывести(ПроцессорКомпоновки);
    
    Если ИтоговаяТаблица.Количество() > 0 Тогда
        Возврат ИтоговаяТаблица[0].Результат;
    КонецЕсли;
    
    Возврат Неопределено;
    
КонецФункции

Преимущества метода СКД:

Недостатки метода СКД:

Сравнение подходов и рекомендации

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

  1. Если вам нужно выполнить **быстрые разовые расчеты** в доверенной среде (например, в закрытой административной обработке), используйте **Решение 1 или 2** с генерацией локального кода через Выполнить(). Это потребует минимум строк кода и обеспечит гибкость.
  2. Если расчеты производятся над **большими массивами данных** (тысячи строк), старайтесь избегать динамической генерации кода в циклах. Вместо этого заранее соберите все необходимые данные в одну таблицу значений и обработайте ее запросом или через СКД.
  3. Если формулы настраиваются **конечными пользователями** (например, формулы расчета скидок в CRM или подсистемы бюджетирования), используйте **Решение 3 (СКД)**. Это единственный надежный способ защитить базу данных от случайного или намеренного деструктивного вмешательства через выполнение вредоносного кода.
← На главную