Structs и паттерны хранения
Как группировать данные в struct, хранить структуры в mapping и массиве, а также обновлять их через storage-ссылку.
Struct группирует связанные поля
struct — это пользовательский тип данных в Solidity. Он собирает несколько полей в одну сущность: профиль пользователя, заявку, ордер, NFT-метаданные или запись голосования.
Сам по себе struct не решает, где хранить данные. Обычно его кладут в mapping для быстрого поиска или в массив, если нужен список и итерация.
Пишем реестр профилей
Создайте файл contracts/ProfileRegistry.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ProfileRegistry {
struct Profile {
string name;
string avatarUri;
uint64 createdAt;
uint64 updatedAt;
bool active;
}
mapping(address => Profile) private profiles;
mapping(address => bool) public hasProfile;
address[] private users;
event ProfileCreated(address indexed user, string name, string avatarUri);
event ProfileUpdated(address indexed user, string name, string avatarUri);
event ProfileDeactivated(address indexed user);
error EmptyName();
error ProfileAlreadyExists(address user);
error ProfileNotFound(address user);
function createProfile(string calldata name, string calldata avatarUri) external {
if (bytes(name).length == 0) {
revert EmptyName();
}
if (hasProfile[msg.sender]) {
revert ProfileAlreadyExists(msg.sender);
}
profiles[msg.sender] = Profile({
name: name,
avatarUri: avatarUri,
createdAt: uint64(block.timestamp),
updatedAt: uint64(block.timestamp),
active: true
});
hasProfile[msg.sender] = true;
users.push(msg.sender);
emit ProfileCreated(msg.sender, name, avatarUri);
}
function updateProfile(string calldata name, string calldata avatarUri) external {
if (bytes(name).length == 0) {
revert EmptyName();
}
if (!hasProfile[msg.sender]) {
revert ProfileNotFound(msg.sender);
}
Profile storage profile = profiles[msg.sender];
profile.name = name;
profile.avatarUri = avatarUri;
profile.updatedAt = uint64(block.timestamp);
emit ProfileUpdated(msg.sender, name, avatarUri);
}
function deactivateProfile() external {
if (!hasProfile[msg.sender]) {
revert ProfileNotFound(msg.sender);
}
Profile storage profile = profiles[msg.sender];
profile.active = false;
profile.updatedAt = uint64(block.timestamp);
emit ProfileDeactivated(msg.sender);
}
function getProfile(address user) external view returns (Profile memory) {
if (!hasProfile[user]) {
revert ProfileNotFound(user);
}
return profiles[user];
}
function getUser(uint256 index) external view returns (address) {
return users[index];
}
function userCount() external view returns (uint256) {
return users.length;
}
}Этот контракт хранит профиль каждого адреса в mapping, а адреса всех пользователей — в массиве. Так контракт умеет быстро найти профиль и при этом не теряет список всех зарегистрированных адресов.
Как это работает
struct Profile — описывает форму записи. Каждая запись содержит имя, URI аватара, время создания, время обновления и статус активности.
mapping(address => Profile) private profiles — хранит профиль по адресу пользователя. Это основной storage для данных.
mapping(address => bool) public hasProfile — отдельно хранит факт существования профиля. Без этого пустой Profile и реальный профиль с пустыми значениями было бы сложно различить.
address[] private users — хранит список ключей для итерации. mapping нельзя перебрать, поэтому массив нужен, если приложение должно показать всех пользователей.
profiles[msg.sender] = Profile({...}) — создаёт структуру через именованные поля. Так код не зависит от порядка аргументов и читается проще.
Profile storage profile = profiles[msg.sender] — создаёт ссылку на структуру в storage. Изменения profile.name, profile.avatarUri и profile.updatedAt сразу меняют состояние контракта.
return profiles[user] — копирует структуру из storage в memory и возвращает её наружу.
Паттерны хранения struct
mapping(id => Struct) подходит для быстрого поиска по ключу: адресу, id заявки, id токена или хэшу.
Struct[] подходит для упорядоченного списка, где важны индексы и длина. Но искать элемент по полю внутри массива дорого: придётся идти циклом.
mapping(key => Struct) + key[] — частый паттерн для контрактов, которым нужны и быстрый поиск, и список ключей. В примере profiles даёт поиск, а users хранит адреса для постраничного чтения.
Частые ошибки
Обновлять копию в memory → Profile memory profile = profiles[msg.sender] создаст копию. Изменения такой переменной не сохранятся в контракте.
Пытаться понять существование по полям struct → значения по умолчанию могут выглядеть как настоящая запись. Храните отдельный флаг exists или hasProfile.
Возвращать огромный массив структур → чем больше данных, тем тяжелее ответ RPC. Для больших списков возвращайте userCount() и читайте элементы по индексам.
Что дальше
- Arrays — push, pop, итерация — храните список структур и обходите его циклом
- Mapping и вложенные маппинги — используйте ключи для быстрого доступа к struct
- Чтение storage через getStorageAt — разберите, как структуры лежат в storage