There are trade-offs between the different types of indexed storage and many of us find that we need to use a combination of the options to accomplish all a given contract's goals.
There is a scalability problem with passing the whole list in one move. There's no need to do that when the client possibly already has most of the list. In any case, you'll find scale is achieved when all operations are arranged as fixed-cost functions.
Even though your example is simple, I'll show you how to accomplish the most common long-term storage needs (long-term, because one doesn't normally get to amend the contract later).
Iterable list, like your example, but return them individually:
function getAddressCount() public view returns(uint count) {
return addesses.length;
}
function getAddressAtRow(uint row) public view returns(address theAddress) {
return addresses[row];
}
You might (very likely) find it handy to know (without iteration) if an address is somewhere in addresses[]
to avoid issues like appending the same address twice. You want the contract to catch an errant client request, so it needs a way to do that at scale (no iteration). Mappings can help. In addition to the array:
mapping(address => bool) bool isActuallyAnAddressOnMyList;
That accomodates something like:
function isAddress(address check) public view returns(bool isIndeed) {
return isActuallyAnAddressOnMyList[check];
}
You can get a little fancier with a pattern that will accomodate additional properties about the addresses (as many properties as you need):
struct AddressStruct {
bool isAddress;
// more fields
}
mapping(address => AddressStruct) public addressStructs;
address[] public addressList;
You can make the contract state completely discoverable:
function getAddressCount() public constant returns(uint count) {
return addressList.length;
}
function getAddressStruct(address fetch) public view returns(bool isAddress, ... ) {
require(isAddress(fetch);
return(addressStructs[fetch].isAddress, ...);
}
You can (should) emit events as changes happen:
event LogNewAddress(address sender, address newAddress);
function appendAddress(address newAddress) returns(bool success) {
require(!isAddress(newAddress));
addressList.push(newAddress);
addressStructs[newAddress].isAddress = true;
LogNewAddress(msg.sender, newAddress);
return true;
}
By emitting the event, you give clients that are already synced a way to simply take note of new addresses as they arrive. It also means it is theoretically possible to reconstruct the entire state history of the contract when done properly.
The idea of using both mapping and array increases the insertion gas cost but it addresses the tradeoffs of using one or the other exclusively. Various structures and tradeoffs are described over here: Are there well-solved and simple storage patterns for Solidity?
Hope it helps.