Solidity manages data locations differently based on whether variables are stored in storage or memory. When working with arrays of structs, attempting to filter data often leads to specific compiler constraints regarding how dynamic arrays behave in memory versus storage.
Consider a contract designed to manage an inventory of items. The struct definition and state variable might look like this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract InventoryManager {
struct Item {
string identifier;
string category;
uint256 quantity;
address owner;
}
Item[] public itemRegistry;Retrieving a Single Record
To find a specific item by its identifier, a simple iteration over the storage array is sufficient. Since the function returns a copy of the struct data, the return variable resides in memory.
function getItemById(string memory _id) public view returns (Item memory) {
for (uint256 i = 0; i < itemRegistry.length; i++) {
if (keccak256(abi.encodePacked(itemRegistry[i].identifier)) == keccak256(abi.encodePacked(_id))) {
return itemRegistry[i];
}
}
revert("Item not found");
}The Memory Array Push Error
A common error occurs when developers attempt to create a filter function that returns multiple matching records. The instinct is to declare an empty array in memory and dynamically add elements to it.
// This code block will fail to compile
function getItemsByCategory(string memory _category) public view returns (Item[] memory) {
Item[] memory results = new Item[](0);
for (uint256 i = 0; i < itemRegistry.length; i++) {
if (keccak256(abi.encodePacked(itemRegistry[i].category)) == keccak256(abi.encodePacked(_category))) {
// TypeError: Member "push" is not available in struct InventoryManager.Item[] memory
results.push(itemRegistry[i]);
}
}
return results;
}The compiler rejects this because results is a memory array. In Solidity, memory arrays are fixed-size once allocated. The push operation, which dynamically resizes an array, is exclusively available for storage arrays. To return a filtered list from a function, the memory array must be initialized with the exact number of elements it will hold.
Solution: Fixed-Size Memory Allocation
To correctly return a filtered array, the logic requires two passes: first to count the matches and second to populate the fixed-size memory array.
function getItemsByCategory(string memory _category) public view returns (Item[] memory) {
// Step 1: Count matching elements
uint256 matchCount = 0;
for (uint256 i = 0; i < itemRegistry.length; i++) {
if (keccak256(abi.encodePacked(itemRegistry[i].category)) == keccak256(abi.encodePacked(_category))) {
matchCount++;
}
}
// Step 2: Initialize memory array with the correct size
Item[] memory filteredItems = new Item[](matchCount);
// Step 3: Populate the array
uint256 currentIndex = 0;
for (uint256 i = 0; i < itemRegistry.length; i++) {
if (keccak256(abi.encodePacked(itemRegistry[i].category)) == keccak256(abi.encodePacked(_category))) {
filteredItems[currentIndex] = itemRegistry[i];
currentIndex++;
}
}
return filteredItems;
}Correct Usage of Push for Storage
The push method should be used when modifying the state variable directly. The following function demonstrates the correct way to append a new struct instance to the storage array.
function addItem(string memory _id, string memory _category, uint256 _qty) public {
// Create the struct in memory
Item memory newItem = Item({
identifier: _id,
category: _category,
quantity: _qty,
owner: msg.sender
});
// Push the memory struct into the storage array
itemRegistry.push(newItem);
}