If you already have some experience as a developer, many of the terms and concepts you encounter when first diving into Solidity will hopefully be familiar to you, such as Solidity's Array type, which is very similar to arrays found in other programming languages:
string cars [];cars = ['volvo', 'ford', 'tesla', 'honda'];
However, Solidity also has a Mapping type, which may seem similar to an array at first since it is also used to store a group of data. You'll see it take a syntax such as this:
mapping (address => uint) highscores;
However, there are some key differences between mapping
and array
in Solidity that are important to understand as you progress in your journey as a smart contract developer.
In this article, we'll be taking a look at both types and discussing their differences, and explaining (with examples) when to use one instead of the other.
To begin, let's take a quick look at the array
type:
Arrays
An array
in Solidity is much like an array in any other programming language. For example, creating an array of fruit in Solidity might look like this:
string fruit[];fruit = ["apple", "mango", "watermelon"];
You'll notice that array declarations require a value type, such as string
, uint
, or even a struct
. This is because Solidity is a statically typed language.
Just like arrays in other languages, arrays in Solidity also have some methods available for reading and editing them:
string fruit[];fruit = ["apple", "mango", "watermelon"];// let's add another fruit to the array...fruit.push("bacon");// updated array: ["apple", "mango", "watermelon", "bacon"]// wait! bacon isn't a fruit... let's get rid of that last entry real quick:fruit.pop();// updated array: ["apple", "mango", "watermelon"]
Note: Unfortunately, the above methods are the only array methods available in Solidity, other than the .length method, a read-only method that returns the length of the array. It could be said that Solidity arrays have far fewer methods than arrays in other languages because blockchain data is rigid by design, and not meant to encourage that any of its data be altered or erased.
Pretty simple stuff right? I bet you're breezing right through 😎
Arrays: Fixed vs. Dynamic length
One final thing to mention about arrays before moving on is that an array
in Solidity can have either a fixed or dynamic length. In the example above, our array assumes a dynamic length
because no fixed length was declared. This means our array can be any of length and thus can be edited freely.
However, if we wanted our array to only ever have 3 fruits inside of it, we would give it a fixed length when we declared it, like so:
string fruit[3];fruit = ["apple", "mango", "watermelon"];// try to add another fruit to the array...fruit.push("pineapple");// ERROR: array "fruit" is of fixed length 3. You may not add another fruit!
Alright, so I think we've done a good job covering the basics of an array
.
Now let's move on and discuss the mapping
type:
Mappings
A mapping
is similar to an array
in that it is a Solidity reference type meant for storing a group of data.
However, its syntax and structure are quite different, in a way that allows it to serve a unique and important purpose.
For example, take this mapping
, which could be used to store the highscores of each player of a game:
mapping (address => uint) highscores;
Simply put, a mapping
is a table of keys and values (each with a pre-defined type).
You can think of the mapping
above as initiating an empty table that lives inside of your smart contract, and is waiting to be filled in with whatever data you want:
address | highscore |
---|---|
Now let's add some data to our highscores mapping
, and see what happens:
mapping (address => uint) highscores;address playerOne = "0xf39Fd6e51aad23F6F4cd6aB9927279cggFb91166";highscores[playerOne] = 750;
Let's check out our table visualization again:
address | highscore |
---|---|
0xf39...166 | 750 |
Easy right? Now let's talk about why the mapping
type is so important, and what makes it different from an array
.
Mapping: A Closer Look
When a mapping
is created, it assumes that every possible key exists, and is mapped to some value. In
other words, unlike an array
, a mapping
does not have a retrievable length, or any real concept of a key or value being "set".
Note: Technically, a mapping is a hash table - however, knowing all the details of hash tables and how they work is very "under the hood" stuff and isn't a requirement for using the mapping type effectively.
Don't worry - when we say that "every possible key exists", it doesn't mean that you have no control over your mapping
,
or that it comes with a bunch of random key-value pairs already built into it. It just means that a mapping
is not
structured in the same way as an array
, and thus cannot be expected to behave like one.
For example, to fetch a value from your mapping, you must have that value's key. If we forgot what
playerOne
's highscore was and wanted to fetch it, we would need to first know playerOne
's address:
address playerOne = "0xf39Fd6e51aad23F6F4cd6aB9927279cggFb91166"uint playerOneHighScore = highscores[playerOne];// playerOneHighScore = 750
If we did not have a player's address, then we would have no way of knowing their highscore, and in theory their highscore would be irretrievable to us forever.
Whereas with an array
, as a last-ditch effort we could at least loop through the array and print all of the highscores and see if any of them looked familiar. But you cannot loop through a mapping.
"So what's the point? Isn't a mapping just a crappy array then?"
No, not at all! Due to the way that a mapping
is structured, fetching a piece of data from a mapping
(if you have the data's key) is far more efficient than fetching the same data from an array
. To fetch data from an array
requires iterating over the whole array until you find the element you're looking for, but a mapping
will run and grab that data right away (again, so long as you have the key).
This makes mapping
the ideal type for storing basic information about an address, for example, since often there is no need to store the address yourself because it is provided to you by whoever
is interacting with your smart contract (in the form of msg.sender
).
And unfortunately in the world of Ethereum, the fact that transactions that edit data on the blockchain require gas fees means that storing/retrieving data from a smart contract as efficiently as possible can save you and your users lots of money in the long run.
Mapping: Default values
You might be wondering what a mapping
will return as a
default value for any key that hasn't had a value explicitly set for it yet.
For example, imagine if we searched for the highscore of playerTwo
, which was never added to the mapping
.
Because our mapping
is storing its values as type uint
, the default value we would get back is 0
:
address playerTwo = "0x0Cca37CBaeE672d8429B148DF67cB3513422925c";uint playerTwoHighScore = highscores[playerTwo];// playerTwoHighScore = 0, because we never added a score for them inside the mapping!
This is because all types in Solidity have a default value if no value is assigned to them, and the
default value for uint
is 0
.
This is true for all types in general, not just for values inside of a mapping
. To quote the Solidity documentation:
The concept of "undefined” or “null” values does not exist in Solidity, but newly declared variables always have a default value dependent on its type.
If you're wondering, in Solidity the default value for each type is:
- uint:0
- int:0
- fixed:0.0 (warning: not fully supported)
- string:""
- boolean:false
- enum: the first element of the enum
- address:0x0
- array: a dynamically-sized []
- mapping: an empty mapping
- struct: a struct where all members are set to default values
- function: if internal, an empty function. If external, a function that throws an error when called.
Mapping vs. Array: When to use one over the other
If you need to be able to iterate over your group of data (such as with a for
loop), then use an array
.
If you don't need to be able to iterate over your data, and will be able to fetch values based on a known key, then use a mapping
.
However, sometimes it is optimal to use both. Since iterating over an array
can be an expensive action in Solidity (compared to fetching data from a mapping
), and since
you may want to be able to store both a value and its key within your smart contract, developers sometimes opt to create an array
of keys, which serve as a reference
to even more data that can then be retrieved from its associated value inside of a mapping
.
Keep in mind that you should never allow an array
in Solidity to grow too large, since in theory iterating over a big enough array could end up costing more in gas fees than the value of the transaction is worth (another reason to consider using mapping
when possible).
Next steps
To learn more about Solidity and its types, I recommend starting with the Solidity documentation.
If you are brand new to Solidity and are looking for more resources to get started, I couldn't recommend these resources enough:
- the legendary CryptoZombies course
- Nader Dabit's Complete Guide to Full Stack Ethereum Development
- the Hardhat Beginner Tutorial
- the Ethers.js Documentation
- consider joining the awesome Developer DAO community
- and one more time: the Solidity Documentation
If you liked this post, consider following me on twitter and/or subscribing to the blog using the form below.