Одна процедура вызывает другую во встроенном языке 1С 8.3: параметры, контекст, рекурсия и ошибки

· 9 мин

Содержание

Чтобы вызвать процедуру из процедуры в 1С, напишите внутри одной процедуры имя другой и круглые скобки с параметрами: ЗаполнитьШапку(Документ);. Если вторая процедура лежит в том же модуле, больше ничего не нужно. Если в другом модуле — у неё должно стоять слово Экспорт, а вызов пишется через имя модуля: РаботаСКлиентами.ЗаполнитьШапку(Документ);.

Вариантов вызова несколько, и выбирают их по месту, где лежит код: тот же модуль — просто имя; общий модуль — ИмяМодуля.Процедура(); модуль объекта или менеджера — через объект или менеджер; форма на клиенте — через серверную процедуру с директивой. Ниже каждый случай с примерами для платформы 8.3 и управляемых форм.

Синтаксис вызова процедуры в 1С 8.3

Процедура — это блок кода без возвращаемого значения. Объявление и вызов выглядят так:

Процедура ПересчитатьСумму(СтрокаТЧ)
	СтрокаТЧ.Сумма = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
КонецПроцедуры

Процедура ТоварыКоличествоПриИзменении(Элемент)
	ТекущаяСтрока = Элементы.Товары.ТекущиеДанные;
	ПересчитатьСумму(ТекущаяСтрока);
КонецПроцедуры

Несколько правил, которые работают всегда:

  • 📌 Порядок объявления в модуле 8.3 не важен. Можно вызвать процедуру, которая описана ниже. Слово Далее для предварительного объявления было нужно в 7.7, в 8.x оно не используется.
  • 📌 Скобки обязательны, даже без параметров: ОбновитьИтоги();.
  • 📌 Процедуру нельзя использовать в выражении: Сумма = ПересчитатьСумму(Стр); не скомпилируется. Если нужен результат — объявите Функция и верните значение через Возврат.
  • 📌 Функцию, наоборот, можно вызвать как процедуру: ПроверитьЗаполнение(); — результат просто отбросится.
  • 📌 Досрочный выход из процедуры — Возврат; без значения.

Необязательные параметры

Параметр со значением по умолчанию можно не передавать:

Процедура ЗаписатьВЖурнал(Текст, Уровень = "Информация", Объект = Неопределено)
	// ...
КонецПроцедуры

ЗаписатьВЖурнал("Загрузка завершена");
ЗаписатьВЖурнал("Нет цены", "Ошибка");
ЗаписатьВЖурнал("Нет цены", , ТекущийДокумент); // второй параметр пропущен

Пропущенный параметр в середине оставляют пустым местом между запятыми. Если передать больше параметров, чем объявлено, или меньше обязательных, модуль не пройдёт синтаксический контроль.

Знач или по ссылке: как передаются параметры

По умолчанию параметры в 1С передаются по ссылке. Вызываемая процедура работает с той же переменной, что и вызывающая. Если присвоить параметру новое значение, оно вернётся наружу.

Процедура Увеличить(Число)
	Число = Число + 1;
КонецПроцедуры

Процедура Тест()
	А = 5;
	Увеличить(А);
	Сообщить(А); // 6
КонецПроцедуры

Ключевое слово Знач перед параметром делает копию значения. Присваивание внутри процедуры наружу не уходит:

Процедура Увеличить(Знач Число)
	Число = Число + 1;
КонецПроцедуры
// после вызова А по-прежнему 5

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

Что передаёмБез ЗначС Знач
Число, строка, дата, булевоприсваивание меняет переменную вызывающегоизменения остаются внутри
Массив, Структура, ТаблицаЗначенийменяется и содержимое, и сама переменнаясодержимое меняется, замена переменной не уходит наружу
Объект (ДокументОбъект и т. п.)то же, что у коллекцийто же, что у коллекций

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

Для серверных вызовов из формы Знач даёт ещё и скорость. Параметр без Знач после выполнения серверной процедуры пересылается обратно на клиент. С Знач обратной передачи нет. На больших таблицах значений разница заметна.

Где платформа ищет вызываемую процедуру

Самый частый вопрос на практике — не «как написать вызов», а «почему процедура не видна». Ответ зависит от модуля, в котором она лежит.

Где лежит процедураКак вызватьУсловие
В том же модулеИмяПроцедуры()ничего
В общем модулеИмяМодуля.ИмяПроцедуры()Экспорт + подходящие флаги модуля
В модуле объектаДокОбъект.ИмяПроцедуры()Экспорт, есть объект
В модуле менеджераДокументы.Реализация.ИмяПроцедуры()Экспорт
В модуле другой формыФорма.ИмяПроцедуры()Экспорт, форма получена через ПолучитьФорму() или открыта

Общий модуль и его флаги

