Ориентация главы

С этой главы начинается структурированные данные. 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 / boolxrtValueNull / BoolGetBool
int64 / doublexrtValueInt / FloatGetInt / GetFloat
string / bytesxrtValueString / BytesGetString (представление) / GetBytes
time / pointerxrtValueTime / PointerGetTime / GetPointer
array / objectArrayNew / ObjectNew + семейство Setпо индексу / по имени
set / intmapSetNew / IntMapNewмножество и отображение с целыми ключами

Null — одноэлементный объект уровня процесса: каждый раз возвращается тот же указатель, безопасно сравнивать через ==. Чтение строки возвращает представление (заимствование, конвенция главы 3), а не свежераспределённую копию. У чисел есть один специализированный снисходительный канал: xrtValueScalarEqual приравнивает int 2 и float 2.0 (межтиповая числовая семантика), и ValueHash с ним согласован (int 2 и float 2.0 хешируются одинаково) — согласованность, необходимая сценарию «дерево значений как ключ кеша».

Философия точного чтения

GetInt, получив значение float, отказывается, а не молча усекает; нужна числовая снисходительность — сперва явно проверьте тип, потом берите. Философия родня строгому разбору главы 26: типовая каша вскрывается у входа чтения, а не взрывается у потребителя. В паре с xrtValueType (запросить тип) и семейством проверок Is/IsNumber/IsContainer код чтения всегда имеет форму «сперва проверка, потом взятие; несовпадение типа — в путь ошибки».

Жизненный цикл: полный расцвет счёта ссылок

1ПостроениеInt/String/ObjectNew возвращают владеющие значения со счётом 1
2РазделениеRetain добавляет ссылку - дерево значений сохраняется в несколько структур без копирования
3ОсвобождениеRelease снимает ссылку - достигший нуля разрушается (включая всё поддерево)
4Подвес в контейнерсемейство Set вешает дочернее значение в контейнер - контейнер держит ссылку; после подвеса свой экземпляр можно безопасно Release-нуть

Это полное применение примитивов xrtRefRetain/xrtRefRelease главы 3: построение возвращает счёт 1; xrtValueRetain разделяет, xrtValueRelease возвращает; при подвесе дочернего значения контейнер приобретает собственную ссылку — вы освобождаете свой экземпляр сразу после построения, и дальнейшая жизнь дочернего значения — забота контейнера. Освобождение всего дерева возвращается рекурсивно: Release корня достигает нуля — разрушается всё дерево.

Операции над деревом значений

Две большие вещи конфигурационного сценария: xrtValueObjectMerge (слияние объектов с перекрытием — стандартная поза «умолчания + пользовательские переопределения», стратегии REPLACE/сохранить/отказ на конфликте) и xrtValueSetUnion/Merge (объединение множеств — стандартная поза слияния прав). xrtValueClone делает мелкое клонирование (разделение элементов) — выбор между мелким и глубоким клонированием разобран контрастным экспериментом в образце о владении.

31.3示例

Полная программа: обзор всех типов и точное чтение

Из образца репозитория examples/value/basic/main.c:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/value/basic/main.c -lws2_32 -liphlpapixrt value API v2time type: time

Что только что произошло. (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:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/value/collections/main.c -lws2_32 -liphlpapioptions=1 permissions=2

Что только что произошло. (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), но не смешивайте семантики: пользоваться деревом значений как высокопроизводительным контейнером или, наоборот, вешать типовые метки на статические контейнеры — значит заново строить противоположный мир.

Типичные ошибки

Ловушка 1: забыть Release своего экземпляра после подвеса в контейнер:Ловушка 1: забыть Release своего экземпляра после подвеса в контейнер

Симптом: утечка памяти — после освобождения дерева значений статистика всё ещё видит живые значения; объём утечки пропорционален «числу дочерних значений, подвешенных в контейнеры».

Причина: при подвесе в контейнер ссылка прибавляется (экземпляр контейнера); экземпляр, полученный при построении, остаётся в вашей руке — не 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);   /* контейнер уже держит ссылку - свой экземпляр возвращаем */
Ловушка 2: чтение через типовую снисходительность:Ловушка 2: чтение через типовую снисходительность

Симптом: число в конфигурации записано строкой ("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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки