китаев.tech

Structs и паттерны хранения

Как группировать данные в struct, хранить структуры в mapping и массиве, а также обновлять их через storage-ссылку.

Struct группирует связанные поля

struct — это пользовательский тип данных в Solidity. Он собирает несколько полей в одну сущность: профиль пользователя, заявку, ордер, NFT-метаданные или запись голосования.

Сам по себе struct не решает, где хранить данные. Обычно его кладут в mapping для быстрого поиска или в массив, если нужен список и итерация.


Пишем реестр профилей

Создайте файл contracts/ProfileRegistry.sol:

solidity
// 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 хранит адреса для постраничного чтения.


Частые ошибки

Обновлять копию в memoryProfile memory profile = profiles[msg.sender] создаст копию. Изменения такой переменной не сохранятся в контракте.

Пытаться понять существование по полям struct → значения по умолчанию могут выглядеть как настоящая запись. Храните отдельный флаг exists или hasProfile.

Возвращать огромный массив структур → чем больше данных, тем тяжелее ответ RPC. Для больших списков возвращайте userCount() и читайте элементы по индексам.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.