Structs come in handy when you’re dealing with data not stored in a database.  For example if you knowyou have a CSV file with columns like [“name”,”address”,”zip”] with customer data:

Customer = Struct.new(:name,:address,:zip)
customers = []
CSV.open("customers.csv","r") do |row|
  customers << Customer.new(*row)
end 

We can now work with our customer data as an array of Customer class instances, hashes or simple arrays.  Because Structs act as arrays (in that you can index them by element number,) we can save back to a CSV file, if our boss insists on it:

CSV.open("new_customers.csv","w") do |writer|
  customers.each{|c| writer << c}
end

Some CSV files have headers labeling the cells, which means you could determine your accessors when you open a CSV.  That would be nice, since you wouldn’t need to know the order of the fields and you’d get a little Active-Record like behaviour for free.

rows = CSV::parse(File.open("customers.csv","r"){|f| f.read})
Customer_class = Struct.new(*rows.shift.map{|name| name.to_sym})
customers = rows.map{|row| Customer_class.new(*row)}

customers.first.members
  =>  ["zip", "name", "address", "customer_number", "loyalty_points"]

Looks like we got two more fields than we expected, but no problem.

The only gotcha here is that you should not copy a record created with the old definition of Customer (like the one at the top of the post,)  onto a new version like the one from the file read above.

It’s not enough that both Structs have the same accessor methods – they must be in the same order as well.  That’s because Struct instances store their contents in an array ordered by the arguments passed to new().  The first customer class had ‘name’ as it’s first element but the second version had ‘zip’ as the first data element.  Normally this won’t happen; if you know it might you can avoid the problem by always accessing the data via the accessor method names or as hash keys, like so:

def copy_cust(cust1, cust2)
  cust1.members.each{|m| cust1[m] = cust2[m]}
  cust1
end