Маппинги: ключ-значение в блокчейне
Маппинг — главная структура данных Solidity. Почти каждый токен и протокол держит балансы именно в нём.
Маппинг в Solidity — это бесконечная заранее заполненная таблица: любой ключ уже «существует» и указывает на ноль. Удивительно, но по ней нельзя пройти циклом.
mapping(KeyType => ValueType) — это ассоциативный массив ключ-значение, живущий в storage. Самый частый случай — балансы: mapping(address => uint256). В отличие от хэш-таблиц в обычных языках, маппинг не хранит список ключей и не знает свою «длину». Все возможные ключи как будто уже есть и указывают на нулевое значение по умолчанию.
Вложенные маппинги
Маппинги можно вкладывать: mapping(address => mapping(address => uint256)) — классика для разрешений ERC-20 (кто кому сколько разрешил тратить). Читается как «владелец → спендер → сумма».
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Balances {
mapping(address => uint256) public balanceOf;
// владелец => спендер => разрешённая сумма
mapping(address => mapping(address => uint256)) public allowance;
function setBalance(address user, uint256 amount) external {
balanceOf[user] = amount; // запись по ключу
}
function approve(address spender, uint256 amount) external {
allowance[msg.sender][spender] = amount; // вложенный ключ
}
}
mapping(address => uint256) balanceOf ===================================== slot = keccak256(key . baseSlot) alice -> keccak256(...) -> storage[h1] = 100 bob -> keccak256(...) -> storage[h2] = 0 (по умолчанию) carol -> keccak256(...) -> storage[h3] = 50 Ключей «не существует» как списка -> итерация невозможна
Как работает под капотом (EVM/газ)
Маппинг не хранит данные подряд. Для ключа k позиция в storage вычисляется как keccak256(k . slot), где slot — номер слота самого маппинга. Поэтому доступ по ключу — это одна хэш-операция и один SLOAD/SSTORE, быстро и предсказуемо. Но именно из-за такой раскладки EVM не держит реестр ключей: нельзя узнать, сколько их и какие они. Если нужна итерация — заводят отдельный массив ключей вручную.
# Та же логика на Python: маппинг балансов как dict с дефолтом 0
from collections import defaultdict
balance_of = defaultdict(int) # любой ключ -> 0 по умолчанию
allowance = defaultdict(lambda: defaultdict(int)) # вложенный маппинг
balance_of["alice"] = 100
allowance["alice"]["exchange"] = 30 # alice разрешила бирже тратить 30
print("balance bob (не задан):", balance_of["bob"]) # 0
print("balance alice:", balance_of["alice"]) # 100
print("allowance alice->exchange:", allowance["alice"]["exchange"])
# по «маппингу» нельзя надёжно итерироваться как в EVM:
print("ключей в реестре EVM не хранится — нужен отдельный список")
«Та же логика на Python ▶». defaultdict(int) точно повторяет поведение маппинга: любой неустановленный ключ читается как 0, без ошибки.
Частые ошибки
- Пытаться «пройтись по всем пользователям» в маппинге циклом — это невозможно, ключи не хранятся списком.
- Думать, что несуществующий ключ кинет ошибку — нет, он вернёт нулевое значение типа.
- Хранить вместе со значением «флаг существования» неправильно: ноль может быть и валидным значением, и «не задано» — иногда нужен отдельный
bool exists.
Best practices
- Если нужна итерация — держите параллельный
address[] usersи добавляйте ключ при первой записи (паттерн «enumerable mapping»). - Используйте вложенные маппинги для отношений «многие ко многим» (allowance), а не массивы пар — это дешевле и проще.
- Помните, что
public-маппинг создаёт геттер по ключу, но не функцию «вернуть всё».
Итоги
Маппинг — это дешёвое и быстрое хранилище ключ-значение с нулём по умолчанию и без возможности итерации. На нём держится почти любой токен. Дальше сгруппируем связанные данные в структуры.