Структуры и перечисления
Структуры группируют связанные поля в одну сущность, а перечисления делают состояния читаемыми. Вместе с маппингом это «таблица» в блокчейне.
Хотите хранить пользователей, заказы или голоса? Это всегдаstructвнутриmapping— рабочая лошадка любого протокола.
Структура (struct) — пользовательский тип из нескольких полей, как запись в таблице. Перечисление (enum) — именованный набор состояний (например Pending, Active, Closed), под капотом это маленькое целое, но читать код гораздо приятнее.
Главный паттерн хранения сущностей: mapping(uint256 => Struct) или mapping(address => Struct). Ключ — идентификатор, значение — вся запись.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Campaigns {
enum Status { Pending, Active, Closed }
struct Campaign {
address creator;
uint256 goal;
uint256 raised;
Status status;
}
mapping(uint256 => Campaign) public campaigns; // id => запись
uint256 public nextId;
function create(uint256 goal) external returns (uint256 id) {
id = nextId++;
campaigns[id] = Campaign({
creator: msg.sender,
goal: goal,
raised: 0,
status: Status.Active
});
}
function pledge(uint256 id) external payable {
Campaign storage c = campaigns[id]; // ссылка на storage
require(c.status == Status.Active, "not active");
c.raised += msg.value;
if (c.raised >= c.goal) c.status = Status.Closed;
}
}
mapping(uint256 => Campaign)
===========================
id=0 -> { creator, goal, raised, status }
id=1 -> { creator, goal, raised, status }
id=2 -> { creator, goal, raised, status }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
struct = «строка таблицы», ключ = «первичный ключ»
Как работает под капотом (EVM/газ)
Поля структуры раскладываются по слотам storage подряд и подчиняются той же упаковке: мелкие поля (enum, bool, address) компилятор может уложить в один слот. Ключевой момент — слово storage в Campaign storage c = ...: это ссылка на данные в блокчейне, и изменение c.raised сразу пишет в storage. Если бы было memory, вы бы меняли временную копию, и изменения не сохранились бы. enum хранится как uint8, поэтому очень дёшев.
# Та же логика на Python: struct внутри mapping
from dataclasses import dataclass
PENDING, ACTIVE, CLOSED = 0, 1, 2 # enum как маленькие целые
@dataclass
class Campaign:
creator: str
goal: int
raised: int = 0
status: int = ACTIVE
campaigns = {} # id -> Campaign (mapping)
next_id = 0
def create(creator, goal):
global next_id
cid = next_id; next_id += 1
campaigns[cid] = Campaign(creator, goal)
return cid
def pledge(cid, amount):
c = campaigns[cid] # ссылка на запись (storage)
assert c.status == ACTIVE, "not active"
c.raised += amount
if c.raised >= c.goal:
c.status = CLOSED
cid = create("alice", goal=100)
pledge(cid, 60); pledge(cid, 50)
print(campaigns[cid]) # raised=110, status=CLOSED
«Та же логика на Python ▶». Объект-ссылка ведёт себя как storage-ссылка: меняя поля, мы меняем саму запись в «маппинге».
Частые ошибки
- Взять
Campaign memory c = campaigns[id], изменить поле и удивляться, что в блокчейне ничего не поменялось — нужнаstorage-ссылка. - Класть в структуру динамические массивы/строки без нужды — это раздувает газ и усложняет копирование.
- Сравнивать
enumс «магическими числами» вместо именованных значений — теряется читаемость.
Best practices
- Объявляйте поля структуры так, чтобы мелкие типы шли подряд — компилятор упакует их и сэкономит слоты.
- Используйте
enumдля конечных состояний вместо «волшебных» чисел или строк. - Помните разницу
storagevsmemoryпри работе со структурами — это частый источник тихих багов.
Итоги
struct группирует поля сущности, enum делает состояния явными, а mapping от id к структуре — это «таблица» протокола. Различие storage/memory здесь критично. Дальше — массивы и циклы.