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

Storage в Solidity: постоянное состояние контракта

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

Главная задача урока — понять разницу между ссылкой на постоянную запись и временной копией. Ошибка в этой разнице может дать функцию, которая возвращает убедительный результат, но не меняет настоящий реестр. Разберём её на отключении курса 17.

Поле контракта живёт между вызовами

Поле courses объявляется на уровне контракта. Каждый успешный вызов работает с состоянием выбранного экземпляра. Если издатель сохранил H1, следующая функция чтения должна увидеть H1, пока другое допустимое действие его не заменит. Локальная переменная функции имеет другую продолжительность жизни: она помогает вычислению текущего вызова.

struct Course {
    bytes32 contentHash;
    bool active;
}

mapping(uint256 => Course) public courses;

Это определения из базового реестра. mapping связывает номер курса с его структурой. Мы не создаём отдельный объект в памяти при каждом чтении и не загружаем состояние из JSON сайта. Источником является выбранный экземпляр EVM-контракта.

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

Ссылка на существующую запись

Сохраним короткую ссылку на курс:

function deactivate(uint256 courseId) external onlyPublisher {
    Course storage item = courses[courseId];
    if (!item.active) revert NotActive();
    item.active = false;
    emit Deactivated(courseId);
}

Этот вариант заменяет только тело базовой deactivate, сохраняя её смысл. Course storage item указывает на запись в courses. Изменение item.active меняет то же поле, которое другой getter потом прочитает как courses[courseId].active. Отпечаток не присваивается заново, поэтому остаётся прежним.

Ожидаемая последовательность: до вызова у курса 17 H1 и true; после допустимого действия — H1 и false. Если курс неактивен, возникает NotActive, и новое событие отключения не создаётся. Никакая локальная переменная не содержит самостоятельную «версию» записи, которую затем нужно автоматически синхронизировать.

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

Временная копия с другим результатом

Теперь рассмотрим неправильную для нашей задачи замену:

function previewDisabled(uint256 courseId)
    external
    view
    returns (Course memory)
{
    Course memory item = courses[courseId];
    item.active = false;
    return item;
}

Сам по себе этот отдельный getter допустим как предварительный просмотр. Он создаёт копию в memory, меняет её и возвращает. Реальная запись не должна измениться. Ошибка возникнет, если разработчик назовёт подобный код деактивацией и решит, что задача выполнена.

Читатель получит пару H1,false как результат функции, но следующий обычный getter по-прежнему увидит H1,true. Здесь нет противоречия EVM: первый ответ описывает временно подготовленную копию. Сайт должен знать назначение метода, а не выводить изменение постоянного состояния из любого возвращённого флага.

Это полезный приём предварительного расчёта, если его назвать честно. Например, редактор может посмотреть, как будет выглядеть запись после планируемого действия. Но такая модель не заменяет транзакцию и не сообщает, что дальнейшее исполнение обязательно пройдёт на новом состоянии.

Присваивание структуры

Базовая публикация использует другое выражение:

courses[courseId] = Course(contentHash, true);

Оно записывает структуру в постоянное поле mapping. Это не только переключение активности: прежний отпечаток заменяется переданным. После publish(17,H2) текущая запись содержит H2,true, даже если раньше курс был отключён. Именно поэтому копия структуры и ссылка на структуру требуют отдельного разбора: присваивание всего объекта тоже имеет определённый смысл.

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

Что не скрывает storage

Постоянные данные публичной цепочки не становятся секретными от объявления поля private. Модификатор видимости ограничивает доступ из Solidity-интерфейса, а не наблюдение байтов состояния. В этой лаборатории сохраняется отпечаток публичного учебного выпуска; личных данных здесь нет.

Удаление постоянной записи также не удаляет её прежнюю публичную историю. Более того, для структур с вложенными mapping очистка имеет дополнительные ограничения, поскольку сами ключи mapping не перечисляются автоматически. Наше простое отключение намеренно вообще не удаляет отпечаток: это бизнес-решение, а не способ забыть исторические данные.

В результате мы можем предсказать три разных эффекта: запись всей структуры, изменение поля через storage-ссылку и изменение копии в memory. Такое различие станет основой последующих уроков о массивах и параметрах функций. Области данных Solidity описывают правила назначения ссылочных типов.

Переход между двумя номерами

Чтобы проверить понимание ссылки, возьмите одновременно курсы 17 и 18. Ссылка Course storage item = courses[17] относится к записи 17. Изменение её флага не должно отключать 18. Наличие одинакового hash у двух курсов не объединяет их поля: ключи mapping выбирают независимые записи.

Теперь замените такую ссылку копией в memory и отключите её. Getter 17 продолжает показывать прежнюю активность, а getter 18 тоже не меняется. Ответ функции может при этом содержать отключённую копию. Это позволяет отличить неверную область данных от неверного ключа: симптомы похожи только при поверхностном чтении интерфейса.

Ссылку на постоянные данные не стоит удерживать мысленно как вечный объект вне исполнения. При следующем вызове программа снова обращается к состоянию по выбранному ключу. Если другая транзакция успела заменить запись, данные нового вызова уже другие. Область storage описывает расположение, а не блокировку от конкурентных переходов.

Есть особые случаи с массивами, удалёнными элементами и висячими ссылками. Наша простая запись не содержит такого удаления, поэтому не требует демонстрации этих сложных эффектов. Если модель изменится, предположение о сохранении ссылки нужно пересмотреть по точному коду. Хорошее правило первого проекта — изменять постоянное поле понятно и один раз, вместо длинной цепочки присваиваний с неожиданной сменой объекта.

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

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