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

Memory и calldata в Solidity: временные данные

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

Разберём memory и calldata на отдельной функции предварительного просмотра. Она не входит в базовый CourseCatalog и не меняет его семантику. Результат урока — умение прочитать сигнатуру и понять, какие данные доступны функции и где может возникнуть копия.

Данные входного вызова

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

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

function inspectIds(uint256[] calldata ids)
    external
    pure
    returns (bool[] memory valid)
{
    if (ids.length > 20) revert TooManyIds();
    valid = new bool[](ids.length);
    for (uint256 i; i < ids.length; ++i) {
        valid[i] = ids[i] != 0;
    }
}

Это функция самостоятельного учебного инспектора. Собственная ошибка TooManyIds объявляется в его контракте. Массив ids читается из входа, valid создаётся в памяти. Мы заранее ограничиваем число элементов, чтобы пример не подразумевал неограниченную работу только из-за отсутствия записи в storage.

При входе [17,0,18] ожидается [true,false,true]. Исходный массив не переписывается, а значения реестра не меняются. У результата та же длина, поскольку для каждого входного номера создаётся один логический ответ. Такой договор проще, чем возвращать отдельный список только допустимых номеров без объяснения их соответствия.

Копия имеет цену и смысл

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

uint256[] memory working = ids;

В контексте допустимой функции это выражение создаёт память для обработки входного массива. Меняя working[0], функция не меняет ранее подписанное сообщение отправителя и не записывает новые номера в реестр. Однако сам процесс копирования расходует ресурсы, и большой вход требует внимания к ограничениям.

Прежде чем копировать, стоит спросить, нужна ли операция в контракте. Например, сортировка списка для интерфейса может выполняться на сайте. Перемещение задачи наружу не должно нарушать требования: если порядок значим для авторизации или расчёта, контракт обязан проверить нужное свойство самостоятельно. Для визуальной сортировки таких требований обычно нет.

Ссылка внутри memory

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

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

Для структуры курса ситуация похожа. Course memory item = courses[17] получает временную копию данных из состояния. Но последующая работа с этой копией не обязана менять courses[17]. В предыдущем уроке именно эта разница отделяла предварительный просмотр от настоящей деактивации.

Вход является недоверенным

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

Повторы также требуют решения. Для инспектора [17,17] означает два одинаковых ответа. Для будущей пакетной публикации повтор может означать несколько замен одного курса в рамках вызова. Тогда последний отпечаток, события и расход будут отличаться от варианта с уникальными номерами. Нельзя считать массив «набором», если порядок и повторы не были проверены.

Область Длительность Что означает изменение
storage Между успешными вызовами Новое состояние экземпляра
memory Текущее исполнение Изменённый временный рабочий объект
calldata Вход внешнего вызова Данные читаются по правилам области

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

В результате можем объяснить сигнатуру uint256[] calldata → bool[] memory: ограниченный вход читается, новый результат создаётся, реестр не меняется. Дальше оформим постоянную запись курса в структуру и рассмотрим, почему группа полей удобнее нескольких несвязанных таблиц. Правила memory и calldata содержат точные условия присваивания.

Отдельный результат для каждого элемента

Для массива [17,0,18] полезно назвать соответствие явно: valid[0] относится к 17, valid[1] к 0, valid[2] к 18. Если интерфейс отсортирует вход, но оставит ответы в прежнем порядке, ошибка появится вне контракта. Типы массивов правильны, но связь индексов разрушена. Поэтому договор результата должен сохраняться и в браузерном коде.

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

Рассмотрим теперь превышение длины. Функция отклоняет 21 элемент до выделения итогового массива и прохода цикла. Это означает один отказ для всего входа. Она не обрабатывает первые двадцать и не игнорирует последний. Клиент, решивший разделить запрос, должен сделать это явно и учитывать момент каждого отдельного чтения.

При самостоятельном упражнении не пытайтесь изменить размер уже выделенного memory-массива вызовом push. Его длина задаётся при создании. Для другого размера создаётся новый массив и переносится нужное содержимое. Эта особенность отделяет динамический тип по размеру входа от возможности менять размер после выделения.

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

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