Одна процедура вызывает другую во встроенном языке 1С 8.3: параметры, контекст, рекурсия и ошибки
Содержание
Чтобы вызвать процедуру из процедуры в 1С, напишите внутри одной процедуры имя другой и круглые скобки с параметрами: ЗаполнитьШапку(Документ);. Если вторая процедура лежит в том же модуле, больше ничего не нужно. Если в другом модуле — у неё должно стоять слово Экспорт, а вызов пишется через имя модуля: РаботаСКлиентами.ЗаполнитьШапку(Документ);.
Вариантов вызова несколько, и выбирают их по месту, где лежит код: тот же модуль — просто имя; общий модуль — ИмяМодуля.Процедура(); модуль объекта или менеджера — через объект или менеджер; форма на клиенте — через серверную процедуру с директивой. Ниже каждый случай с примерами для платформы 8.3 и управляемых форм.
Синтаксис вызова процедуры в 1С 8.3
Процедура — это блок кода без возвращаемого значения. Объявление и вызов выглядят так:
Процедура ПересчитатьСумму(СтрокаТЧ)
СтрокаТЧ.Сумма = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
КонецПроцедуры
Процедура ТоварыКоличествоПриИзменении(Элемент)
ТекущаяСтрока = Элементы.Товары.ТекущиеДанные;
ПересчитатьСумму(ТекущаяСтрока);
КонецПроцедуры
Несколько правил, которые работают всегда:
- 📌 Порядок объявления в модуле 8.3 не важен. Можно вызвать процедуру, которая описана ниже. Слово
Далеедля предварительного объявления было нужно в 7.7, в 8.x оно не используется. - 📌 Скобки обязательны, даже без параметров:
ОбновитьИтоги();. - 📌 Процедуру нельзя использовать в выражении:
Сумма = ПересчитатьСумму(Стр);не скомпилируется. Если нужен результат — объявитеФункцияи верните значение черезВозврат. - 📌 Функцию, наоборот, можно вызвать как процедуру:
ПроверитьЗаполнение();— результат просто отбросится. - 📌 Досрочный выход из процедуры —
Возврат;без значения.
Необязательные параметры
Параметр со значением по умолчанию можно не передавать:
Процедура ЗаписатьВЖурнал(Текст, Уровень = "Информация", Объект = Неопределено)
// ...
КонецПроцедуры
ЗаписатьВЖурнал("Загрузка завершена");
ЗаписатьВЖурнал("Нет цены", "Ошибка");
ЗаписатьВЖурнал("Нет цены", , ТекущийДокумент); // второй параметр пропущен
Пропущенный параметр в середине оставляют пустым местом между запятыми. Если передать больше параметров, чем объявлено, или меньше обязательных, модуль не пройдёт синтаксический контроль.
Знач или по ссылке: как передаются параметры
По умолчанию параметры в 1С передаются по ссылке. Вызываемая процедура работает с той же переменной, что и вызывающая. Если присвоить параметру новое значение, оно вернётся наружу.
Процедура Увеличить(Число)
Число = Число + 1;
КонецПроцедуры
Процедура Тест()
А = 5;
Увеличить(А);
Сообщить(А); // 6
КонецПроцедуры
Ключевое слово Знач перед параметром делает копию значения. Присваивание внутри процедуры наружу не уходит:
Процедура Увеличить(Знач Число)
Число = Число + 1;
КонецПроцедуры
// после вызова А по-прежнему 5
Знач не защищает содержимое коллекций и объектов. Для массива, структуры, таблицы значений или объекта копируется только ссылка. Добавленный в массив элемент увидит и вызывающий код. Запретить можно лишь замену самой переменной на другой массив.
| Что передаём | Без Знач | С Знач |
|---|---|---|
| Число, строка, дата, булево | присваивание меняет переменную вызывающего | изменения остаются внутри |
| Массив, Структура, ТаблицаЗначений | меняется и содержимое, и сама переменная | содержимое меняется, замена переменной не уходит наружу |
| Объект (ДокументОбъект и т. п.) | то же, что у коллекций | то же, что у коллекций |
Нужна независимая копия структуры или массива — копируйте явно. В типовых конфигурациях на БСП для этого есть ОбщегоНазначения.СкопироватьРекурсивно(), без БСП — новый массив и цикл по элементам.
Для серверных вызовов из формы Знач даёт ещё и скорость. Параметр без Знач после выполнения серверной процедуры пересылается обратно на клиент. С Знач обратной передачи нет. На больших таблицах значений разница заметна.
Где платформа ищет вызываемую процедуру
Самый частый вопрос на практике — не «как написать вызов», а «почему процедура не видна». Ответ зависит от модуля, в котором она лежит.
| Где лежит процедура | Как вызвать | Условие |
|---|---|---|
| В том же модуле | ИмяПроцедуры() | ничего |
| В общем модуле | ИмяМодуля.ИмяПроцедуры() | Экспорт + подходящие флаги модуля |
| В модуле объекта | ДокОбъект.ИмяПроцедуры() | Экспорт, есть объект |
| В модуле менеджера | Документы.Реализация.ИмяПроцедуры() | Экспорт |
| В модуле другой формы | Форма.ИмяПроцедуры() | Экспорт, форма получена через ПолучитьФорму() или открыта |
Общий модуль и его флаги
У общего модуля в свойствах стоят флажки: Клиент (управляемое приложение), Сервер, Вызов сервера, Внешнее соединение, Привилегированный. Флаги решают, откуда виден модуль:
- 🖥️ только
Сервер— процедуры доступны из серверного кода: модулей объектов, менеджеров, серверных процедур форм; - 🔁
Сервер+Вызов сервера— процедуры можно вызвать прямо с клиента, платформа сама сделает серверный вызов; - 💻
Клиент— процедуры работают на клиенте, с сервера их не вызвать.
Если клиентская процедура формы обращается к серверному общему модулю без флага Вызов сервера, при проверке модуля появится ошибка, что процедура или метод не обнаружены.
Клиент и сервер внутри формы
В модуле управляемой формы у каждой процедуры есть директива. Клиентская процедура может вызвать серверную, серверная клиентскую — нет.
&НаКлиенте
Процедура Заполнить(Команда)
ЗаполнитьНаСервере();
ОбновитьИтогиНаКлиенте();
КонецПроцедуры
&НаСервере
Процедура ЗаполнитьНаСервере()
Объект.Товары.Очистить();
// запрос и заполнение
КонецПроцедуры
&НаСервереБезКонтекста
Функция ЦенаТовара(Знач Номенклатура)
Возврат РегистрыСведений.ЦеныНоменклатуры.ПолучитьПоследнее(, Новый Структура("Номенклатура", Номенклатура)).Цена;
КонецФункции
&НаСервереБезКонтекста не видит реквизиты формы и Объект. Зато такой вызов не гоняет данные формы на сервер и обратно. Если процедуре нужны только пара значений — передайте их параметрами и ставьте эту директиву.
Рекурсия: процедура вызывает сама себя
Рекурсия нужна для обхода иерархий: дерева значений, иерархического справочника, вложенных структур. Процедура вызывает себя для каждого дочернего узла.
Процедура СобратьЛистья(Строки, Результат)
Для Каждого Строка Из Строки Цикл
Если Строка.Строки.Количество() = 0 Тогда
Результат.Добавить(Строка.Номенклатура);
Иначе
СобратьЛистья(Строка.Строки, Результат);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
// вызов
Листья = Новый Массив;
СобратьЛистья(ДеревоТоваров.Строки, Листья);
Здесь массив Результат передаётся по ссылке и наполняется на всех уровнях. Это и есть полезная сторона передачи без Знач.
Три правила безопасной рекурсии:
- Условие выхода проверяется до рекурсивного вызова. В примере это строка без подчинённых.
- Глубина должна быть ограничена. Стек встроенного языка не бесконечен, при ошибке в данных (элемент ссылается сам на себя через цепочку родителей) процедура упадёт с переполнением стека. Для данных из базы добавьте параметр-счётчик уровня и выходите, например, после 50 уровней.
- Не делайте запрос к базе на каждом шаге. Для иерархического справочника лучше один запрос с
ВЫБРАТЬ ... ИТОГИ ПО Ссылка ИЕРАРХИЯили выгрузка в дерево, а рекурсией обходить уже дерево в памяти.
Ошибки во вложенных вызовах: Попытка и ВызватьИсключение
Исключение, возникшее в глубоко вложенной процедуре, поднимается по цепочке вызовов до первого блока Попытка. Если блока нет нигде — выполнение прерывается, пользователь видит окно ошибки.
Процедура ЗагрузитьФайл(ИмяФайла)
Попытка
ПрочитатьИЗаписать(ИмяФайла);
Исключение
ЗаписьЖурналаРегистрации("Загрузка прайса", УровеньЖурналаРегистрации.Ошибка, , ,
ОписаниеОшибки());
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Процедура ПрочитатьИЗаписать(ИмяФайла)
Если НЕ ЗначениеЗаполнено(ИмяФайла) Тогда
ВызватьИсключение "Не указан файл для загрузки";
КонецЕсли;
// чтение и запись
КонецПроцедуры
Что здесь происходит:
- ⚙️ нижняя процедура сама сообщает о проблеме через
ВызватьИсключение "текст"; - 📝 верхняя ловит исключение, пишет его в журнал регистрации;
- 🔁
ВызватьИсключение;без параметров внутри блокаИсключениепробрасывает ту же ошибку выше, не теряя исходный текст.
Не оставляйте пустой блок Исключение КонецПопытки;. Ошибка исчезнет без следа, а данные останутся записанными наполовину.
С транзакциями правило жёстче. Если процедура начала транзакцию, она же должна её завершить при любом исходе:
НачатьТранзакцию();
Попытка
ЗаписатьДвижения(Документ);
ОбновитьОстатки(Документ);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Вложенная процедура ОбновитьОстатки внутри не должна ловить исключение и продолжать работу молча: после ошибки внутри транзакции её уже нельзя зафиксировать.
Вызов процедуры по имени из строки и через оповещение
Иногда имя процедуры известно только во время работы: из настроек, из таблицы обработчиков. Для этого есть два легальных способа.
Выполнить
ИмяОбработчика = "ПересчитатьСкидки";
Выполнить(ИмяОбработчика + "(Документ)");
Выполнить() исполняет строку кода в текущем контексте, поэтому переменная Документ должна быть видна в месте вызова. Минусы: синтаксис не проверяется при компиляции, а строка из внешних данных открывает дыру для произвольного кода. В типовых на БСП вместо прямого Выполнить используют ОбщегоНазначения.ВыполнитьМетодКонфигурации(ИмяМетода, Параметры) — он проверяет, что имя похоже на метод конфигурации.
ОписаниеОповещения
Для асинхронных диалогов на клиенте процедуру передают как описание оповещения. Она вызовется позже, когда пользователь ответит:
&НаКлиенте
Процедура Очистить(Команда)
Оповещение = Новый ОписаниеОповещения("ОчиститьЗавершение", ЭтотОбъект);
ПоказатьВопрос(Оповещение, "Очистить табличную часть?", РежимДиалогаВопрос.ДаНет);
КонецПроцедуры
&НаКлиенте
Процедура ОчиститьЗавершение(Результат, ДополнительныеПараметры) Экспорт
Если Результат = КодВозвратаДиалога.Да Тогда
Объект.Товары.Очистить();
КонецЕсли;
КонецПроцедуры
Код после ПоказатьВопрос выполняется сразу, не дожидаясь ответа. Всё, что зависит от выбора пользователя, переносите в процедуру-обработчик. Вызвать такой обработчик из своего кода можно через ВыполнитьОбработкуОповещения(Оповещение, Результат).
На новых платформах есть и вариант с Асинх/Ждать, где код пишется последовательно. Но он требует режима совместимости конфигурации не ниже того, где эти слова появились, поэтому в типовых старых релизах встречается редко.
Как ускорить код с вложенными вызовами
Сам по себе вызов процедуры дешёвый. Тормозит то, что внутри и где он выполняется.
- 🚫 Серверный вызов в цикле на клиенте. 100 строк — 100 обращений к серверу. Соберите данные в массив и передайте одним вызовом в серверную процедуру.
- 🚫 Запрос в цикле. Процедура
ПолучитьЦену(Товар), вызванная для каждой строки, делает столько же запросов. Лучше одна процедура, которая получает таблицу товаров и возвращает все цены одним запросом. - ✅ Кэш для неизменных данных. Общий модуль с настройкой
Повторное использование возвращаемых значенийкэширует результат на время вызова или сеанса. Кэшируются только функции: процедура ничего не возвращает, кэшировать нечего. - ✅
Значв серверных процедурах формы — меньше данных летит обратно на клиент. - ✅ Одна процедура — одна задача. Короткие процедуры по 20–40 строк легче отлаживать: в отладчике видно, на каком уровне стека возникла ошибка.
Найти медленный вызов помогает замер производительности в отладчике Конфигуратора или EDT: он показывает время по строкам и количество вызовов каждой процедуры.
Пример целиком: заполнение документа из нескольких процедур
Задача: по кнопке на форме заказа подобрать товары по остаткам, проставить цены и пересчитать итог. Код разложен по уровням.
// Модуль формы
&НаКлиенте
Процедура ПодобратьПоОстаткам(Команда)
ЗаполнитьТоварыНаСервере();
ПересчитатьИтог();
КонецПроцедуры
&НаСервере
Процедура ЗаполнитьТоварыНаСервере()
ДокОбъект = РеквизитФормыВЗначение("Объект");
ДокОбъект.ЗаполнитьПоОстаткам();
ЗначениеВРеквизитФормы(ДокОбъект, "Объект");
КонецПроцедуры
&НаКлиенте
Процедура ПересчитатьИтог()
Объект.СуммаДокумента = Объект.Товары.Итог("Сумма");
КонецПроцедуры
// Модуль объекта документа
Процедура ЗаполнитьПоОстаткам() Экспорт
Товары.Очистить();
Таблица = ПродажиСервер.ОстаткиСклада(Склад);
Для Каждого Строка Из Таблица Цикл
НоваяСтрока = Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Строка);
НоваяСтрока.Сумма = НоваяСтрока.Количество * НоваяСтрока.Цена;
КонецЦикла;
КонецПроцедуры
Цепочка вызовов: клиентская процедура формы → серверная процедура формы → экспортная процедура модуля объекта → функция серверного общего модуля ПродажиСервер (её код с одним запросом остатков и цен здесь не показан). Для этой цепочки модулю ПродажиСервер достаточно флага Сервер: его вызывают уже с сервера, а не с клиента.
Итог пересчитывается на клиенте: данные уже там, серверный вызов ради суммы колонки не нужен.