Одна и та же строка дает 12,5 или 125 — и база тут не виновата
Разработка - Механизмы платформы 1С
Есть присваивание, в котором тип не совпадает:
СтрокаТаблицы.Сумма = "12,5"; // колонка объявлена как Число(10,2)
Платформа не падает. Она молча приводит значение к типу приемника и работает дальше. Это известно и обычно воспринимается как удобство: не надо писать Число() руками.
Удобство выходит боком, и вот насколько. Один и тот же код, одна и та же база, одна и та же таблица. Разница только в том, из какого сеанса код запущен:
| Что кладем в колонку Число(10,2) | Сеанс первый | Сеанс второй |
|---|---|---|
строку "12,5" |
12,5 | 125 |
Не ошибка, не исключение, не предупреждение. Просто в одном случае двенадцать с половиной, а в другом сто двадцать пять. Если это сумма документа, разница в десять раз уедет в проводки. Как, по-вашему, такое ловится до того, как уедет?
Ниже — откуда это берется, полная таблица того, что платформа делает с несовпадающим типом, и почему привычный способ проверить локализацию базы тут не помогает.
Как замерялось
Бухгалтерия 3.0.206, платформа 8.5, прогоны 01.10.2026. Таблица значений с тремя типизированными колонками: Число(10,2), Дата, Строка(100). В каждую по очереди кладется значение не того типа, и сразу после присваивания читается, что в ней оказалось:
Таблица.Колонки.Добавить("Чис", Новый ОписаниеТипов("Число",
Новый КвалификаторыЧисла(10, 2)));
СтрокаТЗ = Таблица.Добавить();
...
СтрокаТЗ.Чис = Значение;
Возврат "тип " + ТипЗнч(СтрокаТЗ.Чис) + " значение [" + Строка(СтрокаТЗ.Чис) + "]";
Читать надо именно сразу. Если присвоить десять значений подряд и посмотреть в конце, увидишь только последнее — я на этом сначала и сел, и из первого прогона половину выводов пришлось выкинуть.
Два сеанса: через внешнее соединение и через веб-клиент. Код между ними не менялся ни на символ — в обоих случаях исполнялся один и тот же модуль одной и той же собранной обработки. Это важно, потому что первая версия замера сравнивала реквизит формы в одном сеансе с колонкой таблицы в другом, и тогда менялись сразу две вещи. Такое сравнение ничего не доказывает, и его пришлось переделать.
Полная таблица
Русская локализация базы, сеанс внешнего соединения:
| Кладем | Получаем | Комментарий |
|---|---|---|
"abc" |
0 | тихо, без исключения |
"12,5" |
12,5 | |
"12.5" |
12,5 | точка тоже понимается |
" 7 " |
7 | пробелы по краям срезаются |
"1 000" |
0 | пробел внутри убивает все число |
Истина |
1 | |
Неопределено |
0 | |
12.345 |
12,35 | округлено до двух знаков по квалификатору |
99999999999 |
99 999 999,99 | обрезано по разрядности колонки |
Три строки тут заслуживают отдельного взгляда.
"1 000" дает ноль, а не тысячу. Пробел как разделитель разрядов платформа не понимает, и в отличие от "abc" эта строка выглядит как совершенно нормальное число. Именно так приходят суммы из выгрузок, из скопированного в буфер и из того, что пользователь набрал руками.
99999999999 превращается в 99 999 999,99. Не ошибка «не лезет», не ноль, а максимум, который влезает в Число(10,2). То есть значение подменяется на похожее и правдоподобное — худший из возможных вариантов, потому что такое число пройдет любую проверку на пустоту и на знак.
12.345 становится 12,35. Округление по квалификатору тоже молчаливое. Для суммы это обычно и нужно, для коэффициента или курса — почти никогда.
Для даты и строки короче:
Кладем в Дата |
Получаем |
|---|---|
0 |
пустая дата |
"нетдаты" |
пустая дата |
Неопределено |
пустая дата |
А вот со строкой есть отдельная засада. Число 1000000, положенное в Строка(100), становится не 1000000, а 1 000 000 — с разделителями разрядов. Коды символов в русском сеансе: 49, 160, 48, 48, 48, 160, 48, 48, 48. Сто шестидесятый — неразрывный пробел. Такую строку не сравнить с той, что пришла из файла, и Число() ее обратно не возьмет.
Тихо — это не то же, что мягко
Самое неприятное выясняется, если положить рядом неявное приведение и явный вызов:
неявно: СтрокаТЗ.Чис = "abc" -> 0
явно: Число("abc") -> Преобразование значения
к типу Число не может быть выполнено
Одна и та же задача, одни и те же данные, и ровно противоположное поведение. Явный вызов кричит, неявное присваивание молчит и кладет ноль.
Отсюда следствие, которое стоит проговорить. Код с Число(ЗначениеИзФайла) упадет на мусоре и заставит разобраться. Код с Реквизит = ЗначениеИзФайла на том же мусоре отработает штатно и запишет ноль. Второй вариант короче, выглядит аккуратнее и в обзоре кода не вызывает вопросов.
А исключение где?
Нигде. За все пробы приведения не выбросилось ни одного исключения: все строки таблицы отчитались словом «прошло». Это и есть вся проблема в одной строке.
Теперь про сеанс
Возвращаюсь к тому, с чего начал. Два прогона, один и тот же модуль:
| внешнее соединение | веб-клиент | |
|---|---|---|
КодЛокализацииИнформационнойБазы() |
ru_RU |
ru_RU |
как сеанс печатает 12.5 |
12,5 |
12.5 |
как сеанс печатает 1000000 |
1 000 000 |
1,000,000 |
"12,5" в колонку Число |
12,5 | 125 |
"12.5" в колонку Число |
12,5 | 12.5 |
Число("12,5") |
12,5 | 125 |
| коды символов у строки из 1000000 | 49 160 48 48 48 160 48 48 48 | 49 44 48 48 48 44 48 48 48 |
Разбор строки идет по региональным настройкам сеанса. В сеансе, где разделитель дробной части точка, запятая понимается как разделитель разрядов: "12,5" читается как «двенадцать тысяч пять», лишние разряды отбрасываются, остается 125.
И вот главное для практики: КодЛокализацииИнформационнойБазы() в обоих прогонах вернул ru_RU. По нему разницу не увидеть, потому что он про базу, а не про сеанс. База одна, а поведение разное.
Веб-клиент в этом замере шел с английскими региональными настройками. Откуда они взялись, я измерял не напрямую: вероятнее всего, такой язык запросил браузер, но прямой пробы с другим языком браузера у меня пока нет, так что берите это как объяснение, а не как факт. Измерено другое, и этого достаточно: разделитель в веб-сеансе оказался точкой при русской локализации базы. То есть одного пользователя с иначе настроенным сеансом хватает, чтобы тот же код в той же базе начал читать суммы по-другому.
Про Новый Тип(...).Метод()
Заодно в этом же прогоне проверилась конструкция, которую легко написать по привычке из других языков:
Р = Новый Файл("C:\").Существует();
// {<Неизвестный модуль>(1,23)}: Неопознанный оператор
Обратиться к методу прямо на выражении Новый нельзя. Нужна промежуточная переменная, и тогда все работает:
Ф = Новый Файл("C:\");
Р = Ф.Существует();
Рисунок одинаковый на любом типе: Новый Массив().Количество(), Новый Структура("А", 1).Количество(), Новый ТаблицаЗначений().Количество() — все «Неопознанный оператор». Скобки не спасают: (Новый Массив).Количество() дает ту же ошибку. А если скобок после типа не поставить вовсе, ошибка меняется на «Тип не определен (Массив.Количество)» — разборщик читает все выражение как одно имя типа.
Отдельно рядом стоит СистемнаяИнформация. Это тип, а не функция, поэтому СистемнаяИнформация().ВерсияПриложения дает «Процедура или функция с указанным именем не определена». Правильно так же, через переменную.
Опасность тут не в самой ошибке — она ловится первой же сборкой. Опасность в выводе, который из нее делают. У меня в практике было так: конструкция не компилировалась в клиентском коде, из этого был сделан вывод, что синхронная работа с файлами на клиенте запрещена, и рабочая проверка файла удалена из состава продукта. Проверка гипотезы заняла одну минуту: в типовых конфигурациях стоит разрешение синхронных вызовов, никакого запрета не было. Выкинули работающий код из-за пропущенной промежуточной переменной.
Где это уже лежит в вашем коде
Тихое приведение не оставляет следов, поэтому искать надо не ошибку, а место, где она возможна. Мест таких немного, и все они узнаются по виду.
Присваивание в типизированный приемник значения, пришедшего извне. Реквизит объекта, реквизит формы, колонка таблицы с заданным типом, параметр запроса — слева тип задан жестко, справа стоит то, что принесли. Если справа строка из файла или из поля ввода, приведение состоится молча.
Заполнение по имени. ЗаполнитьЗначенияСвойств, Структура в цикле, обход колонок строкой — все это переносит значения без сверки типов, и ошибка уезжает на шаг дальше от того места, где ее видно.
Арифметика поверх приведенного значения. Ноль, полученный из "1 000", в сумме не заметен. А вот деление на такой ноль заметно сразу, и именно по этому падению чаще всего и находят первопричину — через неделю после того, как она появилась.
Мы для себя сделали из этого одно правило: если значение пришло не из базы, оно разбирается явно, и у разбора есть ветка отказа. Длиннее на три строки, зато отказ виден.
Функция ЧислоИзСтроки(Текст, Отказ) // Разделитель дробной части берем у самого сеанса, а не предполагаем. Разделитель = Сред(Строка(1.1), 2, 1); Очищенный = СтрЗаменить(СокрЛП(Текст), Символ(160), ""); Очищенный = СтрЗаменить(Очищенный, " ", ""); // Если в строке и точка, и запятая - это не число, а чья-то выдумка. Если СтрНайти(Очищенный, ",") > 0 И СтрНайти(Очищенный, ".") > 0 Тогда Отказ = Истина; Возврат 0; КонецЕсли; Очищенный = СтрЗаменить(Очищенный, ".", Разделитель); Очищенный = СтрЗаменить(Очищенный, ",", Разделитель); Попытка Возврат Число(Очищенный); Исключение Отказ = Истина; Возврат 0; КонецПопытки; КонецФункции
Замены тут не для красоты. Неразрывный пробел с кодом 160 приезжает из строк, которые сама платформа и напечатала. Обычный пробел — из выгрузок и из буфера. А разделитель берется у сеанса через Сред(Строка(1.1), 2, 1) — платформа сама печатает одну десятую так, как принято в этом сеансе, и второй символ и есть нужный знак.
В обоих сеансах числа совпали до знака:
| Строка на входе | Что вышло | |
|---|---|---|
12,5 и 12.5 |
12,5 | оба написания |
1 000 и то же с неразрывным пробелом |
1000 | |
-3,25 и -3.25 |
-3,25 | |
abc, пустая строка |
0 | отказ |
12,5,7, 12.5,7 |
0 | отказ |
1,000,000 |
0 | отказ, и это нарочно |
Последнюю строку поясню. Число с запятыми-разделителями разрядов функция отвергает, хотя могла бы угадать тысячу. Угадывать она не должна: ровно это угадывание и дает 125 вместо 12,5. Лучше отказ, который видно.
А первая версия этой функции была сломана
Писать такое неприятно, но без этого статья будет нечестной.
В первой редакции листинг стоял такой: убрать пробелы, заменить точку на запятую, вызвать Число(). Логика очевидная — привести к русскому разделителю и разобрать. Я так и написал, и формулировка в статье была уверенная: «после такой замены строка читается одинаково в любом сеансе».
Прогон показал обратное. В сеансе с точечным разделителем эта функция берет правильное "12.5", превращает в "12,5" и получает 125. То есть она портит ровно то, что должна была починить, и портит молча — отказа нет, значение «принято». Строку "12,5,7" первая версия тоже приняла, вернув 1257.
Урок из этого ровно тот же, что и у всей статьи. Код, который приводит типы по предположению о разделителе, работает до первого сеанса с другими настройками. Разница только в том, что платформа делает это в одну строку и бесплатно, а я сделал то же самое в пять строк и с комментарием, объясняющим, почему это надежно.
Чего этот замер не доказывает
Два ограничения назову прямо, чтобы на статью не ссылались шире, чем она проверена.
Первое. Разница между сеансами измерена, а вот причина ее названа как объяснение, а не как измеренный факт. Я говорю, что дело в региональных настройках сеанса, и показываю, чем они отличаются, но не ставил опыт, где меняется только язык, а все остальное остается прежним. Пока такой пробы нет, честная формулировка звучит так: сеансы различались вот этим, а не причина в этом.
Второе. Все снято на двух типах сеанса. Если у вас третий набор региональных настроек, картина может оказаться иной, и утверждать про нее мне нечем.
Что с этим делать
Коротко, по убыванию пользы.
Не полагаться на неявное приведение там, где данные пришли извне. Файл, буфер, веб-сервис, ручной ввод — все это строки неизвестного качества. Явный разбор с обработкой отказа длиннее на три строки и отличается тем, что про неудачу вы узнаете.
Проверять не базу, а сеанс. Если поведение зависит от региональных настроек, смотреть надо на то, как сеанс печатает число, а не на код локализации базы. Самая короткая проба — Строка(12.5): увидели точку, значит разделитель дробной части точка, и запятая в строках будет понята как разряды.
Разбирать числа не по умолчанию, а по формату. Там, где формат источника известен заранее, он и должен быть задан явно, а не унаследован от того, кто запустил обработку.
Проверять «ограничение платформы», прежде чем под него подстраиваться. Это не про числа, но урок из того же прогона. Ошибка компиляции с правдоподобным текстом — плохой повод сокращать состав продукта.
Все замеры от 01.10.2026 приложены. Если у вас есть сеанс с третьим набором региональных настроек — скажите, что он делает с "12,5", интересно, сходится ли картина.
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.