При активном использовании интеграционных механизмов в среде «1С:Предприятие» разработчики часто сталкиваются с весьма странным поведением платформы. При чтении числовых данных из другой информационной базы посредством COM-соединения (OLE Automation) вместо ожидаемых точных значений, например, 3.24, система возвращает числа с бесконечным «хвостом» в дробной части — 3.239999999999991 или 3.240000000000002. На первый взгляд это кажется критической ошибкой самой платформы или СУБД, однако за этим поведением скрывается фундаментальная техническая особенность взаимодействия операционной системы Windows и COM-технологий.
В рамках этой статьи мы подробно разберем, почему возникает данная проблема, заглянем во внутреннее устройство механизмов передачи данных VARIANT и изучим четыре практических способа обхода этой неприятной особенности, которые гарантируют абсолютную точность ваших расчетов.
Для того чтобы понять суть проблемы, нам необходимо проанализировать, как именно хранятся и передаются числа в различных программных средах. Давайте поэтапно разберем этот процесс.
Во-первых, рассмотрим внутреннее представление чисел в самой платформе «1С:Предприятие». В прикладных решениях 1С тип Число представляет собой десятичное число с фиксированной точкой. Это сделано намеренно для обеспечения высочайшей точности в финансовых, бухгалтерских и складских операциях. При использовании фиксированной точки любые дробные значения (например, копейки 0.12 или доли ставки 0.25) записываются в памяти компьютера без искажений. Это исключает накопление погрешностей при операциях сложения, вычитания или умножения.
Во-вторых, проанализируем, что происходит в момент передачи данных через COM-соединение. В технологии OLE Automation для обмена примитивными типами данных используется универсальная системная структура, которая называется VARIANT. Платформа 1С обязана упаковать свое внутреннее число в один из типов, поддерживаемых этой структурой VARIANT:
VT_I4. При этом передача данных происходит абсолютно точно, без малейших искажений.VT_R8.В-третьих, выясним, почему тип VT_R8 искажает дробную часть. Тип данных VT_R8 представляет собой двоичное число с плавающей точкой двойной точности (соответствует стандарту IEEE 754). В двоичной системе счисления невозможно абсолютно точно представить большинство десятичных дробей. Точно так же, как в нашей привычной десятичной системе мы не можем записать дробь 1/3 в виде конечной десятичной строки (получается бесконечная периодическая дробь 0.333333...), в двоичной системе невозможно точно представить простые десятичные дроби, такие как 0.1, 0.2 или 0.24.
При передаче значения 3.24 через COM-соединение оно преобразуется в двоичный формат VT_R8 с неизбежным округлением до ближайшего возможного значения в двоичной сетке. Когда это округленное двоичное число возвращается обратно в среду 1С, платформа пытается интерпретировать его в десятичном виде, в результате чего разработчик видит «грязное» число с длинным хвостом из девяток или двоек.
Историческая справка: В спецификации структуры VARIANT существуют специализированные типы для хранения сверхточных десятичных данных, такие как VT_DECIMAL или VT_CY (Currency — денежный тип). Однако фирма «1С» исторически использует универсальный тип VT_R8 для обеспечения максимальной совместимости с широким спектром внешних OLE-клиентов (таких как старые версии Microsoft Excel, скрипты VBScript/JScript, а также для совместимости со старыми версиями платформы 1С 7.7).
---
Самый очевидный и простой способ решения проблемы — это банальное принудительное округление полученного значения до требуемой разрядности непосредственно после его считывания из COM-соединения. Для этого используется стандартная встроенная функция 1С Окр().
Проанализируем пример кода, в котором мы получаем числовое поле из выборки запроса через COM-соединение:
// Подключаемся к базе-источнику через COM
COMСоединение = Новый COMОбъект("V83.COMConnector");
БазаИсточник = COMСоединение.Connect("Srvr=""ServerName"";Ref=""BaseName"";Usr=""Admin"";Pwd=""Password"";");
// Выполняем запрос в базе-источнике
Запрос = БазаИсточник.NewObject("Запрос");
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Цена КАК Цена
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", СсылкаНаНоменклатуруВCOM);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
// Получаем "грязное" число из COM
ЦенаИзCOM = Выборка.Цена;
// Округляем до двух знаков после запятой
ТочнаяЦена = Окр(ЦенаИзCOM, 2);
Сообщить("Полученное значение: " + ЦенаИзCOM); // Выведет, например: 3.239999999999991
Сообщить("Округленное значение: " + ТочнаяЦена); // Выведет точное число: 3.24
КонецЕсли;
Этот способ отлично подходит для разовых операций или небольших обработок. Однако у него есть существенный недостаток: разработчику приходится вручную контролировать разрядность каждого получаемого реквизита, что увеличивает вероятность совершения ошибки при написании кода.
---
Если мы хотим полностью исключить влияние двоичного формата VT_R8 на наши данные, мы можем передавать числа в текстовом виде. Символьные строки передаются через COM-соединение посредством типа VT_BSTR, который не подвержен никаким искажениям точности.
Разберем по шагам этот алгоритм:
Формат(). При этом крайне важно отключить группировку триад цифр (параметр ЧГ=0), чтобы разделители групп (например, пробелы в числе 1 000 000) не помешали последующему преобразованию строки обратно в число.Число().Посмотрим на пример практической реализации данного подхода:
// Пример запроса с приведением числа к строковому формату на стороне источника
ТекстЗапроса =
"ВЫБРАТЬ
| ПРЕДСТАВЛЕНИЕ(Номенклатура.Цена) КАК ЦенаСтрокой
|ИЗ
| Справочник.Номенклатура КАК Номенклатура";
// Выполняем запрос через COM и считываем данные
Выборка = COMБаза.NewObject("Запрос");
Выборка.Текст = ТекстЗапроса;
РезультатВыборки = Выборка.Выполнить().Выбрать();
Пока РезультатВыборки.Следующий() Цикл
// Строковое значение передалось без потерь (например, "3.24")
СтрокаЦена = РезультатВыборки.ЦенаСтрокой;
// Преобразуем строку обратно в полноценное десятичное число
ТочнаяЦена = Число(СтрокаЦена);
// Теперь переменная ТочнаяЦена гарантированно содержит значение 3.24 с типом Число
КонецЦикла;
Данный метод полностью гарантирует сохранность исходных данных, однако он требует дополнительных вычислительных ресурсов на форматирование строк и их обратный парсинг на принимающей стороне.
---
Еще один оригинальный математический прием заключается в искусственном приведении дробных чисел к целым перед их отправкой. Поскольку целые числа передаются через COM без потери точности (в формате VT_I4), мы можем временно избавиться от дробной части.
Рассмотрим этот метод подробнее:
Посмотрим, как это выглядит в коде:
// Исходное дробное число в базе-источнике: 3.24
ЦенаВИсточнике = 3.24;
// Превращаем в целое число (масштабируем)
ЦелаяЦенаДляПередачи = ЦенаВИсточнике * 100; // Получаем ровно 324 (целое)
// На стороне приемника считываем целое число и возвращаем масштаб обратно
ПолученнаяЦена = ЦелаяЦенаДляПередачи / 100; // Получаем ровно 3.24 без искажений
Этот способ является очень быстрым и не требует строковых преобразований, однако он накладывает жесткие ограничения на знание разрядности данных обеими сторонами интеграционного процесса.
---
При работе с COM-соединениями существует серьезная архитектурная проблема, называемая межпроцессным «дребезгом» (chattering). Если вы выполняете запрос через COM-соединение и затем в цикле обходите тысячи строк выборки, считывая каждое свойство отдельно (например, Выборка.Наименование, Выборка.Цена), то при каждом обращении к свойству операционная система выполняет дорогостоящий межпроцессный вызов. Это приводит к катастрофическому падению скорости работы интеграции.
Наиболее современным, быстрым и надежным подходом является сериализация данных в формат JSON или XML на стороне базы-источника. Мы можем выгрузить весь результат запроса или таблицу значений в одну текстовую JSON-строку внутри базы-источника, передать эту строку за один единственный COM-вызов и десериализовать ее в базе-приемнике.
Поскольку форматы XML и JSON хранят числа исключительно в текстовом представлении, мы решаем одновременно две важнейшие задачи:
Давайте проанализируем пример кода, реализующий этот профессиональный подход:
// --- КОД НА СТОРОНЕ БАЗЫ-ИСТОЧНИКА ---
// Данная функция выполняется в базе-источнике и возвращает сериализованную строку JSON
Функция ПолучитьДанныеВФорматеJSON(ПараметрЗапроса) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.Цена КАК Цена
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Родитель = &Родитель";
Запрос.УстановитьПараметр("Родитель", ПараметрЗапроса);
ТаблицаДанных = Запрос.Выполнить().Выгрузить();
// Сериализуем таблицу значений в формат JSON
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ТаблицаДанных);
Возврат ЗаписьJSON.Закрыть(); // Возвращаем одну большую строку текста
КонецФункции
Теперь рассмотрим, как база-приемник вызывает эту функцию через COM-соединение и обрабатывает результат:
// --- КОД НА СТОРОНЕ БАЗЫ-ПРИЕМНИКА ---
COMСоединение = Новый COMОбъект("V83.COMConnector");
БазаИсточник = COMСоединение.Connect(ПараметрыПодключения);
// Вызываем функцию базы-источника (передается одна строка JSON за один вызов)
СтрокаДанныхJSON = БазаИсточник.ПолучитьДанныеВФорматеJSON(СсылкаГруппыВCOM);
// Десериализуем полученную строку в коллекцию 1С на стороне приемника
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(СтрокаДанныхJSON);
// Получаем массив структур, где все числа будут абсолютно точными!
ТаблицаРезультатов = ПрочитатьJSON(ЧтениеJSON);
ЧтениеJSON.Закрыть();
Для Каждого СтрокаТовара Из ТаблицаРезультатов Цикл
// СтрокаТовара.Цена содержит точное число, например 3.24, без каких-либо "хвостов"
Сообщить("Товар: " + СтрокаТовара.Наименование + " | Цена: " + СтрокаТовара.Цена);
КонецЦикла;
Использование JSON-сериализации для COM-соединений является отраслевым стандартом разработки. Оно гарантирует высочайшую скорость работы и полную защиту от любых проблем с типами данных.
---
Мы подробно разобрали природу возникновения «кривых» чисел при COM-интеграции и изучили лучшие практики борьбы с этим явлением. В качестве вывода дадим несколько полезных архитектурных рекомендаций:
Окр(Число, Точность) на стороне приемника.