Mapping Types - Solidity Part 3.3

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:

  1. 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.
  2. 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 return 0.
  3. 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.
  4. 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:

  1. 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.
  2. 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.
  3. 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 a Voter struct, tracking whether the voter has voted and their choice.
  • votes tracks the total votes for each option (1 for Option A and 2 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.

Share:
spacer