Ориентация главы
Взаимные превращения чисел и текста кажутся мелочью, на деле это зона частых аварий: atoi на нечисловом входе молча возвращает 0, strtoll «разбирает, сколько получится» и оставляет полуконечный мусор, %.17g выводит избыточно длинное, а %.6f теряет точность, граница INT64_MIN ломает реализации «сперва в положительное, потом отрицание». Модуль number библиотеки XRT отвечает на эти проблемы тремя решениями: строгий разбор (весь вход легален — или отказ целиком; снисходительность включается явно флагами), кратчайший круговой проход (вывод числа с плавающей точкой несёт ровно столько цифр, чтобы разбор вернул исходное значение) и строки форматирования (стиль Python — группировка, система счисления, проценты объявляются разом). Глава разбирает все три линии; заодно разворачивается идея тестирования граничных значений — зеркало «внедрения отказов» главы 6 в мире чисел: и то и другое сознательно создаёт экстремальные условия и проверяет, надёжен ли результат вне маленьких данных.
26.1引入
Три места аварии. Первое: в конфигурационном файле порт записан как "8080x" — atoi возвращает 8080, хвостовой x молча отброшен, сервис поднялся на «правдоподобном» порту. Второе: числа с плавающей точкой в журнале печатаются %.6f, при чтении теряются три разряда, регрессионное сравнение не проходит никогда; смена на %.17g даёт 0.10000000000000001 — то, что никто не хочет читать. Третье: разобрать " -1_234.567_890e-2 " — краевые пробелы, группировка цифр подчёркиванием, научная нотация: тройка, на которую стандартная библиотека не согласна ни в чём, а ручная реализация съедает вечер.
Общий корень всех трёх сцен: функции преобразования стандартной библиотеки родились в снисходительную эпоху и сделали «терпимость к ошибкам» поведением по умолчанию — инженерии же нужно ровно обратное: строгость по умолчанию, снисходительность явно. Функции разбора XRT не останавливаются на полпути: значение возвращается, только если входное представление легально целиком; любая примесь — отказ с точной категорией в слоте ошибки; чтобы разрешить пробелы и подчёркивания — объявите это флагами явно.
26.2概念
Строгий разбор: всё или отказ
Строгость xrtIntParse(视图, 基数, 标志, 出参) (представление, основание, флаги, выходной параметр) трёхслойна: ни краевых пробелов (кроме XNUMBER_PARSE_SPACE), ни подчёркиваний внутри цифр (кроме XNUMBER_PARSE_SEPARATOR), успех — только при полном потреблении входа — "8080x" выбывает уже на первом слое. Версия для чисел с плавающей точкой xrtNumParse разделяет ту же семантику, добавляя явную обработку научной нотации и специальных значений (inf/nan). Дисциплина граничных значений заслуживает отдельного акцента: в тесты ставьте INT64_MIN — любая реализация «сперва перевести в положительное, потом отрицать» ошибается именно здесь (положительное 9223372036854775808 переполняется); проходит только накопление в отрицательном на всём пути. Запись граничных значений в тесты — жёсткое требование упражнений главы.
Кратчайший круговой проход: фактический стандарт сериализации чисел с плавающей точкой
xrtNumString(值, 精度) (значение, точность) по умолчанию (точность 0) выводит кратчайшее круговое представление: ровно столько десятичных цифр, чтобы Parse вернул double, побитово равный исходному. Оно куда короче %.17g (0.1 выводится как 0.1, а не 0.10000000000000001) и, в отличие от %.6f, ничего не теряет — контур сериализация-десериализация замыкается с нулевой ошибкой. Когда задавать точность руками? При показе человеку (два знака в отчёте); машинный круговой проход — всегда кратчайший.
Строки форматирования: все пожелания объявляются разом
| Формат | Эффект | Эквивалент в стандартной библиотеке |
|---|---|---|
,d | десятичное с разделителями тысяч: -123,456,789 | нет (цикл вручную) |
#_X | префикс 0X + группировка подчёркиваниями + верхний регистр hex | комбинация нескольких флагов |
,.2f | тысячи + два знака после точки | нет |
.1% | проценты (авто ×100 и знак %) | посчитать руками и склеить |
c | кодовая точка целого как символ UTF-8 | конвертация wctomb |
Три входа xrtIntFormat / xrtUIntFormat / xrtNumFormat покрывают знаковые, беззнаковые и плавающие; продукты все владеющие (освобождение xrtFree). Стиль строк форматирования повторяет спецификацию format в Python — писавшие на Python платят ноль стоимости обучения, остальным всё равно спокойнее, чем запоминать, какие из %08X и %'d printf на какой платформе поддерживаются.
Разделение труда с семейством printf
xrtFormat (из главы 2) и семейство Format этой главы не конкуренты: первый собирает произвольную смешанную строку («заказ %s, %d штук»), вторые специализируются на показательных форматах чисел (тысячи, система счисления, проценты). Правило большого пальца: текстовые шаблоны — Format, показ чисел — семейство Format этой главы, оба возвращают владеющие продукты и оба принимают подмножество синтаксиса в стиле printf (с отказом от %n — дисциплина безопасности из главы 4).
26.3示例
Полная программа: строгий разбор и вывод в нескольких системах счисления
Из образца репозитория examples/number/integer/main.c — двойная проверка на граничном INT64_MIN:
*/ #include <stdio.h> #include <xrt.h> int main(void) { int64 iValue; str sDecimal; str sHex; */ if ( !xrtIntParse(XRT_STR_LITERAL(" -9_223_372_036_854_775_808 "), 10, (uint32)XNUMBER_PARSE_SPACE | (uint32)XNUMBER_PARSE_SEPARATOR, &iValue) ) { return 1; } /* 十进制原样输出;十六进制加 0X 前缀并大写。 */ sDecimal = xrtIntString(iValue, 10, 0); sHex = xrtIntString(iValue, 16, (uint32)XNUMBER_PREFIX | (uint32)XNUMBER_UPPER); if ( (sDecimal == NULL) || (sHex == NULL) ) { xrtFree(sHex); xrtFree(sDecimal); return 1; } printf("%s\n%s\n", sDecimal, sHex); xrtFree(sHex); xrtFree(sDecimal); return 0; }
Что только что произошло. (1) Вход разбора — десятичная запись INT64_MIN; образец двумя флагами явно разрешил пробелы и подчёркивания (и те и другие есть во входе) — снисходительность прописана чёрным по белому. (2) Эмпирическое доказательство корректности границ: разобрано -9223372036854775808 и отозвано обратно — будь реализация «сперва в положительное, потом отрицание», положительная сторона переполнилась бы на 9223372036854775808 и результат разошёлся бы на единицу; этот вывод и есть сигнал (не)соответствия. (3) xrtIntString выводит в системе счисления 16 с префиксом 0X и верхним регистром — формат выхода объявлен флагами, префикс не собирается руками. Сломать этот образец нарочно — часть упражнений главы: уберите флаги разбора, подайте вход с примесями и наблюдайте строгий отказ вместо молчаливого усечения.
Полная программа: всё семейство форматирования
Из examples/number/format/main.c — пять строк форматирования одной витриной:
*/ #include <stdio.h> #include <xrt.h> int main(void) { str sInteger = xrtIntFormat( INT64_C(-123456789), XRT_STR_LITERAL(",d")); str sHex = xrtUIntFormat( UINT64_C(0xDEADBEEF), XRT_STR_LITERAL("#_X")); str sFloat = xrtNumFormat( 1234567.895, XRT_STR_LITERAL(",.2f")); str sPercent = xrtNumFormat( 0.125, XRT_STR_LITERAL(".1%")); str sCharacter = xrtIntFormat( 20320, XRT_STR_LITERAL("c")); /* 任何一个失败都统一走清理(xrtFree 允许 NULL)。 */ if ( (sInteger == NULL) || (sHex == NULL) || (sFloat == NULL) || (sPercent == NULL) || (sCharacter == NULL) ) { xrtFree(sInteger); xrtFree(sHex); xrtFree(sFloat); xrtFree(sPercent); xrtFree(sCharacter); return 1; } printf("%s\n%s\n%s\n%s\n%s\n", sInteger, sHex, sFloat, sPercent, sCharacter); xrtFree(sInteger); xrtFree(sHex); xrtFree(sFloat); xrtFree(sPercent); xrtFree(sCharacter); return 0; }
Что только что произошло. (1) Целое с разделителями тысяч — стандарт читаемости отчётов и журналов; стандартной библиотеке нужен рукописный цикл вставки запятых, здесь это один ,d. (2) Hex с группировкой подчёркиваниями — отладочный стиль 0XDEAD_BEEF: префикс, группировка и верхний регистр — три пожелания одной строкой формата. (3)(4) Фиксированная точка и проценты — обратите внимание: проценты это «авто ×100 плюс %», вход 0.125 даёт 12.5%, без ручной арифметики и ручной склейки. (5) Рендеринг символа: целое 20320 по кодовой точке Unicode выводится как символ UTF-8 в пятой строке — первое знакомство с превращением кодовая точка ↔ символ у семейства наборов символов (глава 27). Все пять строк вывода — владеющие продукты, каждая освобождается своим xrtFree.
26.4契约
- Строгость по умолчанию: успех только при лёгком целиком входе; снисходительность (пробелы/подчёркивания) включается явно флагами; отказ ставит ошибку и не пишет выход.
- Точность на границах: граничные значения вроде INT64_MIN/UINT64_MAX разбираются точно; в реализации нет пути «сперва в положительное».
- Кратчайший круговой проход: точность
xrtNumStringпо умолчанию и есть кратчайший круговой проход; машинная сериализация никогда не задаёт точность руками. - Владеющие продукты: семейства String/Format возвращают
str, ровно одно освобождениеxrtFree. - Явные системы счисления: основание разбора и система счисления/префикс вывода объявляются параметрами/флагами — без угадываний и умолчаний в пользу шестнадцатеричной.
От примеров к инженерии: три хозяина функций разбора
Три способа крепления функций разбора в инженерии определяют стратегию обработки ошибок. Валидация на входе: загрузка конфигурации, декодирование полей протокола вызывают разбор на границе системы — отказ отвергает весь вход, а ошибка несёт контекст «какое поле, какая примесь» (звёздный час форматирования ошибок, глава 4). Преобразование на горячем пути: извлечение чисел, исполняющееся на каждый запрос, — строгость покупает определённость «отказ означает баг»; горячему пути не нужны ветки подстраховки. Форматирование слоя показа: семейство Format смотрит в человеческие глаза (журналы, отчёты, сообщения об ошибках) — тысячи и проценты живут здесь, а межмашинная передача ходит кратчайшим круговым проходом, не строками формата. Три хозяина отвечают трём позам «граница отвергает, путь утверждает, показ форматирует» — неправильная поза встречается чаще неправильной функции.
Взгляд на тестирование: граничные значения прежде охвата
Повторное появление INT64_MIN в этой главе не совпадение — оно демонстрирует взгляд на тесты: выбор граничных значений важнее количества случаев. Контрольный список границ для числовых преобразований: экстремумы каждого типа, ноль и минус ноль, ведущие нули, одиночный символ, пустая строка, одинокий знак, строка цифр максимальной длины — пятнадцать отобранных границ убивают девять десятых дефектов реализации, тогда как десять тысяч случайных значений могут не задеть ни одной. Эта идея вернётся в главе 6 (выбор точек внедрения FailAfter) и в главе 30 (числовые границы JSON).
Типичные ошибки
Симптом: после переезда с кода, который «всегда работал», на разбор XRT та же пачка конфигураций вдруг сыпется отказами — ошибки концентрируются на полях с пробелами и примесями.
Причина: снисходительное поведение стандартной библиотеки (пропуск пробелов, разбор «сколько получится») приняли за контракт; строгий разбор превращает эти проблемы из «молчаливых неверных значений» в «явные отказы» — отказ не регрессия, а баг, который был там всегда и впервые попал в поле зрения.
int iPort = atoi(sConfig); /* "8080x" -> 8080, "x" молча отброшен */
int64 iPort; if ( !xrtIntParse((xstrview){ sConfig, strlen(sConfig) }, 10, 0, &iPort) ) { ReportConfigError(sConfig); /* "8080x" отказывается прямо здесь - примеса увидена */ return false; }
Симптом: проверка контура сериализация-десериализация изредка падает; или числа в журнале превращаются в семнадцатизначный «шум точности».
Причина: ручная точность — яма с двух концов: слишком коротко — теряется точность (круговое сравнение больше не сходится), слишком длинно — шум на выходе (судьба .17g).
char sBuf[32]; snprintf(sBuf, sizeof(sBuf), "%.17g", fValue); /* 0.1 -> 0.10000000000000001 */
str sText = xrtNumString(fValue, 0); /* кратчайший круговой проход: 0.1 остаётся 0.1 */ /* ... используем sText ... */ xrtFree(sText);
Практические задания
Разберите пять входов "42", " 42", "4_2", "42x", "" с флагами по умолчанию и с включёнными — запишите десять результатов: сперва в столбик ожидания (какие успешны и с каким значением, какие отказ), потом прогон и сверка; к следующему упражнению — только когда все десять сошлись.
Напишите набор граничных тестов для xrtIntParse: INT64_MIN/MAX, UINT64_MAX, -0, ведущие нули, одиночное подчёркивание, одиночный пробел — точный ожидаемый результат на каждый вход. Подсказка: это миниатюра тестов репозитория; выбор границ важнее количества утверждений.
Реализуйте read_int(配置文本, 键) (текст конфигурации, ключ): разрезка по строкам (глава 25), Cut на ключ и значение, значение — через строгий разбор (пробелы разрешены, подчёркивания нет), ключ — без учёта регистра. Критерии приёмки: пять видов уродливых входов (примесь, выход за диапазон, пустое значение, отсутствующий ключ, дублирующийся ключ) дают различимые отчёты об ошибках; значения INT64_MIN/MAX проходят контур точно.
Итоги
| Тема | Кратко |
|---|---|
| Строгий разбор | xrtIntParse/xrtNumParse: весь вход легален или отказ; снисходительность флагами |
| Флаги разбора | XNUMBER_PARSE_SPACE (пробелы) / XNUMBER_PARSE_SEPARATOR (подчёркивания) |
| Вывод целых | xrtIntString: система счисления + префикс + регистр объявляются флагами |
| Кратчайший круговой проход | xrtNumString(值, 0): ровно столько цифр, чтобы круг сошёлся |
| Строки формата | ,d тысячи / #_X hex / ,.2f фикс / .1% проценты / c кодовая точка |
| Владение | все продукты str, ровно один xrtFree; у семейства Format есть и вариант To в буфер вызывающего |
| Дисциплина границ | тесты обязаны включать INT64_MIN — он специализируется на убийстве реализаций «сперва в положительное, потом отрицание» |
| Три хозяина | валидация на границе отвергает / горячий путь утверждает / слой показа форматирует — поза важнее выбора функции |
| Взгляд на тесты | пятнадцать отобранных границ ценнее десяти тысяч случайных — выбор прежде охвата |
📖 Справочники API в этой главе:number (22 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки