Почему при чтении данных через COM-соединение в 1С появляются «кривые» числа и как это исправить?

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

При активном использовании интеграционных механизмов в среде «1С:Предприятие» разработчики часто сталкиваются с весьма странным поведением платформы. При чтении числовых данных из другой информационной базы посредством COM-соединения (OLE Automation) вместо ожидаемых точных значений, например, 3.24, система возвращает числа с бесконечным «хвостом» в дробной части — 3.239999999999991 или 3.240000000000002. На первый взгляд это кажется критической ошибкой самой платформы или СУБД, однако за этим поведением скрывается фундаментальная техническая особенность взаимодействия операционной системы Windows и COM-технологий.

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

Выясняем причину: разница в представлении чисел в 1С и COM

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

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

Во-вторых, проанализируем, что происходит в момент передачи данных через COM-соединение. В технологии OLE Automation для обмена примитивными типами данных используется универсальная системная структура, которая называется VARIANT. Платформа 1С обязана упаковать свое внутреннее число в один из типов, поддерживаемых этой структурой VARIANT:

  1. Если передаваемое число является целым и укладывается в стандартные границы 32-битного знакового целого числа (от -2 147 483 648 до 2 147 483 647), платформа 1С преобразует его в системный тип VT_I4. При этом передача данных происходит абсолютно точно, без малейших искажений.
  2. Если передаваемое число является дробным или выходит за пределы диапазона целых чисел, платформа 1С вынужденно конвертирует его в тип 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).

---

Способ 1. Классическое округление на стороне приемника

Самый очевидный и простой способ решения проблемы — это банальное принудительное округление полученного значения до требуемой разрядности непосредственно после его считывания из 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
КонецЕсли;

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

---

Способ 2. Конвертация чисел в строковый формат на стороне источника

Если мы хотим полностью исключить влияние двоичного формата VT_R8 на наши данные, мы можем передавать числа в текстовом виде. Символьные строки передаются через COM-соединение посредством типа VT_BSTR, который не подвержен никаким искажениям точности.

Разберем по шагам этот алгоритм:

  1. На стороне базы-источника при формировании запроса или обработке данных мы преобразуем число в строку, используя функцию Формат(). При этом крайне важно отключить группировку триад цифр (параметр ЧГ=0), чтобы разделители групп (например, пробелы в числе 1 000 000) не помешали последующему преобразованию строки обратно в число.
  2. Передаем полученную строку через COM-соединение.
  3. На стороне базы-приемника мы явно конвертируем полученную строку обратно в числовой тип с помощью функции Число().

Посмотрим на пример практической реализации данного подхода:


// Пример запроса с приведением числа к строковому формату на стороне источника
ТекстЗапроса = 
    "ВЫБРАТЬ
    |	ПРЕДСТАВЛЕНИЕ(Номенклатура.Цена) КАК ЦенаСтрокой
    |ИЗ
    |	Справочник.Номенклатура КАК Номенклатура";

// Выполняем запрос через COM и считываем данные
Выборка = COMБаза.NewObject("Запрос");
Выборка.Текст = ТекстЗапроса;
РезультатВыборки = Выборка.Выполнить().Выбрать();

Пока РезультатВыборки.Следующий() Цикл
    // Строковое значение передалось без потерь (например, "3.24")
    СтрокаЦена = РезультатВыборки.ЦенаСтрокой;
    
    // Преобразуем строку обратно в полноценное десятичное число
    ТочнаяЦена = Число(СтрокаЦена);
    
    // Теперь переменная ТочнаяЦена гарантированно содержит значение 3.24 с типом Число
КонецЦикла;

Данный метод полностью гарантирует сохранность исходных данных, однако он требует дополнительных вычислительных ресурсов на форматирование строк и их обратный парсинг на принимающей стороне.

---

Способ 3. Масштабирование (приведение к целому числу)

Еще один оригинальный математический прием заключается в искусственном приведении дробных чисел к целым перед их отправкой. Поскольку целые числа передаются через COM без потери точности (в формате VT_I4), мы можем временно избавиться от дробной части.

Рассмотрим этот метод подробнее:

  1. Нам необходимо заранее знать максимальное количество знаков после запятой у передаваемой величины (например, для денежных сумм это всегда 2 знака, для веса или объема может быть 3 или 5 знаков).
  2. Перед передачей данных мы умножаем дробное число на соответствующий множитель (100 для сумм, 1000 для веса с точностью до граммов), превращая его в целое число.
  3. Передаем целое число через COM.
  4. На стороне приемника делим полученное число на тот же самый множитель.

Посмотрим, как это выглядит в коде:


// Исходное дробное число в базе-источнике: 3.24
ЦенаВИсточнике = 3.24;

// Превращаем в целое число (масштабируем)
ЦелаяЦенаДляПередачи = ЦенаВИсточнике * 100; // Получаем ровно 324 (целое)

// На стороне приемника считываем целое число и возвращаем масштаб обратно
ПолученнаяЦена = ЦелаяЦенаДляПередачи / 100; // Получаем ровно 3.24 без искажений

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

---

Способ 4. Сериализация данных в XML или JSON (лучшая практика)

При работе с COM-соединениями существует серьезная архитектурная проблема, называемая межпроцессным «дребезгом» (chattering). Если вы выполняете запрос через COM-соединение и затем в цикле обходите тысячи строк выборки, считывая каждое свойство отдельно (например, Выборка.Наименование, Выборка.Цена), то при каждом обращении к свойству операционная система выполняет дорогостоящий межпроцессный вызов. Это приводит к катастрофическому падению скорости работы интеграции.

Наиболее современным, быстрым и надежным подходом является сериализация данных в формат JSON или XML на стороне базы-источника. Мы можем выгрузить весь результат запроса или таблицу значений в одну текстовую JSON-строку внутри базы-источника, передать эту строку за один единственный COM-вызов и десериализовать ее в базе-приемнике.

Поскольку форматы XML и JSON хранят числа исключительно в текстовом представлении, мы решаем одновременно две важнейшие задачи:

  1. Полностью исключаем любые погрешности округления и искажения точности чисел, так как передача идет в текстовом виде.
  2. Увеличиваем скорость передачи данных в десятки и сотни раз, минимизируя количество обращений через границу COM-процесса.

Давайте проанализируем пример кода, реализующий этот профессиональный подход:


// --- КОД НА СТОРОНЕ БАЗЫ-ИСТОЧНИКА ---
// Данная функция выполняется в базе-источнике и возвращает сериализованную строку 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-интеграции и изучили лучшие практики борьбы с этим явлением. В качестве вывода дадим несколько полезных архитектурных рекомендаций:

  1. Если вам нужно быстро исправить ошибку в существующем коде с минимальными трудозатратами — используйте явное округление функций Окр(Число, Точность) на стороне приемника.
  2. Если вы проектируете новую систему интеграции через COM — никогда не передавайте данные «построчно» через выборки запросов. Сразу закладывайте передачу данных пакетами через JSON или XML. Это сбережет ваши нервы, сохранит точность чисел и кратно ускорит работу системы.
  3. В долгосрочной перспективе старайтесь полностью отказываться от COM-технологий. COM жестко привязан к операционной системе Windows, требователен к версиям библиотек 1С на сервере и склонен к утечкам оперативной памяти. Переводите интеграции на современные Web-сервисы (SOAP) или HTTP-сервисы (REST API), которые работают по текстовым протоколам и лишены детских болезней старых COM-интерфейсов.
← На главную