При разработке на платформе 1С:Предприятие 7.7 (и даже в более современных версиях 8.x) программисты часто сталкиваются с необходимостью быстро сравнить два сложных объекта, таких как ТаблицаЗначений. Самым очевидным и быстрым способом кажется использование встроенной функции ЗначениеВСтрокуВнутр (или ее аналогов). Логика проста: если содержимое таблиц идентично, то и их строковое представление должно быть одинаковым. Однако на практике этот метод превращается в «грабли», так как платформа не гарантирует повторяемость (детерминированность) результата сериализации для одного и того же набора данных.
В этой статье мы подробно разберем, почему встроенные механизмы сериализации ведут себя именно так, проанализируем внутреннюю структуру хранения данных в 1С и рассмотрим надежные способы сравнения таблиц значений.
Разберем ситуацию: мы имеем две таблицы значений с абсолютно одинаковыми данными. Мы вызываем функцию ЗначениеВСтрокуВнутр для каждой из них и сравниваем полученные строки. К нашему удивлению, строки оказываются разными. Почему это происходит?
Дело в том, что внутренняя реализация объекта ТаблицаЗначений в платформе, написанной на C++, оптимизирована для работы с памятью, а не для текстового сравнения. Данные в таблице хранятся по колонкам (Column-oriented storage). Когда мы добавляем, удаляем или перемещаем строки, физическое расположение данных в блоках памяти может меняться.
При вызове ЗначениеВСтрокуВнутр платформа выгружает данные колонок в том порядке, в котором они фактически «лежат» в оперативной памяти в данный момент. Чтобы при десериализации (обратной функции ЗначениеИзСтрокиВнутр) восстановить правильный логический порядок строк, система дописывает в конец сериализованной строки специальный служебный тег — карту перестановки (индексный массив).
Проанализируем структуру такой строки. Она выглядит примерно так:
Поскольку физический порядок данных в памяти зависит от истории манипуляций с объектом (были ли удаления строк, как выделялась память менеджером C++), блоки данных в строке могут меняться местами, а карта перестановки будет это компенсировать. Для восстановления объекта это не проблема, но для прямого сравнения строк — это катастрофа.
Для разработчиков на 1С 7.7 самым эффективным способом решения проблемы является использование возможностей библиотеки 1С++. В этой библиотеке реализован объект ИндексированнаяТаблица, который лишен многих недостатков штатной таблицы значений.
Объект ИндексированнаяТаблица имеет собственные методы сериализации ВСтроку() и ИзСтроки(). В отличие от платформенных функций, эти методы обеспечивают детерминированный результат. Если данные в двух объектах ИндексированнаяТаблица совпадают, то и строки, полученные через ВСтроку(), будут идентичны. Это позволяет использовать их для быстрого кэширования или проверки изменений.
Если использование внешних компонентов невозможно, нам придется написать собственную функцию для формирования «эталонной» строки. Основная идея заключается в том, чтобы обходить таблицу строго последовательно — по строкам и колонкам, преобразуя каждое значение в строковый вид.
Важно помнить, что простое использование функции Строка() для объектов (справочников, документов) может быть недостаточно, так как представление объекта (наименование) может быть одинаковым у разных элементов. Для надежности нужно использовать внутренний идентификатор. Разберем пример реализации такой функции на языке 1С 7.7:
Функция ПолучитьХешТаблицы(ТЗ)
Перем РезСтрока, Кол, Стр, ТекЗнач;
РезСтрока = "";
// Перебираем строки в логическом порядке
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
// Перебираем колонки по их индексам
Для Кол = 1 По ТЗ.КоличествоКолонок() Цикл
ТекЗнач = ТЗ.ПолучитьЗначение(ТЗ.НомерСтроки, Кол);
// Для ссылочных типов берем внутренний идентификатор
Если ТипЗначенияСтр(ТекЗнач) = "Справочник" Тогда
РезСтрока = РезСтрока + ЗначениеВСтрокуВнутр(ТекЗнач);
ИначеЕсли ТипЗначенияСтр(ТекЗнач) = "Документ" Тогда
РезСтрока = РезСтрока + ЗначениеВСтрокуВнутр(ТекЗнач);
Иначе
// Для примитивных типов достаточно обычного представления
РезСтрока = РезСтрока + Строка(ТекЗнач);
КонецЕсли;
РезСтрока = РезСтрока + "|"; // Разделитель колонок
КонецЦикла;
РезСтрока = РезСтрока + РазделительСтрок; // Разделитель строк
КонецЦикла;
Возврат РезСтрока;
КонецФункции
Этот метод гарантирует, что если таблицы логически равны, то и результат функции будет одинаковым. Однако стоит учитывать, что для очень больших таблиц такая конкатенация строк может работать медленно. В этом случае вместо накопления огромной строки целесообразно вычислять хеш-сумму (например, MD5 или CRC32) прямо в процессе обхода.
Существует еще один «хитрый» способ, позволяющий попытаться использовать штатную ЗначениеВСтрокуВнутр. Проанализируем ситуацию: если данные в памяти лежат хаотично из-за удалений и вставок, нужно заставить платформу пересобрать объект «с нуля».
Для этого можно создать новую пустую таблицу значений с такой же структурой колонок и скопировать в нее данные из исходной таблицы методом Выгрузить(). При выполнении операции Выгрузить платформа создает новый объект и последовательно заполняет его данными. В большинстве случаев это приводит к тому, что физический порядок данных в памяти становится линейным, а карта перестановки в строке сериализации становится пустой или стандартной.
Важное предупреждение: хотя этот метод часто работает, он не является документированным поведением платформы. Полагаться на него в критически важных узлах системы (например, при синхронизации данных между распределенными базами) не рекомендуется.
В современной платформе 8.3 проблема недетерминированности ЗначениеВСтрокуВнутр никуда не исчезла. Более того, сам этот метод официально объявлен устаревшим и предназначен только для служебных целей разработчиков платформы.
Для детерминированного сравнения объектов в 8-й версии рекомендуется использовать:
СериализаторXDTO для ТаблицыЗначений строки выгружаются последовательно.
Функция ТаблицыИдентичны(ТЗ1, ТЗ2)
// Сначала проверяем базовые параметры
Если ТЗ1.Количество() <> ТЗ2.Количество() Или ТЗ1.Колонки.Количество() <> ТЗ2.Колонки.Количество() Тогда
Возврат Ложь;
КонецЕсли;
// Проверяем состав колонок
Для Индекс = 0 По ТЗ1.Колонки.Количество() - 1 Цикл
Если ТЗ1.Колонки[Индекс].Имя <> ТЗ2.Колонки[Индекс].Имя Тогда
Возврат Ложь;
КонецЕсли;
КонецЦикла;
// Построчное сравнение
Для Индекс = 0 По ТЗ1.Количество() - 1 Цикл
Стр1 = ТЗ1[Индекс];
Стр2 = ТЗ2[Индекс];
Для Каждого Колонка Из ТЗ1.Колонки Цикл
Если Стр1[Колонка.Имя] <> Стр2[Колонка.Имя] Тогда
Возврат Ложь;
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат Истина;
КонецФункции
Подведем итоги нашего анализа. Главный вывод: никогда не используйте результат ЗначениеВСтрокуВнутр для сравнения объектов на равенство. Эта функция гарантирует только то, что вы сможете восстановить объект из этой строки, но не гарантирует уникальность самой строки.
Для надежной работы следуйте этим правилам:
Число, Строка или Ссылка в памяти происходит мгновенно.Свернуть()) и сравнивайте итоги по важным колонкам, но помните, что это не гарантирует идентичность состава строк.Понимание внутренних механизмов работы платформы позволяет избегать трудноуловимых багов, которые могут проявляться случайным образом в зависимости от состояния оперативной памяти сервера.