Introduction to Solidity : Reference and Mapping Types

In this post we will continue to discover Solidity by learning about the reference types and mapping types. Let's start with the reference types.

Reference Types

Reference types are types whose values refer to a location in memory or storage where the actual value is stored. This is the opposite of value types, where the value is stored in the variable itself and where you get an independent copy whenever the variable is used.

The reference types are Arrays (including string and bytes) ,Structs and Mappings.

Data Location

When we use a reference type, we always need to provide the data location where the value is stored. This data location can be either memory, storage or calldata.

Memory

It's a temporary data location that is used to hold data during contract execution. It's much cheaper to access than storage. Memory is cleared between external function calls, so any data stored in memory is only available for the duration of the current function call. Any data that needs to be returned to the caller of the function must be stored in memory.

Storage

It's a permanent data location that is used to store data permanently. When a function modifies a state variable, it modifies the value stored in storage.Storage is persistent between function calls, this mean data will be available for the duration of the contract.

Calldata

It's a data location that contains the arguments of the current function call. It's read-only and can only be accessed by the function that is currently being executed. It's also cheaper to access than memory because it doesn't copy the value to memory or memory.

Here's how we use this differents data location:

  function test1(uint[] storage _data) public {
    ...
  }

  function test2(uint[] memory _data) public {
    ...
  }

  function test3(uint[] calldata _data) public {
    ...
  }

By default if nothing is specified, the data location is memory.

How assignment works

When we assign a value from storage to memory or calldata or vice versa, the value is copied. This means that if we modify the value in memory or calldata, it won't affect the value in storage.

In contrast assignments from memory to memory create a reference to the original data, meaning that changes made to one variable will be reflected in all other variables that reference the same data. This is also the case for storage to local storage.

Finally all others assignment to storage will create a copy.

Arrays

In solidity arrays can be fixed or dynamic. Fixed arrays have a fixed length. Dynamic arrays have a dynamic length and can instanciated with an initial size than can be resized later. They are written like this T[k] or T[] where T is the type of the array and k is the length of the array.

  uint[5] fixedArray;
  uint[] dynamicArray;

Their indices are zero-based and they can be accessed using the [] operator.

  fixedArray[0] = 1;
  dynamicArray[0] = 1;

They can be of any types including mapping or struct, but ares subject to restrictions on data location and visibility.\

For example, mappings can only be stored in the storage data location and not in memory or calldata. Similarly, when using a public function, the function parameters must be of ABI types, which do not include mapping or struct types.

This means that if you want to use a mapping or struct type in a public function, you need to create a wrapper function that takes parameters of ABI types and then calls the internal function that takes the mapping or struct type parameters.

Array can be marked as public this will automatically create a getter function for it. This getter function will have the same name as the array and will take one parameter that is the index of the element to get.

You can append new element to an array using the push function.

  dynamicArray.push(1);

And finally if you try to access an element that is out of bounds, the contract will causes a failing assertion.

Special case of string and bytes

The bytes and string types are specialized arrays in Solidity. The bytes type is similar tobytes1[], but it is more compact in calldata and memory. The string type is equivalent to bytes but does not support length or index access.

  string myString = "Hello";
  bytes myBytes = "Hello";

While Solidity does not provide built-in string manipulation functions, there are third-party string libraries available. You can compare two strings using their keccak256-hash by applying keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)). To concatenate two strings, you can use the string.concat(s1, s2) function.

It is recommended to use the bytes type over bytes1[] due to its lower cost. Using bytes1[] in memory introduces 31 padding bytes between the elements, while in storage the padding is absent because of tight packing. For raw byte data of arbitrary length, use bytes, and for arbitrary-length string (UTF-8) data, use string. If the length of the data can be limited to a certain number of bytes, use one of the value types bytes1 through bytes32, as they are more cost-effective.

Allocation of arrays in memory and litterals

You can create an dynamic array in memory but in contrast of storage allocation you can't resize it. The size of the array is fixed at creation time.So you can't use the push function.

  uint[] memory a = new uint[](7);

So basically when you create a dynamic array in memory, like the one above, it will work the same way as a fixed array because both can't be resized.

  uint[] memory a = new uint[](7);
  uint[7] memory b;

You can also use array with litterals.

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

The base type of the array is inferred from the type of the first elements such that all other elements can be implicitly converted to it. It is a type error if this is not possible.

  uint[] memory a = [uint8(1), 2, 3];

Mapping Types

Mapping is a data structure that is used to store data in key-value pairs. It is similar to an associative array or hash table. The key of a mapping can be any type except a mapping type itself or other user-defined or complex types, such as mappings, structs or array types. The value of a mapping can be of any type, including mappings and structs.

It's declared like that : mapping(KeyType => ValueType)

  mapping(uint => string) public names;
  mapping(address => mapping(uint => Book)) public shelves;

or mapping(KeyType KeyName? => ValueType ValueName?)

  mapping(address user => uint balance) public balances;

They can only be declared in storage data location and can't be used as parameter or return value of function that are public. This is also true for array and struct that contain mappings.

Mapping can be public and will automatically create a getter function for it. This getter function will have the same name as the mapping and will take one parameter that is the key of the mapping to get.

Conclusion

That's it for this part. In the next part we will discover Operators and Conversions between Elementary Types.