Arrays are reference types, which means, they don’t store their values directly. Instead, reference types hold the address where the value is stored. This also means, the value of a reference type can be modified through different variable names that hold the same address.

So, if you assign one reference type variable to another, you are not creating a copy of the actual data, instead you are passing the address that points to the memory location that holds the actual data.

When you use a reference type (like an array) you also need to specify the location where the data is stored. The only exception to that rule are state variables. State variables are always stored in the “storage” data location and you don’t need to specify the storage keyword.

In Solidity, we have 3 data locations for reference types: storage, memory and calldata:
  • storage: data is stored permanently and available for the lifetime of the contract. All state variables are stored on the storage data location. Storing data permanently on the blockchain is very expensive and incurs high gas fees.
  • memory: this is a non-persistent data store and the data is only available during the function call. Storing data in memory is relatively cheap.
  • calldata: this is a special data location that is only available for function arguments. Calldata is even cheaper than memory and it prevents data from being modified. Otherwise, calldata behaves exactly as memory.
Fixed and dynamic arrays:

As with most high-level programming languages, you can define dynamic and fixed size arrays in Solidity.

For a fixed size array, you need to specify the number of elements the array should contain. For example, for an array of 3 uint elements you can write: uint[3] myArray;

To make this array dynamic, you simply write: uint[] myArray;

Getter functions for arrays:

When you declare an array as a public state variable, a getter is created automatically. However, the getter allows you to retrieve only one element at a time. So, when you call the getter function you also need to specify the index of the array element you want to return.

Array member functions:

In Solidity, there is only one member function that is available for all array types: The .length property that returns the number of array elements.

For dynamic storage arrays, we have some additional functions:

  • push(): adds a zero-initialized element at the end of the array
  • push(someValue): adds the specified value to the end of the array
  • pop(): removes one element from the end of the array

Don’t forget, those member functions are only available on storage arrays, not on memory arrays.

Creating arrays and assigning values to array elements:

We create a storage array by defining an array-type state variable. The array can either be fixed or dynamic:

uint[] public dynamicArray;
uint[2] public fixedArray;

We can use array literals to assign values to the array:

uint[] public dynamicArray = [1, 2, 3, 4];
uint[2] public fixedArray = [1, 2];

Careful, storage is always pre-allocated during contract construction and cannot be created later on (for example, during a function call). So, if we define a storage array in a function, it needs to refer to an already existing storage array that was created during contract construction, for example:

uint[] storage myStorageArray = dynamicArray

To create a memory array in a function, you can use the “new” keyword. Afterwards, you can assign values to the various array elements, for example:

uint[ ] memory memoryArray = new uint[ ](3);
memoryArray[0] = 5; …

You can also use array literals with memory arrays:

uint[3] memory memoryArray2 = [1, 2, 3];

However, it is not possible to resize a memory array – the push and pop functions are not available on memory arrays!

Defining the correct base type of a memory array when using literals:

The base type of the array is the type of the first element and it must be possible to implicitly convert all other elements into the same type otherwise the compiler will generate an error.

For example, if you provide the following array elements as literals : [1, 2, 3], the corresponding array type needs to be uint8[], because each element in the list is of type uint8.

If you have the following list of elements: [1, 2, 256], the corresponding array type needs to be uint16[], because 256 is of type uint16.

If you want to define this array as uint (instead of uint8 or uint16), you need to convert at least one of the listed array literals to uint, for example: [uint(1), 2, 3]

Array of arrays – 2-dimensional arrays:

In Solidity, we can also use 2-dimensional arrays. For example, an array of 3 dynamic uint arrays would be defined in the following way:

uint[][3] memory my2DimArray = [[1,2], [3,4,5,6], [7]];

To access, let’s say the fourth element of the second dynamic array you would write:

my2DimArray[1][3]

Indices are zero-based and the array elements are accessed in the opposite direction of the declaration.

Assignments between arrays:

As you already know, arrays are reference types and when you assign between two reference types you are passing an address that points to the corresponding data location instead of directly copying the data.

However, there are exceptions to that rule:
  • Assignments between 2 storage arrays create a reference
  • Assignments between 2 memory arrays also create a reference
  • But, assignments between a storage and a memory array create a copy!
Strings and bytes are also arrays:

The types string and bytes are considered as dynamic arrays. Bytes are used for raw byte data and strings are used for UTF-8 encoded data.

Strings are a particular case – they don’t have the length function and individual characters of the string cannot be accessed by index. Strings are quite expensive (high gas fees) and should only be used when there is no alternative.

Finally, let’s play around with what we have learned so far in Remix:

 

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract Arrays {
    // Dynamic & Fixed State Variable (Storage)
    uint[] public arr1 = [1,2,3]; 
    uint[2] public arr2; // we can also assign values : [1,2];

    function testArray() public {

        // *** Modify State Variables
        arr1.push(4);  
        //arr2.push(4); // ERROR!!!
        //delete arr1;
        //arr1[0] = 6; // ERROR!!! => use push()

        // *** Fixed Memory Array
        uint[3] memory arr3 = [21,22,uint(23)]; // works only with memory        
        uint8[3] memory arr4;
        arr4[0] = 1;

        arr1 = arr3; // Memory to Storage
        //arr3 = arr1; // ERROR!!!

        uint[] memory arr5;   
        arr5 = arr1; // this works     
        arr5[0] = 7; // ERROR!!! if we don't use arr5=arr1 before        

        uint[] memory arr6 = new uint[](3); // works only with memory
        arr6[0] = 31;

        // *** Dynamic Memory Array        
        uint[] memory arr7 = arr1; // needs to be same type: uint
        arr7[0] = 10;
        //uint8[] memory arr8 = [21,22,23]; // ERROR!!!
        
        // *** Storage Array
        uint[] storage arr9 = arr1; 
        arr9[0] = 5;

        //uint8[3] storage arr10 = [21,22,23]; // ERROR!!!
        //uint[] storage arr11 = new uint[](3); // ERROR!!! 
        //uint[] storage arr12; 
        //arr12[0] = 5; // ERROR!!!
    }
}

State variables:

The code is pretty much self-explanatory. At the beginning of the contract, we define a dynamic and a fixed-size storage array of type uint.

Assigning data to our state variables:

Then, we assign some data to our state variables. Careful, we cannot use the push and pop functions on a fixed-size array (arr2).

Using the “delete” keyword, allows us to delete all elements from our array. However, after deleting all elements, we can no longer assign a value to a specific array position. We need to use the push function to add a new element to our now empty dynamic array.

Using fixed-size memory arrays:

We define a fixed-size memory array of 3 elements. The provided values are all of type uint8, so, if we want to use uint as the type of our array, we need to convert at least one of the array elements to uint. To assign values to the array, we can either use literals (first example) or provide values for specific array indices. (second example).

We can also assign the fixed-size memory array to our dynamic storage array (state variable: arr1). However, the opposite is not possible, because the dynamic storage array (arr1) cannot be converted into a fixed-size array.

However, we can assign the dynamic storage array (arr1) to a dynamic memory array (arr5). But, careful, you can’t assign individual values to a newly defined dynamic memory array, you first have to assign an existing dynamic array to it.

We can also use the “new” keyword to create a memory array.

Dynamic memory arrays:

We already used dynamic memory arrays in the previous section, so, there is not really anything new here. Just be aware, that you can’t assign literals to a dynamic memory array, because the literals define a fixed-size memory array.

Storage arrays in a function:

As we already know, storage is pre-allocated during contract creation and cannot be created later on. So, when you create a storage array in a function you cannot directly assign values to it, you need to assign an already existing array to it.

For example, if you assign arr1 (our state variable) to arr9 (which is also of type storage), you can modify the values of arr1 by changing the values of arr9 => remember: assignments between 2 storage arrays create a reference!