Solidity Array Members and Manipulation Techniques

5/5 - (3 votes)
Solidity Array Members and Manipulation Techniques

In this article, we’ll cover an important topic on array members and array manipulation techniques. This knowledge will prove extremely useful as we progress with our journey through Solidity and gain experience.

It’s part of our long-standing tradition to make this (and other) articles a faithful companion, or a supplement to the official Solidity documentation, starting with these docs for this article’s topics.

Array Members

In this section, we’ll list and explain the most important and commonly used array members, meaning both properties and functions.

Length

length

The length array member holds a number representing the number of elements stored in the array.

The length of memory arrays is fixed, meaning once it’s created, the array is unchanged; also, a memory array is dynamic in that its pre-initialization length can be configured during the runtime via runtime parameters.

push()

push()

Dynamic storage (not memory) arrays and bytes types, excluding string types, contain a member function push() – note there are no parameters.

We use .push() to add, i.e. append a zero-initialized element (remember the default values) at the end of an array. Calling the function, a reference to the element is returned, and we can use it to access or initialize the element, e.g. in an array called x, we’d use one of the two:

  • x.push().t = 2 or
  • x.push() = b.

⚡ Warning: If we tried pushing an element on a memory array, we’d get an error message: TypeError: Member "push" is not available in uint256[2] memory[] memory outside of storage.

push(x)

push(x)

This member function is a close relative of push(), available with dynamic storage arrays and bytes types, excluding string types.

We use this function to append an element to the end of an array. Since the element is already given to the function, the function returns nothing.

pop()

pop()

This member function is available with dynamic storage arrays and bytes types, excluding string types. We use this function to remove an element from the end of the array; it also calls delete on the removed element.

As opposed to some other programming languages that return the popped (removed) element, the pop() function returns nothing.

ℹī¸ Note: By increasing the length of a storage array via the push() member function (the simpler, no-parameters variant), the gas cost is constant because the element is always the same, i.e. the array’s default value is used in zero-initialization.

However, we should be aware that by calling pop(), the gas cost can vary greatly.

The reason for this lies in the size of the element is removed, i.e. it’s much cheaper to remove a simple-type element, than an array element, which includes explicitly clearing the removed elements as if delete was used:

delete … can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements set to their initial value”

Source: https://docs.soliditylang.org/en/v0.8.15/types.html#delete

ℹī¸ Note: Before using arrays containing other arrays in external (instead of public) functions, we’d have to activate ABI coder v2 (in versions where it’s not activated automatically).

ℹī¸ Note: EVM versions prior to Byzantium didn’t support accessing functions that return dynamic arrays. To enable such function calls, we have to make sure that our EVM is set to at least Byzantium release/fork/version.

Example

In the following example, we’ll showcase several of the array manipulation techniques described so far.

I’ve replaced some of the original in-code comments with the regular text comments that I found necessary to improve the clarity of the code.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract ArrayContract {
    uint[2**20] aLotOfIntegers;

Remember how we declare a multidimensional array: bool[2] is an array of pairs, and adding [] makes it a dynamic array of pairs. That’s why T[] is always a dynamic array, even if T itself is an array.

    // Data location for all state variables is storage.
    bool[2][] pairsOfFlags;

    // newPairs is stored in memory - the only possibility
    // for public contract function arguments
    function setAllFlagPairs(bool[2][] memory newPairs) public {
        // assignment to a storage array performs a copy of 'newPairs' and
        // replaces the complete array 'pairsOfFlags'.
        pairsOfFlags = newPairs;
    }

    struct StructType {
        uint[] contents;
        uint moreInfo;
    }

    StructType s;

    function f(uint[] memory c) public {
        // stores a reference to 's' in 'g'
        StructType storage g = s;
        // also changes 's.moreInfo'.
        g.moreInfo = 2;

ℹī¸ Note: Assigning a reference type value to a name (variable) in a different memory area will always incur a content copy operation, i.e., it will produce a cloned instance of the reference type value, assigned to a name.

Here we’re assigning content from memory to storage, so a copy operation will be performed.

        g.contents = c;
    }

Sets boolean values (0 or 1) for an indexed pair.

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // access to a non-existing index will throw an exception
        pairsOfFlags[index][0] = flagA;
        pairsOfFlags[index][1] = flagB;
    }

Resizes the dynamically-sized array pairsOfFlags by pushing additional default values (notice the pop() function without arguments) or popping the elements until the desired array size is reached.

    function changeFlagArraySize(uint newSize) public {
        // using push and pop is the only way to change the
        // length of an array
        if (newSize < pairsOfFlags.length) {
            while (pairsOfFlags.length > newSize)
                pairsOfFlags.pop();
        } else if (newSize > pairsOfFlags.length) {
            while (pairsOfFlags.length < newSize)
                pairsOfFlags.push();
        }
    }

    function clear() public {
        // these clear the arrays completely
        delete pairsOfFlags;
        delete aLotOfIntegers;
        // identical effect here
        pairsOfFlags = new bool[2][](0);
    }

    bytes byteData;

    function byteArrays(bytes memory data) public {
        // byte arrays ("bytes") are different as 
        // they are stored without padding,
        // but can be treated identical to "uint8[]"

The copying occurs due to the same reason as above:

  • the content source is memory, and
  • the destination is storage.
        byteData = data;

Seven values are added to byteData.

        for (uint i = 0; i < 7; i++)
            byteData.push();
        byteData[3] = 0x08;
        delete byteData[2];
    }

Adds one more pair of flags to the array by copying the content (from memory to storage).

    function addFlag(bool[2] memory flag) public returns (uint) {
        pairsOfFlags.push(flag);
        return pairsOfFlags.length;
    }

Creates, populates, and returns a memory array.

    function createMemoryArray(uint size) 
        public pure returns (bytes memory) 
    {
        // Dynamic memory arrays are created using `new`:
        uint[2][] memory arrayOfPairs = new uint[2][](size);

        // Inline arrays are always statically-sized and if you only
        // use literals, you have to provide at least one type.
        arrayOfPairs[0] = [uint(1), 2];

        // Create a dynamic byte array:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = bytes1(uint8(i));
        return b;
    }
}

Conclusion

In this article, we learned about array members, manipulation techniques, and several informative, specific examples showcasing the use of array members.

What’s Next?

This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):