Advanced Solidity Datatypes Explained with Code Examples
Let's Get Technical
Table of contents
🛑This is a long technical session, but if you manage to go through it you will come out a bad-ass solidity datatypes veteran!
Solidity is a statically typed language used for writing smart contracts and building decentralized applications (DApps) on the Ethereum blockchain. It offers a wide range of data types to handle different kinds of data. Understanding these data types is crucial for writing efficient and secure smart contracts. Here, we will explore the advanced datatypes in Solidity and provide code examples to illustrate their usage.
Let's start getting technical!
Value Types
Value types in Solidity store data directly in memory and are passed by value. But wait what is this memory
thing?
In technical terms, memory in Solidity refers to a temporary storage area where variables are stored during the execution of a function or a contract. It is a part of the Ethereum Virtual Machine (EVM) and is separate from the storage area where variables are permanently stored on the blockchain.
Here are some key points about memory in Solidity:
Memory is used to store variables that are needed temporarily during the execution of a function or a contract.
When you define a local variable in Solidity, it is stored in memory and then pushed to the stack for execution.
Memory code is executed on the stack, which is a temporary storage area used by the EVM to bring data from other storage areas to work on them.
The stack has a maximum depth of 1024 elements and supports the word size of 256 bits.
Variables stored in memory are not stored simultaneously in memory and stack. Instead, they are moved from memory to the stack when needed for execution.
This separation between memory and stack is cost-efficient, as it allows the EVM to optimize the usage of resources.
It is important to note that memory is different from storage in Solidity:
Storage refers to variables that are permanently stored on the blockchain. State variables, which are variables declared outside of functions, are by default storage and written permanently to the blockchain.
Memory, on the other hand, is a temporary storage area used during the execution of a function or a contract.
Variables stored in storage can be accessed and modified by other contracts or external entities, while variables stored in memory are local to the function or contract and are not accessible from outside.
By understanding the concept of memory in Solidity, you can effectively manage the storage and manipulation of variables in your smart contracts.
So back to value types:
A. Integers: Solidity provides both signed and unsigned integers of various sizes. The uint
type is used for unsigned integers, while the int
type is used for signed integers. Examples:
uint256 public myUint = 100;
int8 public myInt = -10;
So if you are curios enough you probably starting to thing why do we need these typings for just a number variable? Perfect her is why,
In Solidity, the availability of different-sized integers, such as uint8, uint16, uint32, int8, int16, int32, etc., serves several important purposes:
Efficient memory usage: By allowing developers to specify the size of integers, Solidity enables more efficient memory usage. For example, if you know that a variable will never exceed a certain range of values, you can use a smaller-sized integer to conserve memory. This can be particularly important in resource-constrained environments like blockchain networks.
Optimization of gas costs: Gas is the measure of computational effort required to execute operations on the Ethereum network. By using integers with smaller sizes, you can reduce the gas costs associated with storing and manipulating these variables. This optimization can lead to more cost-effective smart contracts.
Data validation and range restrictions: Different-sized integers allow developers to enforce specific range restrictions on variables. For example, if you have a variable that should only store values between 0 and 255, you can use uint8 to limit the range and prevent unintended values from being assigned.
Interoperability and integration: Solidity is designed to interact with other programming languages and systems. By providing different-sized integers, Solidity aligns with the capabilities and conventions of other languages, making it easier to integrate with existing systems and ensure compatibility.
So You can now see the availability of different-sized integers in Solidity offers flexibility, efficiency, and control over memory usage and gas costs. It allows developers to optimize their smart contracts and tailor the variables to specific requirements, leading to more efficient and secure blockchain applications.
B. Boolean: The bool
type represents a boolean value, which can be either true
or false
. I like to think that this is the most simple type to go by.
bool public isReady = true;
C. Addresses: The address
type represents Ethereum account addresses. It can hold a 20-byte value and is used for interacting with other contracts or transferring balances.
address public myAddress = 0x1234567890123456789012345678901234567890;
Right, the address type serves as a key cornerstone in Solidity and the Ethereum Virtual Machine (EVM) overall. Here are some important points to understand about the address type:
Functionality: The address type provides several built-in functions and members that allow you to interact with the account at a specific address. These functions include retrieving the balance of the address, sending funds to the address, and executing low-level calls to the address. These functionalities enable smart contracts to interact with other contracts or transfer balances securely.
Readability and type safety: Using the address type instead of a generic uint160 or bytes20 provides improved readability and type safety in your smart contracts. It clearly communicates that the variable is intended to store an Ethereum address, making the code more understandable for other developers.
Address types and inheritance: The address type serves as a base for all contracts. Contracts inherit certain members and functions from the address type, allowing you to access the functionality of the address type directly within your contracts. This inheritance simplifies the interaction with addresses and provides a consistent interface for address-related operations.
Address vs. address payable: Solidity also provides the address payable type, which is identical to the address type but includes additional members such as
transfer(...)
andsend(...)
. The address payable type is used specifically for addresses that can receive Ether, distinguishing them from plain addresses that are not designed to receive Ether. This differentiation ensures that Ether is sent only to addresses that are capable of handling it.
In short, the address type in Solidity is essential for interacting with Ethereum accounts, transferring balances, and executing low-level calls. It improves code readability, provides type safety, and enables powerful functionalities within smart contracts.
D. Enums: Enums in Solidity are user-defined data types that allow you to assign names to integral constants. They provide a way to define a set of related values and restrict a variable to have only one predefined value from that set. Enums are useful for creating custom data types with a limited set of options, making the code more readable, maintainable, and bug-resistant.
To define an enum in Solidity, you use the enum
keyword followed by the enum name and a set of curly braces containing the values that the enum will contain. For example:
enum Color { RED, GREEN, BLUE }
In this example, the enum Color
is defined with three possible values: RED
, GREEN
, and BLUE
. Each value in the enum is automatically assigned an integer value starting from 0, so RED
is assigned 0, GREEN
is assigned 1, and BLUE
is assigned 2.
You can declare variables of the enum type and assign them one of the predefined values. For example:
Color public myColor = Color.RED;
In this case, myColor
is a variable of type Color
and it is assigned the value RED
from the Color
enum.
Some key points to understand about enums in Solidity include:
Enums are treated as numbers internally, and Solidity automatically converts them to unsigned integers.
An enum should have at least one value in the enumerated list, and the values cannot be numbers or boolean values.
Enums can be declared at the file level, outside of contract or library definitions.
Enums can be used as function parameters and as keys in mappings.
Okay let's wrap it up, enums in Solidity provide a powerful and convenient way to define user-defined data types with a fixed set of values, making the code more expressive and reducing the chances of bugs. They are a fundamental tool in Solidity programming for creating custom data types and improving code organization and readability.
E. Bytes: The bytes
type is used to store fixed-sized byte arrays, while the string
type is used for dynamic-sized character arrays. Example:
bytes1 public myByte = 0x12;
string public myString = "Hello, World!";
Let's get a little bit technical with bytes.
Representation at the low-level: In Solidity, bytes are represented as arrays of bytes, where each element of the array represents a single byte. The size of the byte array is fixed and specified by the bytes type declaration. For example,
bytes1
represents a single byte,bytes2
represents two bytes, and so on.Storage and manipulation: Fixed-sized byte arrays, such as
bytes1
,bytes2
, etc., are useful when you know the exact size of the data you want to store. We have talked about this earlier if you remember! Bytes typings provide efficient storage and manipulation of binary data. You can access individual bytes in the array using indexing, and you can perform bitwise operations, such as shifting and bitwise AND/OR, on the byte array.Conversion between bytes and string: Solidity provides built-in functions to convert between bytes and string. The
bytes
type can be converted to a string using thestring
typecast, and a string can be converted to bytes using thebytes
typecast. However, it's important to note that the conversion from bytes to string and vice versa can be expensive in terms of gas costs, especially for large data sizes. Therefore, it's recommended to use bytes for binary data and string for textual data whenever possible.
So that was a bit of Value Types, now let us see how we can apply these types with more advanced typing cases. Yes, we are not done🥹.
- -Let's get technical!*
Reference Types
Reference types in Solidity store reference to data and are passed by reference. They include:
- Arrays: Solidity supports both fixed-sized and dynamic-sized arrays. Arrays are used to store multiple values of the same type. Example:
uint256[] public myArray;
In the code snippet uint256[] public myArray;
the myArray
variable is declared as an array of type uint256
. This means that myArray
can store multiple values of type uint256
.
To relate this to the section we saw above about the uint
type, uint256
is a specific subtype of the uint
type in Solidity. The uint
type is used to represent unsigned integers, which are non-negative whole numbers. The uint256
subtype specifically represents unsigned integers with a size of 256 bits, allowing for a wider range of values to be stored.
By declaring myArray
as an array of uint256
, we are specifying that each element in the array can hold a value of type uint256
. This allows us to store and manipulate multiple unsigned integer values within the array.
- Strings: Strings are used to store dynamic-sized character arrays. Example:
string[] public myDynamicArray;
In the code snippet string[] public myDynamicArray;
, the myDynamicArray
variable is declared as an array of type string
. This means that myDynamicArray
can store multiple values of type string
.
To relate this to the section we saw earlier about bytes, the string
type in Solidity is used to store dynamic-sized character arrays, similar to how the bytes
type is used to store fixed-sized byte arrays. While bytes
are used for binary data, string
is specifically designed for storing textual data.
In the context of the code snippet, myDynamicArray
is an array that can hold multiple string values. Each element in the array can store a dynamic-sized character array, allowing for flexibility in the length of the strings that can be stored.
It's important to note that strings in Solidity are stored as arrays of bytes internally. Each character in the string is represented by one or more bytes, depending on the character's encoding. Solidity provides built-in functions and operators to manipulate and access individual characters within a string.
- Structs: Structs allow you to define custom datatypes that can contain both value types and reference types. They are useful for organizing related data into a single unit. Example:
struct Person {
string name;
uint256 age;
}
Person public myPerson = Person("John", 30);
In the code snippet struct Person { string name; uint256 age; } Person public myPerson = Person("John", 30);
, the Person
struct is defined as a custom datatype that contains two fields: name
of type string
and age
of type uint256
.
Structs in Solidity allow you to create composite data types that can hold multiple values of different types. They are useful for organizing related data into a single unit. In this example, the Person
struct represents a person's information, with the name
field storing the person's name as a string and the age
field storing the person's age as a uint256
.
The myPerson
variable is declared as a public variable of type Person
. It is initialized with the values "John" for the name
field and 30 for the age
field. This allows you to create an instance of the Person
struct and access its fields.
- Mappings: Mappings are key-value data structures similar to hash tables or dictionaries in other programming languages. They allow you to store and retrieve data based on a unique key. Example:
mapping(address => uint256) public balances;
In this example, the balances
mapping is used to store and retrieve uint256
values based on unique address
keys. Each address
key is associated with a corresponding uint256
value. This allows you to keep track of balances or other data associated with specific addresses.
By declaring balances
as a public mapping, it can be accessed and modified by other contracts or external entities. The public
visibility modifier generates a getter function automatically, allowing other contracts or external entities to retrieve the values stored in the mapping.
Oh yeah, keep diving general🫡
Visibility Modifiers
In Solidity, visibility modifiers determine how functions and state variables can be accessed. They include:
- Public: Public functions and state variables can be accessed from any other contract or externally. Example:
uint256 public myVariable = 10;
function myFunction() public {
// Function code here
}
First, the uint256
state variable myVariable
is declared as public
. This means that the value of myVariable
can be accessed from any other contract or externally. The public
visibility modifier automatically generates a getter function for myVariable
, allowing other contracts or external entities to retrieve its value.
In this example, myVariable
is initialized with the value 10. Since it is declared as public
, its value can be read by other contracts or external entities using the generated getter function.
Next, the myFunction
function is declared with the public
visibility modifier. This means that the function can be called from any other contract or externally. The function code can be written within the function body.
By declaring a function as public
, it can be accessed and called by other contracts or external entities. This allows for interaction with the contract and execution of the function's code.
- Private: Private functions and state variables can only be accessed from within the same contract. Example:
uint256 private myVariable = 10;
function myFunction() private {
// Function code here
}
First, the uint256
state variable myVariable
is declared as private
. This means that the value of myVariable
can only be accessed from within the same contract. Other contracts or external entities cannot directly access or read the value of myVariable
.
In this example, myVariable
is initialized with the value 10. Since it is declared as private
, its value can only be accessed and modified within the same contract. This provides encapsulation and restricts direct access to the variable from external sources.
Next, the myFunction
function is declared with the private
visibility modifier. This means that the function can only be called from within the same contract. Other contracts or external entities cannot directly call or execute the myFunction
function.
By declaring a function as private
, it ensures that the function's code can only be executed within the same contract. This allows for encapsulation and restricts the visibility and accessibility of the function to the contract itself.
Are you getting the flow now right?
- Internal: Internal functions and state variables can only be accessed from within the same contract or contracts derived from it. Example:
uint256 internal myVariable = 10;
function myFunction() internal {
// Function code here
}
- External: External functions can only be called from outside the contract. They cannot be accessed internally or from derived contracts. Example:
function myFunction() external {
// Function code here
}
BONUS✨
Function Modifiers
Function modifiers are used to modify the behavior of functions. They include:
- View: The
view
modifier specifies that the function does not modify the contract's state. It is used for reading data from the blockchain. Example:
function myFunction() public view returns (uint256) {
// Function code here
}
- Returns: The
returns
keyword is used to specify the return type of a function. Example:
function myFunction() public returns (uint256) {
// Function code here
}
Okay, that's enough! , I am thinking of having a deep dive into the EVM and getting even more technical with it. See you in the next one🫡