Ориентация главы
С этой главы начинается структурированные данные. xvalue — динамическая типовая система XRT: один непрозрачный указатель вмещает null/bool/int/float/string/bytes/time/массив/объект/множество/отображение — всё, что выражает JSON, плюс расширенные типы XLON (глава 32), живущие здесь же. Три решения определяют опыт употребления: точное чтение (GetInt принимает только int; несовпадение типа — отказ; первый шлюз против типовой каши), жизненный цикл на счёте ссылок (полное раскрытие примитива главы 3) и неизменяемое разделение (дерево значений может цитироваться из многих мест без копирования). Это продукт разбора глав 31/32/35 и источник данных шаблонов — фундамент второй половины тома.
31.1引入
Классическая головоломка конфигурационных систем: в одном файле тайм-аут — число, выключатель — булево, теги — список, вложенный сервис — объект; статические типы C не вмещают данные, «форма которых известна только в рантайме». Рукописный void* с типовой меткой — колесо, которое все изобретают заново, с чудовищной плотностью ловушек: суждение о типе — по памяти, ответственность за освобождение — по договорённости, семантика копирования — по молитве.
xvalue стандартизует это колесо: одно перечисление типов (xvaluetype) плюс семейство функций построения/чтения/проверки, с жизненным циклом на счёте ссылок. По концепции он изоморфен объектам-значениям динамических языков (object в Python, value в JS), но ставит «строгость типов» на первое место — выходной параметр даётся ровно того типа, который вы спросили, без снисходительности «число и строка преобразуются сами». Эта строгость в разборе конфигурации — твёрдая потребность: "timeout": "30" (строка) и "timeout": 30 (число) — два разных сигнала ошибки; снисходительность тихо превратит первое во второе, и баг затаится глубоко.
31.2概念
Семейство типов и построение
| Тип | Построение | Точное чтение |
|---|---|---|
| null / bool | xrtValueNull / Bool | GetBool |
| int64 / double | xrtValueInt / Float | GetInt / GetFloat |
| string / bytes | xrtValueString / Bytes | GetString (представление) / GetBytes |
| time / pointer | xrtValueTime / Pointer | GetTime / GetPointer |
| array / object | ArrayNew / ObjectNew + семейство Set | по индексу / по имени |
| set / intmap | SetNew / IntMapNew | множество и отображение с целыми ключами |
Null — одноэлементный объект уровня процесса: каждый раз возвращается тот же указатель, безопасно сравнивать через ==. Чтение строки возвращает представление (заимствование, конвенция главы 3), а не свежераспределённую копию. У чисел есть один специализированный снисходительный канал: xrtValueScalarEqual приравнивает int 2 и float 2.0 (межтиповая числовая семантика), и ValueHash с ним согласован (int 2 и float 2.0 хешируются одинаково) — согласованность, необходимая сценарию «дерево значений как ключ кеша».
Философия точного чтения
GetInt, получив значение float, отказывается, а не молча усекает; нужна числовая снисходительность — сперва явно проверьте тип, потом берите. Философия родня строгому разбору главы 26: типовая каша вскрывается у входа чтения, а не взрывается у потребителя. В паре с xrtValueType (запросить тип) и семейством проверок Is/IsNumber/IsContainer код чтения всегда имеет форму «сперва проверка, потом взятие; несовпадение типа — в путь ошибки».
Жизненный цикл: полный расцвет счёта ссылок
Это полное применение примитивов xrtRefRetain/xrtRefRelease главы 3: построение возвращает счёт 1; xrtValueRetain разделяет, xrtValueRelease возвращает; при подвесе дочернего значения контейнер приобретает собственную ссылку — вы освобождаете свой экземпляр сразу после построения, и дальнейшая жизнь дочернего значения — забота контейнера. Освобождение всего дерева возвращается рекурсивно: Release корня достигает нуля — разрушается всё дерево.
Операции над деревом значений
Две большие вещи конфигурационного сценария: xrtValueObjectMerge (слияние объектов с перекрытием — стандартная поза «умолчания + пользовательские переопределения», стратегии REPLACE/сохранить/отказ на конфликте) и xrtValueSetUnion/Merge (объединение множеств — стандартная поза слияния прав). xrtValueClone делает мелкое клонирование (разделение элементов) — выбор между мелким и глубоким клонированием разобран контрастным экспериментом в образце о владении.
31.3示例
Полная программа: обзор всех типов и точное чтение
Из образца репозитория examples/value/basic/main.c:
#include <stdio.h> #include <xrt.h> */ /* 演示全部基础标量、精确 Getter、类型查询、哈希和数值相等。 */ int main(void) { unsigned char arrBytes[] = { 1, 2, 3 }; int iMarker = 0; xvalue* arrValues[] = { xrtValueInt(2), xrtValueFloat(2.0), xrtValueString(XRT_STR_LITERAL("xrt")), xrtValueBytes((xbytesview){ arrBytes, sizeof(arrBytes) }), xrtValueTime((xtime)1234567), xrtValuePointer(&iMarker) }; xvalue* pTrue = xrtValueBool(true); xstrview Name; xbytesview Data; int64 iVersion; double fVersion; xtime Time; ptr pPointer; bool bTrue; uint64 iIntHash; uint64 iFloatHash; int iResult = 0; for ( size_t i = 0; i < (sizeof(arrValues) / sizeof(arrValues[0])); i++ ) { if ( arrValues[i] == NULL ) { iResult = 1; goto cleanup; } } if ( !xrtValueGetBool(pTrue, &bTrue) || !xrtValueGetInt(arrValues[0], &iVersion) || !xrtValueGetFloat(arrValues[1], &fVersion) || !xrtValueGetString(arrValues[2], &Name) || !xrtValueGetBytes(arrValues[3], &Data) || !xrtValueGetTime(arrValues[4], &Time) || !xrtValueGetPointer(arrValues[5], &pPointer) || !xrtValueHash(arrValues[0], &iIntHash) || !xrtValueHash(arrValues[1], &iFloatHash) || !xrtValueScalarEqual(arrValues[0], arrValues[1]) || !bTrue || (iVersion != 2) || (fVersion != 2.0) || (Data.Size != 3) || (Time != (xtime)1234567) || (pPointer != &iMarker) || (iIntHash != iFloatHash) || (xrtValueType(arrValues[2]) != XVALUE_STRING) || !xrtValueIs(arrValues[2], XVALUE_STRING) || !xrtValueIsNumber(arrValues[0]) || xrtValueIsContainer(arrValues[0]) || !xrtValueTruthy(arrValues[2]) || (xrtValueType(xrtValueNull()) != XVALUE_NULL) ) { iResult = 2; goto cleanup; } printf("%.*s value API v%lld\n", (int)Name.Size, Name.Data, (long long)iVersion); printf("time type: %s\n", xrtValueTypeName(xrtValueType(arrValues[4]))); cleanup: for ( size_t i = 0; i < (sizeof(arrValues) / sizeof(arrValues[0])); i++ ) { xrtValueRelease(arrValues[i]); } xrtValueRelease(pTrue); xrtValueRelease(xrtValueNull()); return iResult; }
Что только что произошло. (1) Семейство построения создаёт значения по одному: Int, Float, String, Bytes, Time... каждый конструктор возвращает владеющее значение со счётом 1. (2) Семейство точного чтения берёт их обратно по одному: GetInt читает int успешно, а float — с отказом: шлюз «даю ровно тот тип, что спрошен» проверен на каждом типе. (3) Семейство проверок идёт в паре с ValueTypeName (перечисление типа в имя в нижнем регистре) — отладочный вывод и сообщения об ошибках сообщают устойчивые имена вроде "time". (4) ScalarEqual проверяет, что 2 равно 2.0 через границу типов, а ValueHash — что оба хешируются одинаково: снисходительная числовая семантика и согласованность хеша ходят парой — предпосылка деревьев значений как ключей.
Полная программа: слияние конфигурации и объединение множеств
Из examples/value/collections/main.c:
#include <stdio.h> #include <xrt.h> */ /* 演示对象覆盖合并和保持稳定顺序的 Set 并集。 */ int main(void) { xvalue* pDefaults = xrtValueObject(); xvalue* pOptions = xrtValueObject(); xvalue* pLeft = xrtValueSet(); xvalue* pRight = xrtValueSet(); xvalue* pUnion = NULL; xvalue* pMerged = NULL; int iResult = 1; if ( (pDefaults == NULL) || (pOptions == NULL) || (pLeft == NULL) || (pRight == NULL) || !xrtValueObjectSetNew( pDefaults, XRT_STR_LITERAL("timeout"), xrtValueInt(30) ) || !xrtValueObjectSetNew( pOptions, XRT_STR_LITERAL("timeout"), xrtValueInt(5) ) || !xrtValueObjectMerge( pDefaults, pOptions, XVALUE_MERGE_REPLACE ) || !xrtValueSetAddNew(pLeft, xrtValueString(XRT_STR_LITERAL("read"))) || !xrtValueSetAddNew(pRight, xrtValueString(XRT_STR_LITERAL("write"))) || !xrtValueSetIsDisjoint(pLeft, pRight) ) { goto cleanup; } pUnion = xrtValueSetUnion(pLeft, pRight); pMerged = xrtValueClone(pLeft); if ( (pUnion == NULL) || (pMerged == NULL) || !xrtValueSetMerge(pMerged, pRight) || !xrtValueSetEqual(pMerged, pUnion) ) { goto cleanup; } printf( "options=%zu permissions=%zu\n", xrtValueCount(pDefaults), xrtValueCount(pUnion) ); iResult = 0; cleanup: xrtValueRelease(pMerged); xrtValueRelease(pUnion); xrtValueRelease(pRight); xrtValueRelease(pLeft); xrtValueRelease(pOptions); xrtValueRelease(pDefaults); return iResult; }
Что только что произошло. (1) Слияние объектов Merge(默认, 用户, REPLACE) (умолчания, пользователь, REPLACE): тайм-аут 5 из пользовательской конфигурации перекрывает 30 по умолчанию — после слияния count остаётся 1, перекрытие не добавляет ключей. (2) Объединение множеств двумя путями: Union порождает новое множество, Merge вливает на месте; продукты сверяются равенством SetEqual — разные дороги, один пункт назначения. (3) IsDisjoint решает, что два множества не пересекаются, — проверка прав «есть ли пересечение» в один шаг. (4) Обе операции вместе — скелет конфигурационного слоя «умолчания + пользовательские переопределения + слияние прав»; шаблоны главы 38 и деревья значений, прочитанные JSON-ом главы 31, употребляют их напрямую.
31.4契约
- Точное чтение: несовпадение типа — отказ, без неявных преобразований; числовая снисходительность — через явный
ScalarEqual. - Счёт ссылок: построение = 1,
Refразделяет,Releaseвозвращает, ноль разрушает всё дерево; контейнер при подвесе значения приобретает свою ссылку. - Одноэлементность Null: один указатель на весь процесс, сравним через
==. - Строка как заимствование:
GetStringвозвращает представление, срок действия следует за деревом значений. - Согласованность хеша:
ValueHashв паре сScalarEqual— дерево значений годится в ключи кеша. - Стратегии слияния: REPLACE перекрывает / сохранить существующее / отказ на конфликте — выбор явный.
От примеров к инженерии: три хозяина деревьев значений
Три типовых хозяина деревьев значений в инженерии определяют способ балансировки счёта ссылок.Дерево конфигурации: разбор на старте (JSON главы 31), построение один раз, чтение на всём пути, Release корня один раз на выходе — простейшая балансировка (только построение и освобождение корня, без Ref/Release посередине).Передача сообщений: дерево значений путешествует сообщением между потоками и модулями — отправитель Retain-ит и передаёт, обе стороны Release-ят каждая своё; либо передача владения через SetTake (стандартная поза сценария очереди главы 18 — одна передача, ноль лишних ссылок).Разделяемый кеш: одно дерево значений цитируется многими потребителями (конфигурация по умолчанию на все запросы) — построитель Retain-ит и кладёт в глобальное, потребители Retain/Release по отдельности; семантика COW (копирование при записи) заставляет мелко разделяемое Clone по-настоящему копироваться только при первом редактировании. Поверх трёх хозяев — одна генеральная дисциплина: балансировка записывается в документ дизайна, а не выпрашивается у рантайма — окончательный судья правильности балансировки, статистика главы 6.
Разделение труда с контейнерами
Каково отношение контейнеров значений главы 30 (array/object/set/intmap) к одноимённым контейнерам тома 3 (главы 14/18/19)? Две параллельные реализации на два мира. Контейнеры тома 3 — мир статических типов: тип элемента фиксирован на компиляции, без типовых меток, максимальная производительность — их берут структуры данных горячих путей. Контейнеры значений — мир динамических типов: каждое значение несёт метку типа, разнородная смесь поддерживается, сериализация доступна — их берут данные конфигураций и протоколов. Мнемоника выбора: форма данных известна на компиляции → контейнеры тома 3; известна только в рантайме (из JSON/сети/скриптов) → дерево значений. Между мирами есть мосты (intmap дерева значений конвертируется с xintmap), но не смешивайте семантики: пользоваться деревом значений как высокопроизводительным контейнером или, наоборот, вешать типовые метки на статические контейнеры — значит заново строить противоположный мир.
Типичные ошибки
Симптом: утечка памяти — после освобождения дерева значений статистика всё ещё видит живые значения; объём утечки пропорционален «числу дочерних значений, подвешенных в контейнеры».
Причина: при подвесе в контейнер ссылка прибавляется (экземпляр контейнера); экземпляр, полученный при построении, остаётся в вашей руке — не Released, он навсегда лишний.
xvalue* pTimeout = xrtValueInt(30); xrtValueObjectSet(pConfig, XRT_STR_LITERAL("timeout"), pTimeout); /* нет Release: счёт pTimeout стоит на 2; после освобождения контейнера остаётся 1 - утечка */
xvalue* pTimeout = xrtValueInt(30); xrtValueObjectSet(pConfig, XRT_STR_LITERAL("timeout"), pTimeout); xrtValueRelease(pTimeout); /* контейнер уже держит ссылку - свой экземпляр возвращаем */
Симптом: число в конфигурации записано строкой ("30") — программа «работает», но поведение дрейфует вместе с форматом источника; при рефакторинге мина взрывается.
Причина: дисциплина «сперва проверь IsNumber, потом бери» вытеснена настроением «попробую-ка GetInt» — снисходительность маскирует ошибку формы данных у источника.
int64 iTimeout; if ( xrtValueGetInt(pValue, &iTimeout) ) { /* "30" (строка) отказывается здесь - но хуже, если кто-то добавит fallback со второй попыткой строкой */ }
if ( xrtValueType(pValue) == XVALUE_INT ) { int64 iTimeout; xrtValueGetInt(pValue, &iTimeout); } else { ReportTypeMismatch("timeout", "int"); /* ошибка формы докладывается у входа чтения */ }
Практические задания
Постройте по одному значению каждого скалярного типа, напечатайте для каждого ValueTypeName и ValueType; вызовите GetFloat на значении int и проверьте отказ — ощущение строгости.
Постройте два дерева объектов — «конфигурация по умолчанию» и «пользовательская» (по 3~4 ключа, с вложенными объектами), слейте с REPLACE и выведите все пары ключ-значение; затем смените стратегию на «отказ при конфликте» и подайте вход с конфликтующим ключом, наблюдая отказ. Подсказка: продумайте и семантику слияния вложенных объектов (заменять целиком или сливать рекурсивно — решайте по своему бизнесу и зафиксируйте в комментарии).
Храните «имя конфигурации → дерево значений» в отображении главы 18; дерево значений хешируйте ValueHash, эквивалентность проверяйте ScalarEqual: одна конфигурация, разобранная дважды, обязана дать один отпечаток; изменение одного поля обязано его сменить. Критерии приёмки: отпечаток стабилен и годится в тестовое утверждение; деревья при попадании и промахе кеша равны по ScalarEqual; после освобождения всего статистика главы 6 подтверждает ноль утечек.
Итоги
| Тема | Кратко |
|---|---|
| Семейство типов | скаляры + string/bytes/time/pointer + array/object/set/intmap |
| Точное чтение | несовпадение типа — отказ; числовая снисходительность через ScalarEqual; проверка типа через ValueType |
| Жизненный цикл | построение = 1 / Ref разделяет / Release возвращает; контейнер при подвесе держит свою ссылку |
| Одноэлементность Null | тот же указатель, сравним через ==; чтобы отличить «нет» от «значение null», проверяйте тип через XVALUE_NULL |
| Хеш | ValueHash в паре с ScalarEqual — дерево значений годится в ключи кеша |
| Конфигурационная пара | ObjectMerge (REPLACE/сохранить/конфликт) + SetUnion/Merge |
| Строки | GetString возвращает заимствованное представление; нужна копия — Dup-ните сами |
📖 Справочники API в этой главе:value (159 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки