mapping とは
mapping は、Solidityの最も重要なデータ構造のひとつです。キーと値のペア(Key-Value)を格納するハッシュテーブルで、JavaScriptの Map やPythonの dict に近い概念です。
// 基本構文
mapping(キーの型 => 値の型) アクセス修飾子 変数名;
// 例
mapping(address => uint256) public balances;
mapping(uint256 => string) public names;
mapping(address => bool) public isRegistered;
mappingの特徴は以下のとおりです。
- O(1)のアクセス: キーによる読み書きが定数時間で行える
- デフォルト値: 存在しないキーにアクセスすると型のデフォルト値が返る(uint256なら0、boolならfalse、addressなら0x0)
- 列挙不可: キーの一覧を取得する方法がない(自分で管理する必要がある)
- ストレージ専用: memory変数やローカル変数としては使えない
基本的なmappingの使い方
アドレスごとのポイント管理をする簡単なコントラクトで、mappingの基本操作を学びましょう。
// contracts/PointSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract PointSystem {
// アドレス => ポイント残高
mapping(address => uint256) public points;
// 登録済みアドレスの一覧(mappingは列挙できないため配列で管理)
address[] public registeredUsers;
mapping(address => bool) public isRegistered;
event PointsAdded(
address indexed user,
uint256 amount,
uint256 newBalance
);
event PointsUsed(
address indexed user,
uint256 amount,
uint256 newBalance
);
/// @notice ポイントを付与する
function addPoints(address user, uint256 amount) public {
if (!isRegistered[user]) {
isRegistered[user] = true;
registeredUsers.push(user);
}
points[user] += amount;
emit PointsAdded(user, amount, points[user]);
}
/// @notice ポイントを使用する
function usePoints(uint256 amount) public {
require(points[msg.sender] >= amount, "Insufficient points");
points[msg.sender] -= amount;
emit PointsUsed(msg.sender, amount, points[msg.sender]);
}
/// @notice ポイント残高を確認する
function getBalance(address user) public view returns (uint256) {
return points[user];
}
/// @notice 登録ユーザー数を取得する
function getUserCount() public view returns (uint256) {
return registeredUsers.length;
}
}
テスト
// test/PointSystem.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
describe("PointSystem", function () {
async function deployFixture() {
const [owner, user1, user2] = await ethers.getSigners();
const PointSystem = await ethers.getContractFactory("PointSystem");
const points = await PointSystem.deploy();
return { points, owner, user1, user2 };
}
it("ポイントを付与して残高を確認できる", async function () {
const { points, user1 } = await deployFixture();
await points.addPoints(user1.address, 100);
expect(await points.getBalance(user1.address)).to.equal(100);
// さらにポイントを追加
await points.addPoints(user1.address, 50);
expect(await points.getBalance(user1.address)).to.equal(150);
});
it("ポイントを使用できる", async function () {
const { points, user1 } = await deployFixture();
await points.addPoints(user1.address, 100);
await points.connect(user1).usePoints(30);
expect(await points.getBalance(user1.address)).to.equal(70);
});
it("残高不足の場合はエラーになる", async function () {
const { points, user1 } = await deployFixture();
await points.addPoints(user1.address, 50);
await expect(
points.connect(user1).usePoints(100)
).to.be.revertedWith("Insufficient points");
});
it("未登録アドレスの残高は0", async function () {
const { points, user2 } = await deployFixture();
// 存在しないキーのデフォルト値は0
expect(await points.getBalance(user2.address)).to.equal(0);
});
});
ネストされた mapping
mappingの値としてさらにmappingを使うことで、2次元以上のデータ構造を表現できます。
// contracts/AccessControl.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract AccessControl {
// ロール名 => (アドレス => 権限の有無)
mapping(string => mapping(address => bool)) private roles;
// アドレス => (コントラクトアドレス => 承認額)
mapping(address => mapping(address => uint256)) public allowances;
event RoleGranted(string role, address indexed account);
event RoleRevoked(string role, address indexed account);
address public admin;
constructor() {
admin = msg.sender;
roles["admin"][msg.sender] = true;
}
modifier onlyAdmin() {
require(roles["admin"][msg.sender], "Not admin");
_;
}
/// @notice ロールを付与する
function grantRole(
string memory role,
address account
) public onlyAdmin {
roles[role][account] = true;
emit RoleGranted(role, account);
}
/// @notice ロールを取り消す
function revokeRole(
string memory role,
address account
) public onlyAdmin {
roles[role][account] = false;
emit RoleRevoked(role, account);
}
/// @notice ロールを持っているか確認する
function hasRole(
string memory role,
address account
) public view returns (bool) {
return roles[role][account];
}
/// @notice 承認額を設定する
function setAllowance(
address spender,
uint256 amount
) public {
allowances[msg.sender][spender] = amount;
}
/// @notice 承認額を確認する
function getAllowance(
address owner,
address spender
) public view returns (uint256) {
return allowances[owner][spender];
}
}
ネストされたmappingのテスト
describe("AccessControl", function () {
it("ロールの付与と確認ができる", async function () {
const [admin, curator, visitor] = await ethers.getSigners();
const AC = await ethers.getContractFactory("AccessControl");
const ac = await AC.deploy();
// 学芸員ロールを付与
await ac.grantRole("curator", curator.address);
expect(await ac.hasRole("curator", curator.address)).to.be.true;
expect(await ac.hasRole("curator", visitor.address)).to.be.false;
expect(await ac.hasRole("admin", admin.address)).to.be.true;
});
it("承認額の設定と確認ができる", async function () {
const [owner, spender] = await ethers.getSigners();
const AC = await ethers.getContractFactory("AccessControl");
const ac = await AC.deploy();
await ac.setAllowance(spender.address, 1000);
expect(
await ac.getAllowance(owner.address, spender.address)
).to.equal(1000);
// 別のアドレスの承認額は0
expect(
await ac.getAllowance(spender.address, owner.address)
).to.equal(0);
});
});
構造体(struct)とmappingの組み合わせ
mappingの値として構造体を使うのは、非常に一般的なパターンです。
// contracts/CulturalItemRegistry.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CulturalItemRegistry {
struct CulturalItem {
string name;
string institution;
string dateCreated;
address registeredBy;
uint256 registeredAt;
bool exists;
}
// itemId => CulturalItem
mapping(uint256 => CulturalItem) public items;
// 所蔵機関 => アイテムIDの配列
mapping(string => uint256[]) public itemsByInstitution;
// 登録者 => アイテムIDの配列
mapping(address => uint256[]) public itemsByRegistrar;
uint256 public nextItemId;
event ItemRegistered(uint256 indexed itemId, string name);
function registerItem(
string memory name,
string memory institution,
string memory dateCreated
) public returns (uint256) {
uint256 itemId = nextItemId++;
items[itemId] = CulturalItem({
name: name,
institution: institution,
dateCreated: dateCreated,
registeredBy: msg.sender,
registeredAt: block.timestamp,
exists: true
});
// 逆引き用のマッピングも更新
itemsByInstitution[institution].push(itemId);
itemsByRegistrar[msg.sender].push(itemId);
emit ItemRegistered(itemId, name);
return itemId;
}
/// @notice アイテムを取得する
function getItem(uint256 itemId)
public view returns (CulturalItem memory)
{
require(items[itemId].exists, "Item not found");
return items[itemId];
}
/// @notice 所蔵機関ごとのアイテムIDリストを取得
function getItemsByInstitution(string memory institution)
public view returns (uint256[] memory)
{
return itemsByInstitution[institution];
}
/// @notice 登録者ごとのアイテムIDリストを取得
function getItemsByRegistrar(address registrar)
public view returns (uint256[] memory)
{
return itemsByRegistrar[registrar];
}
}
mappingを使う際の注意点
1. 列挙(イテレーション)ができない
mappingのすべてのキーをループで回すことはできません。キーの一覧が必要な場合は、別途配列で管理します。
// 悪い例: mappingだけでは全ユーザーの一覧が取れない
mapping(address => uint256) public balances;
// 良い例: 配列でキー一覧を管理する
mapping(address => uint256) public balances;
address[] public users;
2. deleteの挙動
delete を使うとデフォルト値にリセットされますが、キー自体は消えません。
mapping(address => uint256) public data;
data[msg.sender] = 100;
delete data[msg.sender];
// data[msg.sender] は 0 に戻るが、キーが「削除」されたわけではない
3. ガスコストの考慮
ストレージへの書き込みはガスコストが高い操作です。mappingの値を頻繁に更新する場合はガスコストに注意が必要です。
まとめ
本章では、Solidityの mapping について学びました。基本的な使い方から、ネストされたmapping、構造体との組み合わせまでを実践しました。mappingはスマートコントラクト開発で最も頻繁に使うデータ構造であり、しっかりと理解しておくことが重要です。
次章では、modifier を使ったアクセス制御について学びます。