Understanding Structs and Enums in Solidity

Pieces 🌟 - Nov 2 '22 - - Dev Community

Structs allow you to create custom data types in Solidity. A variable declared to be a struct can have multiple data types in it.

Enums (Enumerables) are user-defined data types that restrict a variable to have only one predefined value from a set of multiple predefined values. Enums are assigned integer values starting from zero to the value of the last index.

In this article, I will explain how structs and enums work and how to use them.

Getting Started

It will be best if you code along with me in order to best understand the material. Visit remix.ethereum.org— This online IDE is an excellent and easy-to-use environment for beginners. Go to the File Explorer tab, click on contracts and create a new file with your preferred name and “.sol” at the end. I will be using “StructsAndEnums.sol” as my file name:

The file setup for this project.

Now in the file, start by specifying the license identifier. License identifiers indicate relevant license information at any level from the package to the source code file level. Make sure you make it a comment:

// SPDX-License-Identifier: Unlicense
Enter fullscreen mode Exit fullscreen mode

Specify the solidity compiler version you want to use:

pragma solidity 0.8.0;
Enter fullscreen mode Exit fullscreen mode

Finally, create a contract by using the “contract” keyword and the name you wish to assign to the smart contract:

contract StructsAndEnums{
}
Enter fullscreen mode Exit fullscreen mode

This empty contract StructsAndEnums is already a valid contract and is deployable to any blockchain. Go to the Solidity Compiler tab, change the compiler version to the version you are working with and click compile. Once the compilation is successful, you should see a green checkmark by the side of the compiler icon:

The Solidity compiler tab.

Awesome! Now we can write all of our smart contract code into StructsAndEnums.

Variable types in Solidity

Solidity has six main variable types. They are:

  1. Strings - represented as string.
  2. Signed integers - represented as int.
  3. Unsigned integers - represented as uint.
  4. Booleans - represented as bool.
  5. Addresses - represented as address.
  6. Bytes - represented as bytes.

How to declare a Struct

To declare a struct, start off by typing the struct keyword along with the name you wish to assign to the struct. Inside the struct, list out the different data types you want the struct to have. In the code snippet below, I have a struct named “Shipment” with some variable names and types in it:

struct Shipment {
    string package;
    string state;
    address sender;
    address receiver;
    uint price;
 bool delivered;
}
Enter fullscreen mode Exit fullscreen mode

Creating Struct variables

To create a struct variable, type in the name of the struct followed by the name of your variable. You will need to parse the values of the different data types of the struct into the variable. One way of doing this is by assigning the values according to the order of the data type in the struct like so:

Shipment purchase = Shipment("Flash Drive", "Texas", 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB, 20, true);
Enter fullscreen mode Exit fullscreen mode

Another way is by assigning the values in any order with the key of the variable and the value:

Shipment purchase = Shipment({price: 20, sender:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, package: "Flash Drive", delivered: true, receiver:0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB});
Enter fullscreen mode Exit fullscreen mode

To see the variable, we have to assign it to the public keyword:

Shipment public purchase = Shipment("Flash Drive", "Texas", 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB, 20, true);
Enter fullscreen mode Exit fullscreen mode

Let’s try and look into the variables stored in this struct variable called purchase. First, we have to deploy the contract. We will deploy to the remix virtual machine, a simulation used for tests by developers and not a live blockchain network. With any new changes we add to the smart contract, we will be deploying new contracts and deleting previous ones to see the changes. By nature, smart contracts are immutable and can never be edited after deployment. Go to the Deploy & run transactions tab at the bottom and click deploy.

The Deploy and Run Transactions tab.

After a successful deployment, at the bottom, you will find a Deployed Contracts section which holds the contract you just deployed. When you click on the dropdown, you will find the public variable called purchase we created earlier in our code. Click on the purchase button. Remix will return the variables stored in the purchase struct variable. Here’s what the output will look like:

The output of a deployed contract.

Updating Struct variables

So we’ve created a struct and a struct variable, but what if we wanted to change certain things in that variable? What if we wanted to change the values inside the purchase variable from outside the contract? Let’s create a function that does just that.

Let’s name the function update and make it public. This function will take all of the variables needed by the struct variable. Variable types such as strings and bytes are stored in data locations. The keyword memory tells Solidity to temporarily store the input data of the string variable while a function is being called.

function update (string memory _package, string memory _state, 
    address _sender, address _receiver, uint _price, bool _delivered) public{
        purchase = Shipment(_package, _state, _sender, _receiver, _price, _delivered);
}
Enter fullscreen mode Exit fullscreen mode

Once we compile and deploy this code, we should be able to see two buttons in the Deployed Contracts section. One of which is the purchase struct variable and the update function. If we click on purchase, we will see the variables from earlier:

The ability to update or purchase a deployed contract.

We'll try and change these values. So by the right of the update button, click on the dropdown icon, fill in your preferred values and click on transact:

Updating a contract.

And, once we click on purchase again:

Purchasing a contract.

Sweet! We were able to change the values in purchase successfully.

How to declare an Enum

To declare an enum, type in the enum keyword before the name you wish to assign to the enum. Enums are somewhat similar to booleans. The only difference between them is that booleans can only have two choices which are “0” (false) and “1” (true), while enums can have more than two choices. Let us see an example of an enum with four choices:

enum Status {
    None,
    Pending,
    Shipped,
    Received
}
Enter fullscreen mode Exit fullscreen mode

Creating Enum variables

To assign a variable to an enum, similarly to what we did earlier with structs, we have the name of the enum in front of the variable and declare it public for us to see its value:

Status public orderStatus;
Enter fullscreen mode Exit fullscreen mode

And when we compile and deploy this:

The order status of a contract.

We get a value of 0 which is the first choice assigned to the enum (None). Enums always use the first case as the default. We could have also given a different choice to orderStatus and it would be assigned that value.

Updating Enum variables

Let us use a function similar to the one we created for structs. The function will be called change and will take in a single parameter of type uint:

function change(uint value) public {
  orderStatus = Status(value);
}
Enter fullscreen mode Exit fullscreen mode

And once we compile and deploy this:

Updating an order status.

The value of orderStatus is now at the third index (2) which is "Shipped".

Using an Enum in a Struct

Since a struct is a compilation of different data types, it can also take in enums. Let us replace the boolean variable called delivered with an enum:

struct Shipment {
  string package;
  string state;
  address sender;
  address receiver;
  uint price;
  Status delivered;
}
Enter fullscreen mode Exit fullscreen mode

Before compiling, let us make a few changes. Back in our purchase variable, change:

Shipment public purchase = Shipment("Flash Drive", "Texas", 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB, 20, true);
Enter fullscreen mode Exit fullscreen mode

To this:

Shipment public purchase = Shipment("Flash Drive", "Texas",0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB, 20, Status(0));
Enter fullscreen mode Exit fullscreen mode

Do the same for the update function. Change this:

function update(string memory _package, string memory _state, 
address _sender, address _receiver, uint _price, bool _delivered) public{
    purchase = Shipment(_package, _state, _sender, _receiver, _price, _delivered);
}
Enter fullscreen mode Exit fullscreen mode

To this:

function update(string memory _package, string memory _state, 
address _sender, address _receiver, uint _price, Status _delivered) public{
    purchase = Shipment(_package, _state, _sender, _receiver, _price, _delivered);
}
Enter fullscreen mode Exit fullscreen mode

Now compile and deploy the contract. To input the value for the enum data type when calling the update function, type in the uint value for the choice you wish to use. For example, if we want the choice to be “Pending”, our input value has to be 1.

Storing Structs and Enums in a mapping

Mappings act like hash tables or dictionaries in other languages. We can use a mapping to store multiple data on the blockchain with a key. Mappings are more precise and also more efficient than arrays. We will keep the enum Status in the Shipment struct like we did previously but will be storing the struct in a mapping called getPurchase. The key for the mapping will be the address of the sender and the value will be the data from the current Shipment struct tied to that address. To declare a mapping, type in the mapping keyword along with the key and value of the mapping in brackets and a name for the mapping at the end:

mapping(address => Shipment) public getPurchase;
Enter fullscreen mode Exit fullscreen mode

In the update function, we will take in new struct variables as well as new changes made to existing struct variables and store them in the mapping. Let’s make the sender the actual sender of the transaction by changing _sender to the msg.sender keyword. After doing this, we will no longer need address _sender as a parameter in the function:

function update(string memory _package, string memory _state, address _receiver, uint _price, Status _delivered) public{
  purchase = Shipment(_package, _state, msg.sender, _receiver, _price, _delivered);
  getPurchase[msg.sender] = purchase;
}
Enter fullscreen mode Exit fullscreen mode

After we compile and deploy, let’s input new values to the update function:

Updating structs and enums.

Let’s look for the sender data in the getPurchase mapping by inputting the address of msg.sender as the key. To get your address, scroll to the top of the “Deploy & Run Transactions” tab, and you will see a list of virtual addresses created by remix:

Finding your virtual address.

Copy the first address used to make the transaction and input that address to the getPurchase mapping:

Using the above address to create a purchase.

Great! All new and existing sender addresses that interact with the update function will also have their struct and enum data stored or updated in the mapping. To test this further, you can try switching to multiple account addresses at the top, interacting with the update function from each and inputting those addresses as the key to the mapping.

Conclusion

Structs and enums reduce complexity and make it easier for other developers to read and understand your code. Whenever you receive an error from your compiler that says, “Stack too deep. Try removing local variables,” it means that you’ve created too many local variables inside of a single function. Using a struct to store these local variables lets you get past that error. Using structs and enums is also great for optimization.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .