So if we create a struct inside a contract called Crowdfunding
like so:
contract Crowdfunding {
struct Request {
string description;
uint value;
address recipient;
bool complete;
}
and then add it to our list of variables like so:
contract Crowdfunding {
struct Request {
string description;
uint value;
address recipient;
bool complete;
}
Request[] public requests;
address public manager;
uint public minimumContribution;
address[] public approvers;
We then will go forth and apply it like so:
contract Crowdfunding {
struct Request {
string description;
uint value;
address recipient;
bool complete;
}
Request[] public requests;
address public manager;
uint public minimumContribution;
address[] public approvers;
function createRequest(string description, uint value, address recipient) public restricted {
Request newRequest = Request({
description: description,
value: value,
recipient: recipient,
complete: false
});
requests.push(newRequest);
}
}
At the very left hand side we have the word request with a capital R. This is a reference to the struct I have defined at the top of my contract.
The word Request
tells Solidity that I am about to create a new variable that contains a value of type request. Immediately after that I create the new variable and its name is newRequest
.
This newRequest
does not have to have the word request in it at all.
It obeys all the same rules as normal variables do. So it could have been called newElephant
or whatever, but newRequest
makes more sense. Taken together on the left hand side of the equal sign, the two words mean I want to create a new variable called newRequest
and it will hold a value of type Request
.
So the left hand side is about storing the value and the right hand side is about creating it. To create a new instance of a struct we create the name of the struct, a set of parentheses, a set of curly braces and then inside of there we iterate over the different fields that we want to initialize this request with.
Whenever we create a new Request
we have to add every property that was originally added to the struct of Request
.
Specifically declaring the struct on the right hand side like I did, I used a key/value pair to define it. However, there is another way of defining a struct that does not involve all the keys listed out.
This is an alternative syntax that you can use that you can find documentation on but I recommend you don’t use it.
The alternative syntax works in a tricky way:
Request(description, value, recipient, false);
It does not tell us which value belongs to which field.
So that alternative syntax is all based on having a consistent order of fields. Using this alternative syntax, Solidity assumes that the first, second, third and other arguments are in the same order as the struct originally created.
The problem with that is if I reorder the fields inside my original struct, everything will shift by one and it will create an error message for that alternative syntax. It will complain that there is no match between what would be the key/value pair.
Imagine that the struct had all the same type of bool and when you create your new request they all have true, true, true, false like so:
Request(true, true, true, false);
If you rearrange your original struct key value pairs in the future, it will lead to confusion.
When we use the key/value approach we can jumble the order all we want and we will not get any additional error messages.
Alright, so back to the struct itself, we define it, assign it to newRequest
and we push that to an array that holds all our different structs, but we still have this error message of yours.
So what is this error message all about?
And I understand the frustration with it as the documentation out there is not helpful.
You may have also gotten a message about memory. In the world of Solidity, storage and memory are two separate topics.
Sometimes storage and memory are referencing where our contract stores data such as integers, strings, arrays and so on. Sometimes storage and memory is referencing how Solidity stores variables.
You are going to see storage and memory and sometimes it will be in documentation about where our contract stores data and other times it talks about how Solidity works with variables.
Lets first speak about the case in which it references where our contract stores data.
All the documentation available speaks about the case in which it references where our contract stores data, none of the documentation addresses the case in which the error is referencing how our Solidity variables stores values.
On the one hand we have one bucket that stores all our data and we refer to it as storage.
Storage is used to hold data between function calls. An example of storage is all the different variables that I have initialized at the top of the contract here:
Request[] public requests;
address public manager;
uint public minimumContribution;
address[] public approvers;
These are all variables or pieces of data held in our contracts storage.
Storage is available between function calls. If I call one function on a contract and modify one piece of data, that piece of data is available on the next follow up function call like so:
contract Crowdfunding {
struct Request {
string description;
uint value;
address recipient;
bool complete;
}
Request[] public requests;
address public manager;
uint public minimumContribution;
address[] public approvers;
modifier restricted() {
require(msg.sender == manager);
_;
}
A good way to imagine storage, its like a computer hard drive, if you write a text file to your hard drive, that data is still there, it does not matter if you shut off your computer. So that is storage in Solidity.
On the other hand there is memory, a temporary place to store data like a computer’s RAM. Once the computer is turned off completely, any data that existed in RAM is emptied out.
Then there is the second context of Storage and Memory of referencing our Solidity variables store values and that is what this error message is speaking to, the more obscurely documented context.
So I have this example contract thats easier to understand and will illustrate the differences between memory and storage.
contract Numbers {
int[] public numbers;
function Numbers() public {
numbers.push(20);
numbers.push(32);
}
}
So this Numbers
contract has a single variable, an array of integers called numbers
. Inside the Numbers
function we take the array and push the numbers 20
and 32
.
So If I were to now print out everything inside of numbers
, I would expect to see something like 20
and 32
.
I will add another line of code inside that function. I will declare a new variable and assign it a value. I will declare a new array of integers called myArray
and assign it the value of numbers
like so:
contract Numbers {
int[] public numbers;
function Numbers() public {
numbers.push(20);
numbers.push(32);
int[] myArray = numbers;
}
}
Now when I do that I will get a warning on the new line where I added the integer of numbers. If you look at your warning it says variable is declared as a storage pointer. There must be something similar to what I did here and what you did inside your contract.
I will add one additional key word that is going to make that warning go away and then talk about what that keyword is doing for us.
contract Numbers {
int[] public numbers;
function Numbers() public {
numbers.push(20);
numbers.push(32);
int[] storage myArray = numbers;
}
}
So now I only have one warning which says unused local variable, that warning means you declared a variable but never used it, thats fine.
The storage
keyword resolved the warning message.
SO what is going on with the storage
keyword? What’s its purpose?
Any contract level variable is always stored inside the storage area of the contract.
At the righthand side of my equal sign I have the numbers variable. And as we well know, it refers to an array of integers that is stored inside of the storage area of my contract.
Here is the interesting part. When I use the storage
keyword, it changes the way in which the new variable called myArray
works.
SO the storage
keyword changes how a variable works. The keyword storage
makes the variable point directly at the exact same location as the numbers variable is pointing at.
The myArray
variable will be pointing to the exact same array.
I will show you proof that this is the case.
I will reference myArray, access the first element and set the value to be 1 like so:
contract Numbers {
int[] public numbers;
function Numbers() public {
numbers.push(20);
numbers.push(32);
int[] storage myArray = numbers;
myArray[0] = 1;
}
}
So when I deploy the contract in Remix and I will retrieve the first element inside the numbers array.
I will hit the deploy button
If I access the first value by putting a zero inside of numbers I get back 1.
If I access the second value which is 1, I get 32 and if I try to access the third element or index of 2 I get nothing because there are only two elements inside of numbers.
Now this numbers array looks like [1, 32] whereas before it was [20, 32]. So as soon as I use the storage
keyword it changes how the variable behaves, it makes it look at the exact same location, the exact same data structure.
What would I use instead of storage
?
The other keyword we could use is memory
. If we use memory
it changes the way in which this variable behaves. If we use memory
an additional step will come into play.
So if I executed this line of code right here with the keyword of memory
int[] memory myArray = numbers;
It would take the numbers
array and make a copy of it and place it into memory inside our contract and memory gets dumped anytime a function exits and myArray
would no longer point at the array inside of storage, instead, its going to point to the new copy sitting in memory.
I will look at the first element in memory, when I click on numbers, its 20 which is what it originally was. In this case, when we modify an array we are modifying a totally different array, not the same array.
But why should we care about memory or storage keywords at all?
I will add a new function to my contract and call this new function changeArray like so:
contract Numbers {
int[] public numbers;
function Numbers() public {
numbers.push(20);
numbers.push(32);
}
function changeArray() private {
}
}
So I cannot directly call changeArray
only other functions inside this contract can do so.
I will say that this array takes one argument of integers called myArray
like so:
contract Numbers {
int[] public numbers;
function Numbers() public {
numbers.push(20);
numbers.push(32);
}
function changeArray(int[] myArray) private {
}
}
Then inside of the function I will take the first element inside the array and set it equal to 1 like so:
contract Numbers {
int[] public numbers;
function Numbers() public {
numbers.push(20);
numbers.push(32);
}
function changeArray(int[] myArray) private {
myArray[0] = 1;
}
}
At this point I am not calling changeArray
in any way shape or form. I create a new instance of this contract, this function will not be invoked.
I will add a call to change array inside my constructor like so:
contract Numbers {
int[] public numbers;
function Numbers() public {
numbers.push(20);
numbers.push(32);
changeArray(numbers);
}
function changeArray(int[] myArray) private {
myArray[0] = 1;
}
}
Here is where things start to get interesting.
Imagine in your head, what would happen if we deploy this contract and access the first element inside of numbers.
I will deploy the contract and retrieve the first element. This first element will be either 1 or 20 and sure enough its 20. What does that mean?
It means we passed in the numbers array, received it inside the changeArray()
function, modified the array, but that modification did not change the numbers array. What that means is that whenever we pass a value into a function like this, we are actually making a copy of that array. So myArray is a copy of numbers.
Whenever you take in function arguments they are assumed to be memory type variables, so its assumed you want to make copies, its assumed you want that behavior.
However, if we want to change the behavior, we could replace the memory keyword with storage instead.
The storage
keyword in this case would mean do not attempt to make a copy of the array, instead take the numbers array and make that exact array available in this function.
So the memory
and storage
keyword changes how we pass around references to these data structures. If you have never worked with a programming language that works with by value passing as opposed to by reference passing before, this may seem over the top.
Working with Solidity you will be creating contracts where you will be working with array, mapping or struct and you may want to change the data structure and that its used from the original function that its called from, great use case for storage, otherwise you don’t need to use that keyword and just let Solidity work as usual.
Participant newParticipant = Participant({})
inside a function? I ask because you declared a struct but where or how did you use it? Share that code as well.