Перейти к содержанию

Mapping в Solidity: ключи и отсутствующая запись

У библиотеки есть стабильные номера курсов. Читателю нужен прямой ответ по номеру 17, а не поиск по массиву всех выпущенных материалов. Для такой связи подходит mapping: отображение ключа на значение. Рассмотрим его поведение и ограничения, прежде чем пытаться построить из него каталог страниц.

Главный новый результат — различать значение по ключу, факт публикации и перечень известных ключей. Эти вопросы часто объединяют, хотя mapping не решает их автоматически одним механизмом.

Получение записи по номеру

Базовое поле имеет форму:

mapping(uint256 => Course) public courses;

Слева указан тип ключа, справа — тип значения. Внутри функции выражение courses[17] обращается к записи для этого номера. Другой номер обращается к другому логическому месту состояния. Нам не требуется проходить курс 1, затем 2 и так далее, чтобы найти 17.

Ключ 17 не сообщает, что в реестре есть ровно семнадцать курсов. Издатель может сначала опубликовать 17, затем 2000. Ни порядок номеров, ни отсутствие промежуточных ключей не создают ошибку в этой модели. Если сайту нужна последовательная нумерация, она должна быть отдельным правилом или свойством редакционного процесса.

Кроме того, отображение не является словарём JavaScript, который можно перечислить встроенным методом. Сам mapping не хранит доступный список всех использованных ключей. Чтобы получить каталог, приложение обычно использует события и внешнюю индексацию либо отдельный ограниченный список, если именно это требуется контракту.

Стандартное значение

Для никогда не записанного ключа чтение возвращает стандартные значения его типа. Для нашей структуры это нулевой bytes32 и false. Здесь не возникает автоматического исключения «ключ не найден». Поэтому интерфейс обязан интерпретировать нули по требованиям модели.

courses[17] до публикации → (0x00…00, false)
courses[17] после H1     → (H1, true)
courses[18] без записи   → (0x00…00, false)

Если нулевой отпечаток был бы допустимым содержимым, отсутствие стало бы неоднозначным. Мы устранили эту неоднозначность правилом publish: ненулевой номер и ненулевой отпечаток. Поэтому ненулевой hash служит признаком, что запись была опубликована, независимо от текущего флага активности.

Это частный выбор модели. В других задачах стандартное значение может быть допустимым полезным значением, и тогда потребуется отдельный флаг существования. Нельзя универсально объявить «ноль всегда означает отсутствие» без чтения требований. Даже в нашем реестре нулевой номер отклоняется не из-за свойства mapping, а из-за явного условия.

Отсутствие и деактивация

После deactivate(17) отпечаток H1 сохраняется. Следовательно, состояние отключённого выпуска отличается от состояния никогда не опубликованного курса. Для сайта это полезная информация: можно показать, что выпуск существовал, но сейчас не считается действующим.

Если разработчик заменит деактивацию на delete courses[17], getter снова вернёт стандартные нули. Исторические логи при этом не исчезают, но текущая модель уже не отражает тот же смысл. Поэтому слово «удалить» нельзя без разбора подставить вместо «отключить». Они решают разные требования, даже если карточка в интерфейсе скрывается в обоих случаях.

function exists(uint256 courseId) external view returns (bool) {
    return courses[courseId].contentHash != bytes32(0);
}

Это дополнительный getter, который опирается на ненулевой hash как признак опубликованной записи. Он корректен именно при сохранённом правиле базового publish. Если в будущей модели разрешены нулевые значения или настоящая очистка, договор такой функции должен измениться.

Почему каталог строится отдельно

Сайт ProfessorWeb показывает несколько тысяч материалов. Даже если перенести их номера в реестр, функция «вернуть всё» создаст проблему объёма. У mapping нет встроенного списка ключей, а добавленный список может разрастись. Поэтому для внешнего каталога естественно читать события ограниченными диапазонами и строить обычную поисковую базу.

При этом индексатор не становится источником полномочий. Он ускоряет поиск известных курсов и истории, а актуальная запись проверяется в выбранном контракте. Если индексатор временно отстаёт, сайт может не показать новый курс сразу. Это задержка представления, а не отсутствие публикации в самом состоянии.

Можно добавить отдельный массив ключей внутри контракта, но тогда потребуется правило первого добавления, проверка дублей, ограничение чтения и оценка стоимости. При повторном publish(17,H2) нельзя бездумно снова добавлять 17, если список должен содержать уникальные курсы. Такой массив — отдельный механизм, а не бесплатное свойство отображения.

Учебный вариант запроса

Сравним вопросы, которые возникают у клиента:

Вопрос Достаточно courses(id) Дополнительная модель
Какой текущий hash у 17 Да Не требуется
Активен ли выпуск 17 Да Не требуется
Был ли 17 опубликован Да при нашей семантике hash В иных моделях флаг existence
Какие номера вообще есть Нет Индекс событий или список
Все прошлые выпуски 17 Нет История событий либо журнал версий

Такая таблица помогает не перегружать базовый реестр. Он хорошо отвечает на точные вопросы по ключу. Поиск, полнотекстовый индекс и страницы каталога удобнее решаются в обычной инфраструктуре сайта, которая умеет выдавать ограниченные страницы и кэшировать результаты.

В результате у нас есть понятный маршрут прямого чтения и отдельный маршрут обнаружения материалов. На следующих страницах назначим издателя и разрешим ему менять записи. Уже сейчас полезно сформулировать ожидаемый случай: чтение неизвестного номера нормально возвращает нули, а изменение неизвестного номера зависит от правила конкретной функции. Mapping Solidity описывает его свойства и отсутствие автоматического перечисления ключей.

Внешний список и доверие к нему

Пусть индексатор сообщает номера 17,18,2000. Сайт читает каждый по контракту и получает запись. Это не означает, что все возможные номера перечислены: индекс может быть неполным или отстающим. Поэтому удобный список каталогов и доказательство отсутствия любых других ключей являются разными задачами.

Для редакционной системы достаточно определить источник списка и показывать его свежесть. Если нужна строгая полнота относительно истории реестра, потребуется обработка всех выбранных событий от известного блока размещения с учётом принятой истории сети. Одного ответа getter для трёх номеров мало, чтобы судить обо всём пространстве mapping.

Не стоит проверять существование простым проходом от 1 до самого большого увиденного номера. Издатель может использовать разреженные ключи, а тип допускает огромные значения. Стоимость такого поиска определяется диапазоном, а не фактическим числом курсов. Внешнее перечисление событий решает другой, более подходящий вопрос.

Также нельзя вывести удаление истории из стандартного результата. Если текущая запись нулевая в другой модели, события могли существовать раньше. Наш базовый реестр не очищает hash при отзыве, поэтому обычное состояние помогает различить случаи. Это свойство именно выбранных методов, которое нужно сохранить при развитии.

Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.

Оглавление серии