У общего модуля в свойствах стоят флажки: Клиент (управляемое приложение), Сервер, Вызов сервера, Внешнее соединение, Привилегированный. Флаги решают, откуда виден модуль:

  • 🖥️ только Сервер — процедуры доступны из серверного кода: модулей объектов, менеджеров, серверных процедур форм;
  • 🔁 Сервер + Вызов сервера — процедуры можно вызвать прямо с клиента, платформа сама сделает серверный вызов;
  • 💻 Клиент — процедуры работают на клиенте, с сервера их не вызвать.

Если клиентская процедура формы обращается к серверному общему модулю без флага Вызов сервера, при проверке модуля появится ошибка, что процедура или метод не обнаружены.

Клиент и сервер внутри формы

В модуле управляемой формы у каждой процедуры есть директива. Клиентская процедура может вызвать серверную, серверная клиентскую — нет.

&НаКлиенте
Процедура Заполнить(Команда)
	ЗаполнитьНаСервере();
	ОбновитьИтогиНаКлиенте();
КонецПроцедуры

&НаСервере
Процедура ЗаполнитьНаСервере()
	Объект.Товары.Очистить();
	// запрос и заполнение
КонецПроцедуры

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

&НаСервереБезКонтекста не видит реквизиты формы и Объект. Зато такой вызов не гоняет данные формы на сервер и обратно. Если процедуре нужны только пара значений — передайте их параметрами и ставьте эту директиву.

Рекурсия: процедура вызывает сама себя

Рекурсия нужна для обхода иерархий: дерева значений, иерархического справочника, вложенных структур. Процедура вызывает себя для каждого дочернего узла.

Процедура СобратьЛистья(Строки, Результат)
	Для Каждого Строка Из Строки Цикл
		Если Строка.Строки.Количество() = 0 Тогда
			Результат.Добавить(Строка.Номенклатура);
		Иначе
			СобратьЛистья(Строка.Строки, Результат);
		КонецЕсли;
	КонецЦикла;
КонецПроцедуры

// вызов
Листья = Новый Массив;
СобратьЛистья(ДеревоТоваров.Строки, Листья);

Здесь массив Результат передаётся по ссылке и наполняется на всех уровнях. Это и есть полезная сторона передачи без Знач.

Три правила безопасной рекурсии:

  1. Условие выхода проверяется до рекурсивного вызова. В примере это строка без подчинённых.
  2. Глубина должна быть ограничена. Стек встроенного языка не бесконечен, при ошибке в данных (элемент ссылается сам на себя через цепочку родителей) процедура упадёт с переполнением стека. Для данных из базы добавьте параметр-счётчик уровня и выходите, например, после 50 уровней.
  3. Не делайте запрос к базе на каждом шаге. Для иерархического справочника лучше один запрос с ВЫБРАТЬ ... ИТОГИ ПО Ссылка ИЕРАРХИЯ или выгрузка в дерево, а рекурсией обходить уже дерево в памяти.

Ошибки во вложенных вызовах: Попытка и ВызватьИсключение

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

Процедура ЗагрузитьФайл(ИмяФайла)
	Попытка
		ПрочитатьИЗаписать(ИмяФайла);
	Исключение
		ЗаписьЖурналаРегистрации("Загрузка прайса", УровеньЖурналаРегистрации.Ошибка, , ,
			ОписаниеОшибки());
		ВызватьИсключение;
	КонецПопытки;
КонецПроцедуры

Процедура ПрочитатьИЗаписать(ИмяФайла)
	Если НЕ ЗначениеЗаполнено(ИмяФайла) Тогда
		ВызватьИсключение "Не указан файл для загрузки";
	КонецЕсли;
	// чтение и запись
КонецПроцедуры

Что здесь происходит:

  • ⚙️ нижняя процедура сама сообщает о проблеме через ВызватьИсключение "текст";
  • 📝 верхняя ловит исключение, пишет его в журнал регистрации;
  • 🔁 ВызватьИсключение; без параметров внутри блока Исключение пробрасывает ту же ошибку выше, не теряя исходный текст.

Не оставляйте пустой блок Исключение КонецПопытки;. Ошибка исчезнет без следа, а данные останутся записанными наполовину.

С транзакциями правило жёстче. Если процедура начала транзакцию, она же должна её завершить при любом исходе:

НачатьТранзакцию();
Попытка
	ЗаписатьДвижения(Документ);
	ОбновитьОстатки(Документ);
	ЗафиксироватьТранзакцию();
Исключение
	ОтменитьТранзакцию();
	ВызватьИсключение;
КонецПопытки;

Вложенная процедура ОбновитьОстатки внутри не должна ловить исключение и продолжать работу молча: после ошибки внутри транзакции её уже нельзя зафиксировать.

Диагностика: выберите симптом

Вызов процедуры по имени из строки и через оповещение

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

Выполнить

ИмяОбработчика = "ПересчитатьСкидки";
Выполнить(ИмяОбработчика + "(Документ)");

Выполнить() исполняет строку кода в текущем контексте, поэтому переменная Документ должна быть видна в месте вызова. Минусы: синтаксис не проверяется при компиляции, а строка из внешних данных открывает дыру для произвольного кода. В типовых на БСП вместо прямого Выполнить используют ОбщегоНазначения.ВыполнитьМетодКонфигурации(ИмяМетода, Параметры) — он проверяет, что имя похоже на метод конфигурации.

ОписаниеОповещения

Для асинхронных диалогов на клиенте процедуру передают как описание оповещения. Она вызовется позже, когда пользователь ответит:

&НаКлиенте
Процедура Очистить(Команда)
	Оповещение = Новый ОписаниеОповещения("ОчиститьЗавершение", ЭтотОбъект);
	ПоказатьВопрос(Оповещение, "Очистить табличную часть?", РежимДиалогаВопрос.ДаНет);
КонецПроцедуры

&НаКлиенте
Процедура ОчиститьЗавершение(Результат, ДополнительныеПараметры) Экспорт
	Если Результат = КодВозвратаДиалога.Да Тогда
		Объект.Товары.Очистить();
	КонецЕсли;
КонецПроцедуры

Код после ПоказатьВопрос выполняется сразу, не дожидаясь ответа. Всё, что зависит от выбора пользователя, переносите в процедуру-обработчик. Вызвать такой обработчик из своего кода можно через ВыполнитьОбработкуОповещения(Оповещение, Результат).

На новых платформах есть и вариант с Асинх/Ждать, где код пишется последовательно. Но он требует режима совместимости конфигурации не ниже того, где эти слова появились, поэтому в типовых старых релизах встречается редко.

Как ускорить код с вложенными вызовами

Сам по себе вызов процедуры дешёвый. Тормозит то, что внутри и где он выполняется.

  • 🚫 Серверный вызов в цикле на клиенте. 100 строк — 100 обращений к серверу. Соберите данные в массив и передайте одним вызовом в серверную процедуру.
  • 🚫 Запрос в цикле. Процедура ПолучитьЦену(Товар), вызванная для каждой строки, делает столько же запросов. Лучше одна процедура, которая получает таблицу товаров и возвращает все цены одним запросом.
  • ✅ Кэш для неизменных данных. Общий модуль с настройкой Повторное использование возвращаемых значений кэширует результат на время вызова или сеанса. Кэшируются только функции: процедура ничего не возвращает, кэшировать нечего.
  • ✅ Знач в серверных процедурах формы — меньше данных летит обратно на клиент.
  • ✅ Одна процедура — одна задача. Короткие процедуры по 20–40 строк легче отлаживать: в отладчике видно, на каком уровне стека возникла ошибка.

Найти медленный вызов помогает замер производительности в отладчике Конфигуратора или EDT: он показывает время по строкам и количество вызовов каждой процедуры.

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

Задача: по кнопке на форме заказа подобрать товары по остаткам, проставить цены и пересчитать итог. Код разложен по уровням.

// Модуль формы
&НаКлиенте
Процедура ПодобратьПоОстаткам(Команда)
	ЗаполнитьТоварыНаСервере();
	ПересчитатьИтог();
КонецПроцедуры

&НаСервере
Процедура ЗаполнитьТоварыНаСервере()
	ДокОбъект = РеквизитФормыВЗначение("Объект");
	ДокОбъект.ЗаполнитьПоОстаткам();
	ЗначениеВРеквизитФормы(ДокОбъект, "Объект");
КонецПроцедуры

&НаКлиенте
Процедура ПересчитатьИтог()
	Объект.СуммаДокумента = Объект.Товары.Итог("Сумма");
КонецПроцедуры

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

Цепочка вызовов: клиентская процедура формы → серверная процедура формы → экспортная процедура модуля объекта → функция серверного общего модуля ПродажиСервер (её код с одним запросом остатков и цен здесь не показан). Для этой цепочки модулю ПродажиСервер достаточно флага Сервер: его вызывают уже с сервера, а не с клиента.

Итог пересчитывается на клиенте: данные уже там, серверный вызов ради суммы колонки не нужен.

Частые вопросы

Можно ли вызвать процедуру из модуля формы в модуле объекта?
Нет. Модуль объекта работает на сервере и формы не знает. Общий код переносите в модуль объекта, менеджера или общий модуль, а форма вызывает его.
Чем процедура отличается от функции при вызове?
Функция возвращает значение через Возврат и может стоять в выражении. Процедура вызывается только отдельной инструкцией.
Сколько параметров можно передать в процедуру?
Жёсткого практического предела нет, но больше 5–7 параметров неудобно. Удобнее передать одну структуру с именованными полями.
Нужно ли слово Экспорт, если процедура в том же модуле?
Не нужно. Экспорт открывает процедуру для других модулей. Внутри своего модуля она видна и без него.
Как вызвать процедуру внешней обработки из конфигурации?
Создайте объект обработки через ВнешниеОбработки.Создать() после подключения файла и вызовите её экспортную процедуру модуля объекта через точку.