Mappings are one of the key data structures in Solidity, used extensively to create associations between data. If you’re familiar with the concept of hash tables or dictionaries in other programming languages, mappings in Solidity serve a similar purpose. However, they come with unique characteristics and limitations due to the blockchain’s nature.
Source Code: https://github.com/scaihai/enkwadore-blog-blockchain-demos/tree/main/solidity/contracts/3.3
What is a Mapping?
A mapping is a reference type in Solidity that allows you to store data in a key-value pair format. The key can be of any elementary type, such as uint
, address
, bytes
, or even bool
, while the value can be any type, including another mapping or an array.
Here’s a basic example of how a mapping is declared:
// Mapping of addresses to integers mapping(address => uint) public balances;
In this example, balances
is a mapping where each Ethereum address (address
) is associated with an unsigned integer (uint
), representing the balance of that address.
Accessing and Modifying Mappings
You can access and modify the values stored in a mapping using the key. For example:
// Setting a balance for an address balances[msg.sender] = 100; // Accessing the balance of an address uint balance = balances[msg.sender];
The above code snippet demonstrates how to set a balance of 100
for the message sender (msg.sender
) and how to retrieve the balance later.
Properties and Characteristics of Mappings
Mappings in Solidity have some unique properties:
- Non-Iterable: Mappings are not iterable. You cannot loop through a mapping to get all the keys or values. This is a significant difference from arrays or structs.
- Default Values: If you query a key that has not been set yet, the mapping will return the default value for the type. For example, if you try to access a
uint
mapping with a key that hasn’t been assigned a value, it will return0
. - Storage Efficiency: Mappings are more storage-efficient than arrays because they don’t store keys. Instead, the key is hashed to find the corresponding value’s storage location.
- Gas Costs: Reading from a mapping is relatively cheap in terms of gas, while writing is more expensive due to storage costs on the blockchain.
Nested Mappings
Solidity allows nested mappings, where the value itself is another mapping. This is useful in scenarios where you need to create multi-dimensional associations.
// Nested mapping from address to another mapping mapping(address => mapping(uint => bool)) public nestedMapping;
In this example, nestedMapping
is a mapping where each address maps to another mapping that maps a uint
to a bool
.
Here’s how you might work with a nested mapping:
// Setting a value in the nested mapping nestedMapping[msg.sender][1] = true; // Accessing a value from the nested mapping bool value = nestedMapping[msg.sender][1];
Use Cases for Mappings
Mappings are highly versatile and are often used in a variety of smart contract applications. Some common use cases include:
- Token Balances: Track balances of users in a token contract.
- Voting Systems: Store votes or voter statuses.
- Access Control: Manage permissions or roles within a contract.
Limitations of Mappings
While mappings are powerful, they come with limitations that developers need to be aware of:
- No Length Property: Unlike arrays, mappings do not have a length property, meaning you can’t directly determine the number of elements stored in a mapping.
- No Enumeration: You can’t list all the keys stored in a mapping. This makes mappings unsuitable for situations where you need to access all elements.
- No Key Existence Check: Solidity doesn’t provide a built-in way to check if a key exists in a mapping. You have to rely on the default value behavior to infer this.
Best Practices with Mappings
- Use Structs for Complex Data: If you need to store multiple values associated with a key, consider using a struct as the value type in your mapping.
- Delete Unused Data: Although mappings automatically return default values, you can delete entries to save gas and avoid confusion.
delete balances[msg.sender];
- Design with Non-Iterability in Mind: Since mappings can’t be iterated, design your contract logic accordingly. If you need to keep track of all keys, maintain a separate array.
Example: A Simple Voting Contract
To illustrate mappings in action, let’s build a simple voting contract:
Voting2.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.7; contract Voting2 { struct Voter { bool voted; uint8 vote; // 0 for no vote, 1 for option A, 2 for option B } mapping(address => Voter) public voters; mapping(uint8 => uint) public votes; // Mapping to store the vote count for each option function vote(uint8 _vote) external { require(!voters[msg.sender].voted, "Already voted."); require(_vote == 1 || _vote == 2, "Invalid vote."); voters[msg.sender] = Voter(true, _vote); votes[_vote] += 1; } function getVotes(uint8 _option) external view returns (uint) { return votes[_option]; } }
In this contract:
voters
maps each address to aVoter
struct, tracking whether the voter has voted and their choice.votes
tracks the total votes for each option (1
for Option A and2
for Option B).
This example showcases how mappings and structs can be combined to create a simple yet functional voting system.
Conclusion
Mappings are a fundamental part of Solidity and are essential for creating robust and efficient smart contracts. By understanding their properties, limitations, and best practices, you can leverage mappings to build secure and scalable decentralized applications. While they come with certain constraints, their ability to store and quickly retrieve data makes them indispensable in the Ethereum ecosystem.