Data Structures

Helping Us Store Data

Any programming language would not be complete without the inclusion of data structures; in Solidity, we are provided with the following data structures:

Mappings

Mappings can be thought of as dictionaries, where we are mapping keys to values. The syntax for mappings are as follows:

mapping(keyType => valueType) mappingName;

As an example, if I wanted to create a mapping called ages which mapped users (represented by their addresses) to their age, I could create the following mapping:

mapping(address => uint) userAges;

Perhaps one of best known use cases of mappings is for tracking the ERC20 token balances of user within a smart contract. Listed below is an elementary ERC20 contract which shows this off:

contract BabyERC20 {

    // Tracks balances of token holders
    mapping(address => uint) balances;

}

Assuming map is a mapping, the following code below demonstrates how to assign a key-value pair within a mapping:

map[key] = value;

Restrictions on Key/Value Types

As noted in the Solidity documentation, there are restrictions regarding what types keys can be. The types that are not allowed to be used as keys are mappings, structs, and arrays. Furthermore, any user-defined types are not allowed to be used as key types.

One note about mappings is that they can only be used as state variables. In other words, one cannot create a new mapping within a function; one can only use a mapping associated with a state variable within a function.

Aside: Default Values

In languages like Python, we run into exceptions whenever we try to query an undefined key-value pair. An example is as follows:

>>> x = {}
>>> x["key"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'key'

In Solidity, however, we are able to query undefined key-value pairs (assuming we obey relevant type rules). An interesting question, then, is what values are returned when querying an undefined key? For any undefined key, the value returned is the default value for the value type. As an example, assume we have the following (empty) mapping:

mapping(uint => uint) map;

If we try to query the value paired to key 7, we will get the value 0. If we try to query any key in map, we will always get the value 0 in return. Listed below are the default values for some types:

  • uint/int: 0

  • bool: false

Static Arrays

Static arrays are arrays whose size is defined at creation time. The syntax for creating an array in memory is as follows:

T[] arrayName = new T[](n);

where T is the type of the array and n is the size of the array. In Solidity, all elements of an array must be of the same type. Another way of creating an array in memory is as follows:

T[n] arrayName;

As the name might imply, we cannot change the size of a static array once initialized. If you wish to have flexibility with the size of your array, consider using a dynamic array.

To create a state variable of the static array type, we can again use the folowing syntax:

T[n] arrayName;

In the case that we know how to define our static array, below is an example of how we can declare and define a static array at the same time (this holds for both memory and storage):

uint[2] lst = [1, 2];

Nested Arrays

In certain situations, it may be necessary to use a nested array. Solidity allows us to create static nested arrays as follows:

T[n][m] nestedArrayName;

where T is the type of the array, and n, m are the 1st and 2nd dimension sizes of the nested array respectively.

Accessing Array Elements

Below is an example of how to access an element of an array:

lst[0];

Structs

Structs are data structures that can be used to hold several variables in one place; below is an example of structs in the C++ programming language:

struct {
    string name;
    int age;
    bool isAdult;
} Person;

In Solidity, we can create structs as follows:

struct Person {
    string name;
    int age;
    bool isAdult;
}

Below is an example of how to create a struct in memory and how to access an element of a struct:

Person memory p = Person("Rodrigo", 21, false);
p.age;

Enums

Enums, or enumerations, allow us to group 'constants' without having to explicitly declare them.

Consider a contract where we want to organize students by their year. If we wanted to map students (represented by their addresses) to their year, we could use integers to represent their year, as seen below:

0 - Freshman
1 - Sophomore
2 - Junior
3 - Senior

However, keeping track of these constants manually introduces the possibility of bugs. Enums allow us to use these constants without explicitly using their values:

enum Year {
    Freshman, // 0
    Sophomore, // 1
    Junior, // 2
    Senior // 3
}

Enum Values

The elements of an enum are associated with an integer determined by their position. For example, the first element is equal to 0, the second element is equal to 1, and so on.

Below is an example of a contract which uses the Year enum to keep track of students:

contract Cornell {

    enum Year {
        Freshman, 
        Sophomore, 
        Junior, 
        Senior 
    }

    mapping(address => Year) students;

    function admitStudent(address student) public {
        students[student] = Year.Freshman;
    }

}

The function admitStudent, in particular, takes the address of an admitted students and maps them to the Freshman value.

References

Solidity Types: https://docs.soliditylang.org/en/v0.8.7/types.html#

Solidity Expressions and Control Structures: https://docs.soliditylang.org/en/latest/control-structures.html#

Last updated