Overview
Struct provides a mechanism for creating classes that encapsulate related data with named attributes. Ruby implements Struct as a class that generates new classes when called, creating accessor methods automatically based on provided attribute names. The generated classes inherit from Struct itself, gaining enumerable behavior and comparison methods.
Ruby creates Struct classes through Struct.new
, which returns a new class rather than an instance. This class can then create instances that store values for the defined attributes. Struct classes include getter and setter methods for each attribute, along with methods for iteration, conversion, and comparison.
# Creating a Struct class
Point = Struct.new(:x, :y)
# Creating instances
p1 = Point.new(1, 2)
p1.x # => 1
p1.y # => 2
# Setter methods are created automatically
p1.x = 10
p1.x # => 10
Struct classes behave like regular Ruby classes but with predefined structure. They support equality comparison based on attribute values, enumerable methods for iteration over values, and conversion to arrays and hashes. Struct instances are mutable by default, allowing modification of attribute values after creation.
The primary use cases include creating simple data containers, value objects for data transfer, and lightweight alternatives to full class definitions when only data storage and basic operations are needed.
Basic Usage
Creating Struct classes requires passing attribute names as symbols to Struct.new
. The returned class can instantiate objects that store values for those attributes.
# Basic Struct creation
Person = Struct.new(:name, :age, :email)
person = Person.new("Alice", 30, "alice@example.com")
# Accessing attributes
person.name # => "Alice"
person.age # => 30
person.email # => "alice@example.com"
# Modifying attributes
person.age = 31
person.name = "Alice Smith"
Struct instances can be created with positional arguments matching the attribute order, or with a hash of attribute names to values. Missing arguments default to nil
.
# Different initialization approaches
Product = Struct.new(:name, :price, :category)
# Positional arguments
p1 = Product.new("Laptop", 999.99, "Electronics")
# Partial initialization
p2 = Product.new("Book", 29.99)
p2.category # => nil
# No arguments
p3 = Product.new
p3.name # => nil
p3.price # => nil
Struct classes inherit enumerable behavior, making instances iterable over their attribute values. This enables functional programming patterns and easy conversion to other data structures.
Contact = Struct.new(:name, :phone, :email)
contact = Contact.new("Bob", "555-1234", "bob@test.com")
# Enumeration over values
contact.each { |value| puts value }
# Outputs: Bob, 555-1234, bob@test.com
# Array-like access
contact[0] # => "Bob"
contact[1] # => "555-1234"
contact[-1] # => "bob@test.com"
# Conversion to array and hash
contact.to_a # => ["Bob", "555-1234", "bob@test.com"]
contact.to_h # => {:name=>"Bob", :phone=>"555-1234", :email=>"bob@test.com"}
Struct instances support equality comparison based on attribute values rather than object identity. Two Struct instances are equal if they belong to the same Struct class and have identical attribute values.
Address = Struct.new(:street, :city, :zip)
addr1 = Address.new("123 Main St", "Boston", "02101")
addr2 = Address.new("123 Main St", "Boston", "02101")
addr3 = Address.new("456 Oak Ave", "Boston", "02101")
addr1 == addr2 # => true (same values)
addr1 == addr3 # => false (different street)
addr1.eql?(addr2) # => true
addr1.hash == addr2.hash # => true
Advanced Usage
Struct supports several advanced creation patterns beyond basic attribute specification. Anonymous Struct classes can be created without assignment to constants, and Struct classes can be subclassed to add custom methods.
# Anonymous Struct class
coordinate_class = Struct.new(:lat, :lng) do
def distance_to(other)
# Simplified distance calculation
Math.sqrt((lat - other.lat)**2 + (lng - other.lng)**2)
end
def to_s
"(#{lat}, #{lng})"
end
end
loc1 = coordinate_class.new(42.3601, -71.0589)
loc2 = coordinate_class.new(40.7128, -74.0060)
distance = loc1.distance_to(loc2) # Custom method usage
Struct classes can accept blocks during creation to define additional methods directly within the class definition. This approach creates more cohesive data objects with behavior attached to the data structure.
# Struct with block for custom methods
Rectangle = Struct.new(:width, :height) do
def area
width * height
end
def perimeter
2 * (width + height)
end
def square?
width == height
end
def resize(factor)
self.width *= factor
self.height *= factor
self
end
end
rect = Rectangle.new(5, 3)
rect.area # => 15
rect.perimeter # => 16
rect.square? # => false
rect.resize(2) # Returns self after modification
rect.width # => 10
Subclassing existing Struct classes enables inheritance hierarchies with shared attributes and specialized behavior. Subclasses inherit all parent attributes and methods while adding their own.
# Base Struct class
Vehicle = Struct.new(:make, :model, :year)
# Subclass with additional attributes and methods
class Car < Vehicle
def initialize(make, model, year, doors = 4)
super(make, model, year)
@doors = doors
end
attr_reader :doors
def description
"#{year} #{make} #{model} (#{doors} doors)"
end
def vintage?
year < 1990
end
end
car = Car.new("Honda", "Civic", 2020)
car.make # => "Honda" (inherited)
car.doors # => 4 (added in subclass)
car.description # => "2020 Honda Civic (4 doors)"
Struct classes support keyword arguments in their initializers when using Ruby 2.5+, providing more explicit instantiation patterns. This feature requires enabling keyword initialization explicitly.
# Keyword argument initialization
User = Struct.new(:username, :email, :role, keyword_init: true)
user1 = User.new(username: "john_doe", email: "john@example.com", role: "admin")
user2 = User.new(email: "jane@example.com", username: "jane_smith") # Order doesn't matter
# Mix with default values through custom initializer
class ApiUser < Struct.new(:username, :email, :role, :active, keyword_init: true)
def initialize(username:, email:, role: "user", active: true, **kwargs)
super
end
end
api_user = ApiUser.new(username: "test", email: "test@api.com")
api_user.role # => "user" (default)
api_user.active # => true (default)
Common Pitfalls
Struct instances are mutable by default, which can lead to unexpected modifications when Struct objects are shared between different parts of an application. This mutability can cause debugging challenges when attribute values change unexpectedly.
# Mutability pitfall
Config = Struct.new(:host, :port, :ssl)
default_config = Config.new("localhost", 8080, false)
# Sharing the same instance
service_a_config = default_config
service_b_config = default_config
# Modification affects all references
service_a_config.port = 3000
service_b_config.port # => 3000 (unexpected change)
# Solution: Create separate instances
service_a_config = Config.new(*default_config.to_a)
service_b_config = Config.new(*default_config.to_a)
service_a_config.port = 3000
service_b_config.port # => 8080 (unchanged)
Struct equality comparison only works between instances of the same Struct class, even when attribute names and values are identical. This behavior can cause unexpected inequality results when comparing logically equivalent data from different Struct classes.
# Equality pitfall with different Struct classes
Point2D = Struct.new(:x, :y)
Coordinate = Struct.new(:x, :y)
point = Point2D.new(1, 2)
coord = Coordinate.new(1, 2)
point == coord # => false (different classes)
point.to_a == coord.to_a # => true (same values)
# Classes themselves are also different
Point2D == Coordinate # => false
Point2D.new.class == Coordinate.new.class # => false
Struct attribute access using bracket notation with invalid indices or keys raises exceptions rather than returning nil
. This behavior differs from Hash access patterns that developers might expect.
# Index access pitfalls
Person = Struct.new(:name, :age)
person = Person.new("Alice", 25)
person[0] # => "Alice"
person[1] # => 25
person[2] # raises IndexError: offset 2 too large for struct(size:2)
person[-3] # raises IndexError: offset -3 too small for struct(size:2)
# String keys don't work with bracket notation
person["name"] # raises TypeError: no implicit conversion of String into Integer
# Use members for safe attribute name access
person.class.members # => [:name, :age]
person.class.members.include?(:name) # => true
Struct classes created with the same attribute names are not identical classes, leading to type checking issues when expecting specific Struct classes rather than duck typing.
# Class identity pitfall
def create_point_class
Struct.new(:x, :y)
end
PointA = create_point_class
PointB = create_point_class
# Classes are different despite same structure
PointA == PointB # => false
PointA.new(1, 2).class == PointB.new(1, 2).class # => false
# Type checking fails
def process_point(point)
raise TypeError unless point.is_a?(PointA)
# Process point
end
point_b = PointB.new(3, 4)
process_point(point_b) # raises TypeError
# Solution: Use duck typing or shared base class
def process_point_duck(point)
raise TypeError unless point.respond_to?(:x) && point.respond_to?(:y)
# Process point - works with any object having x and y methods
end
Production Patterns
Struct excels as a foundation for value objects in domain-driven design, providing immutable-like data containers with semantic meaning. Value objects encapsulate related data and can include validation and business logic.
# Value object pattern with validation
class Money < Struct.new(:amount, :currency)
def initialize(amount, currency = "USD")
raise ArgumentError, "Amount must be positive" if amount < 0
raise ArgumentError, "Invalid currency" unless valid_currency?(currency)
super(amount.round(2), currency.upcase)
end
def +(other)
raise ArgumentError, "Currency mismatch" unless currency == other.currency
Money.new(amount + other.amount, currency)
end
def to_s
format("%.2f %s", amount, currency)
end
private
def valid_currency?(code)
%w[USD EUR GBP JPY].include?(code.upcase)
end
end
# Usage in business logic
price = Money.new(29.99, "USD")
tax = Money.new(2.40, "USD")
total = price + tax # => Money instance with amount: 32.39, currency: "USD"
Data Transfer Objects (DTOs) benefit from Struct's automatic accessor generation and serialization capabilities, particularly in API responses and service layer communication.
# DTO pattern for API responses
class UserResponse < Struct.new(:id, :username, :email, :created_at, :profile, keyword_init: true)
def self.from_user(user)
new(
id: user.id,
username: user.username,
email: user.email,
created_at: user.created_at.iso8601,
profile: ProfileResponse.from_profile(user.profile)
)
end
def to_json(*args)
to_h.to_json(*args)
end
def sanitized
dup.tap { |dto| dto.email = "[REDACTED]" if email }
end
end
class ProfileResponse < Struct.new(:first_name, :last_name, :bio, keyword_init: true)
def self.from_profile(profile)
return new unless profile
new(
first_name: profile.first_name,
last_name: profile.last_name,
bio: profile.bio&.truncate(100)
)
end
end
# Controller usage
def show
user = User.find(params[:id])
response = UserResponse.from_user(user)
render json: current_user.admin? ? response : response.sanitized
end
Configuration objects using Struct provide type safety and validation while maintaining simplicity for application settings and feature flags.
# Configuration pattern with defaults and validation
class DatabaseConfig < Struct.new(:host, :port, :database, :username, :password, :pool_size, :timeout, keyword_init: true)
DEFAULT_VALUES = {
host: "localhost",
port: 5432,
pool_size: 5,
timeout: 30
}.freeze
def initialize(**options)
merged_options = DEFAULT_VALUES.merge(options)
validate_required_fields(merged_options)
validate_types(merged_options)
super(**merged_options)
end
def connection_string
"postgresql://#{username}:#{password}@#{host}:#{port}/#{database}"
end
def pool_config
{
pool: pool_size,
checkout_timeout: timeout,
reaping_frequency: timeout * 2
}
end
private
def validate_required_fields(options)
required = [:database, :username, :password]
missing = required - options.keys
raise ArgumentError, "Missing required fields: #{missing.join(', ')}" if missing.any?
end
def validate_types(options)
raise ArgumentError, "Port must be integer" unless options[:port].is_a?(Integer)
raise ArgumentError, "Pool size must be positive" unless options[:pool_size] > 0
end
end
# Application usage
db_config = DatabaseConfig.new(
database: "myapp_production",
username: ENV["DB_USERNAME"],
password: ENV["DB_PASSWORD"],
host: ENV["DB_HOST"],
pool_size: ENV["DB_POOL_SIZE"]&.to_i
)
# Use in database connection setup
connection = DatabaseConnection.new(
db_config.connection_string,
**db_config.pool_config
)
Event sourcing patterns leverage Struct for creating immutable event objects that capture state changes with timestamps and metadata.
# Event sourcing with Struct-based events
class BaseEvent < Struct.new(:aggregate_id, :occurred_at, :version, :metadata, keyword_init: true)
def initialize(**args)
args[:occurred_at] ||= Time.current
args[:version] ||= 1
args[:metadata] ||= {}
super
freeze # Make events immutable
end
def event_type
self.class.name.demodulize
end
end
class UserRegisteredEvent < BaseEvent.new(:user_id, :email, :registration_source, keyword_init: true)
def initialize(user_id:, email:, registration_source: "web", **base_args)
super(aggregate_id: user_id, user_id: user_id, email: email, registration_source: registration_source, **base_args)
end
end
class OrderPlacedEvent < BaseEvent.new(:order_id, :customer_id, :total_amount, :items, keyword_init: true)
def initialize(order_id:, customer_id:, total_amount:, items:, **base_args)
super(aggregate_id: order_id, order_id: order_id, customer_id: customer_id,
total_amount: total_amount, items: items.freeze, **base_args)
end
def item_count
items.sum { |item| item[:quantity] }
end
end
# Event store usage
event_store = EventStore.new
event = UserRegisteredEvent.new(user_id: 123, email: "user@example.com")
event_store.append(event)
Reference
Class Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Struct.new(*attrs, **opts, &block) |
*attrs (Symbol/String), keyword_init: (Boolean), block (optional) |
Class |
Creates new Struct class with specified attributes |
Struct.keyword_init? |
none | Boolean |
Returns true if Struct uses keyword initialization |
Instance Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#initialize(*values) |
*values (any) |
self |
Creates new instance with attribute values |
#[] |
index (Integer/Symbol) |
Object |
Accesses attribute by index or name |
#[]= |
index (Integer/Symbol), value (Object) |
Object |
Sets attribute by index or name |
#each |
&block |
Enumerator/self |
Iterates over attribute values |
#each_pair |
&block |
Enumerator/self |
Iterates over attribute name-value pairs |
#length |
none | Integer |
Returns number of attributes |
#size |
none | Integer |
Alias for length |
#members |
none | Array<Symbol> |
Returns attribute names as symbols |
#to_a |
none | Array |
Converts to array of values |
#to_h |
&block |
Hash |
Converts to hash of names to values |
#values |
none | Array |
Returns array of attribute values |
#values_at(*indices) |
*indices (Integer) |
Array |
Returns values at specified indices |
#dig(*keys) |
*keys (Integer/Symbol) |
Object |
Navigates nested structures safely |
Comparison and Equality
Method | Parameters | Returns | Description |
---|---|---|---|
#== |
other (Object) |
Boolean |
Value equality comparison |
#eql? |
other (Object) |
Boolean |
Hash key equality comparison |
#hash |
none | Integer |
Hash code based on values |
Enumerable Methods
Struct instances include Enumerable, providing access to all enumerable methods:
Method | Returns | Description |
---|---|---|
#map(&block) |
Array |
Maps over attribute values |
#select(&block) |
Array |
Filters attribute values |
#find(&block) |
Object |
Finds first matching value |
#any?(&block) |
Boolean |
Tests if any value matches |
#all?(&block) |
Boolean |
Tests if all values match |
Creation Options
Option | Type | Description |
---|---|---|
keyword_init: |
Boolean |
Enables keyword argument initialization |
Common Patterns
Anonymous Struct Creation:
point_class = Struct.new(:x, :y)
point = point_class.new(1, 2)
Named Struct Creation:
Point = Struct.new(:x, :y)
point = Point.new(1, 2)
Keyword Initialization:
User = Struct.new(:name, :email, keyword_init: true)
user = User.new(name: "Alice", email: "alice@example.com")
Block Definition:
Rectangle = Struct.new(:width, :height) do
def area
width * height
end
end
Error Types
Error | Condition |
---|---|
ArgumentError |
Wrong number of arguments to constructor |
IndexError |
Invalid index access with [] |
TypeError |
Invalid type for index access |
FrozenError |
Modification of frozen Struct instance |
Memory and Performance
- Struct instances have lower memory overhead than equivalent custom classes
- Attribute access is faster than hash lookup but slower than instance variable access
- Struct creation has minimal overhead compared to class definition
- Enumeration methods create intermediate arrays, consider memory usage with large datasets