Solidity / Mapping

Mapping

mapping бол Solidity-ийн хамгийн чухал өгөгдлийн бүтэц — түлхүүр (key) → утга (value) хосоор хязгааргүй тооны өгөгдөл хадгалдаг. ERC-20 token, NFT өмчлөл, хэрэглэгчийн үлдэгдэл зэрэг бараг бүх contract-д mapping ашиглагддаг.

Синтакс

solidity
mapping(KeyType => ValueType) visibility variableName;
solidity
mapping(address => uint256) public balances;
mapping(uint256 => address) public tokenOwner;
mapping(address => bool)    public isWhitelisted;
mapping(bytes32 => string)  public messages;

KeyType байж болох төрлүүд: uint, int, address, bytes, string, bool — бараг бүх value type.

ValueType нь дурын төрөл байж болно — тэр дотроо өөр mapping байж болно.

Хамгийн энгийн жишээ

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Wallet {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function getBalance(address user) public view returns (uint256) {
        return balances[user];
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Үлдэгдэл хүрэлцэхгүй");
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }
}

balances[msg.sender] нь "msg.sender хаягийн үлдэгдэл" гэсэн утга. JavaScript-ийн object[key]-тай синтакс адил.

Default утга

Mapping-д хадгалаагүй key-г уншихад алдаа гарахгүй — өгөгдлийн төрлийн default утгыг буцаана:

solidity
mapping(address => uint256) public scores;
mapping(address => bool)    public hasVoted;
mapping(address => string)  public names;

// Хадгалагдаагүй хаягийг унших:
scores[0x1234...]     // → 0      (uint256-ийн default)
hasVoted[0x1234...]   // → false  (bool-ийн default)
names[0x1234...]      // → ""     (string-ийн default)

Энэ нь маш ашигтай — require(balances[user] > 0) шалгалтыг шинэ хэрэглэгчид ч аюулгүй ажиллуулна.

Nested mapping — mapping дотор mapping

ValueType-д өөр mapping тавьж хоёр түлхүүртэй хадгалах боломжтой.

solidity
// ERC-20 стандартын allowance: A нь B-д хэдий зарцуулахыг зөвшөөрсөн
mapping(address => mapping(address => uint256)) public allowance;

// Тохируулах:
function approve(address spender, uint256 amount) public {
    allowance[msg.sender][spender] = amount;
}

// Унших:
function getAllowance(address owner, address spender) public view returns (uint256) {
    return allowance[owner][spender];
}

Гурван түвшний nested mapping ч бичиж болно, гэхдээ уншихад хэцүү болдог тул хоёр түвшин хангалттай:

solidity
// Тоглоомын мэдээлэл: тоглогч → орц → оноо
mapping(address => mapping(uint256 => uint256)) public levelScore;

levelScore[player][1] = 500;   // тоглогч 1-р орцноос 500 оноо авсан
levelScore[player][2] = 800;

Mapping-г iterate хийж болохгүй

Solidity-д mapping-г давтаж (loop) уншиж чадахгүй — энэ нь хамгийн чухал ялгаа:

solidity
mapping(address => uint256) public balances;

// ❌ БОЛОМЖГҮЙ — compile error
for (address key in balances) { }

// ❌ БОЛОМЖГҮЙ
balances.length;

Яагаад? Mapping нь Ethereum storage-д hash table хэлбэрээр хадгалагддаг — key бүрийн хаяг keccak256(key, slot) hash-аар тодорхойлогддог. Бүх key-ийн жагсаалт blockchain-д огт хадгалагддаггүй тул давтах боломжгүй.

Давтах шаардлагатай бол тусдаа array хадгална:

solidity
contract Registry {
    mapping(address => uint256) public scores;
    address[] public players;           // хэн бүртгүүлсэн — жагсаалт
    mapping(address => bool) public registered;

    function register() public {
        require(!registered[msg.sender], "Аль хэдийн бүртгэлтэй");

        players.push(msg.sender);       // array-д нэмнэ
        registered[msg.sender] = true;  // mapping-д тэмдэглэнэ
        scores[msg.sender] = 0;
    }

    function getAllPlayers() public view returns (address[] memory) {
        return players;
    }

    // одоо давтаж болно
    function getTopScore() public view returns (uint256 top) {
        for (uint256 i = 0; i < players.length; i++) {
            if (scores[players[i]] > top) {
                top = scores[players[i]];
            }
        }
    }
}

Mapping устгах

delete ашиглан утгыг default болгож болно:

solidity
mapping(address => uint256) public balances;

function removeUser(address user) public {
    delete balances[user];   // balances[user] = 0 болно
}

delete нь утгыг default-д тавих ч array-аас арилгадаггүй — array-г тусдаа удирдах хэрэгтэй.

Практик жишээ: санал өгөх систем

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Voting {
    address public owner;

    mapping(address => bool)    public hasVoted;
    mapping(string  => uint256) public voteCount;
    string[] public candidates;

    constructor() {
        owner = msg.sender;
    }

    function addCandidate(string memory name) public {
        require(msg.sender == owner, "Зөвхөн эзэмшигч");
        candidates.push(name);
    }

    function vote(string memory candidate) public {
        require(!hasVoted[msg.sender], "Аль хэдийн санал өгсөн");

        hasVoted[msg.sender]  = true;
        voteCount[candidate] += 1;
    }

    function getVotes(string memory candidate) public view returns (uint256) {
        return voteCount[candidate];
    }

    function getCandidateCount() public view returns (uint256) {
        return candidates.length;
    }
}

Mapping vs Array

| | Mapping | Array | | ---------------------- | --------------------------- | ---------------------------- | | Хайх хурд | O(1) — тэр даруй | O(n) — давтана | | Iterate хийх | ❌ боломжгүй | ✅ болно | | Дурын key | ✅ address, uint, ... | ❌ зөвхөн index (0, 1, 2...) | | Давхар хадгалалт мэдэх | ❌ | ✅ .length | | Gas | Key-ийн тоогоос үл хамаарна | Element нэмэх бүр нэмэгдэнэ |

Хоёрыг хослуулах нь ихэвчлэн хамгийн сайн шийдэл — mapping хурдан хайхад, array давтахад.

Дараагийн хичээлд:

Array — динамик ба тогтмол урттай жагсаалт, push, pop, delete ажиллагааг судална.