Маппинги: ключ-значение в блокчейне

Маппинг — главная структура данных 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-маппинг создаёт геттер по ключу, но не функцию «вернуть всё».

Итоги

Маппинг — это дешёвое и быстрое хранилище ключ-значение с нулём по умолчанию и без возможности итерации. На нём держится почти любой токен. Дальше сгруппируем связанные данные в структуры.

Проверьте себя
1. Что вернёт чтение маппинга mapping(address => uint256) по ключу, которому ничего не присваивали?
AОшибку «ключ не найден»
BНулевое значение (0)
CСлучайное значение
DАдрес нулевого аккаунта
2. Почему по маппингу нельзя итерироваться напрямую?
AЭто запрещено лицензией
BEVM не хранит список ключей — позиции вычисляются хэшем по требованию
CИтерация стоит слишком много газа и потому заблокирована
DМаппинги хранятся в памяти