rubyのStruct Classについて調べる
きっかけ
railsのコードを見ているときに、Structが使われていました。
全然構造体知らなかったので、この機に気になるところをまとめてみたいと思います。
Structとは何か?
まずはドキュメントを読んでみて、理解してみます。
- Rubyの標準組み込みである
- 属性アクセサ(.nameみたいな)を持つClassを作成できる
そもそも構造体とは、 プログラミング的に、異なるデータ型の変数をまとめて扱えるものだと思います。
new(*args, keyword_init: nil) -> Class
Structクラスに新しいサブクラスを作って、それを返します。
サブクラスでは、構造体のメンバに対するアクセスメソッドが定義されているみたいですね。
dog = Struct.new("Dog", :name, :age)
fred = dog.new("fred", 5)
fred.age = 6
printf "name:%s age:%d", fred.name, fred.age
#=> "name:fred age:6" を出力します
また、keyword_init: 引数によって、キーワード引数を使用するかを指定できるみたいです。
- nil: キーワード引数と位置引数のどちらを使用してもよい
- true: キーワード引数のみ使用できる
- false: キーワード引数は使用できず、位置引数のみ使用できる
new(*args, keyword_init: nil) {|subclass| block } -> Class
blockを指定することもできそうです。
個人的にこっちの使い方を見に来ました。
Struct.new にブロックを指定した場合は定義した Struct をコンテキストにブロックを評価します。また、定義した Struct はブロックパラメータにも渡されます。
Customer = Struct.new(:name, :address) do
def greeting
"Hello #{name}!"
end
end
Customer.new("Dave", "123 Main").greeting # => "Hello Dave!"
なるほど!上記の例だと、greetingのようなgetter?みたいなメソッドだったりを定義できるのですね。
色々試してみる
まずはブロックが評価されるとのことなので、次のコードを試してみます。
Customer = Struct.new(:name, :address) do
# selfは?
puts "1.self: #{self}"
def greeting
"Hello #{name}!, your address is #{address}."
end
# クラスメソッドも定義できる?
def self.default_customer
new("匿名", "不明")
end
end
puts "------"
puts "2. Customer", Customer
puts "3. ", Customer.new("山田太郎", "東京都")
puts "4. ", Customer.default_customer
puts "5. ", Customer.default_customer.greeting
selfやクラスメソッドを試してみます。
結果としては以下のようになりました。
1.self: #<Class:0x0000ffff7e3e89a0>
------
2. Customer
Customer
3.
#<struct Customer name="山田太郎", address="東京都">
4.
#<struct Customer name="匿名", address="不明">
5.
Hello 匿名!, your address is 不明.
面白いです、本当にクラスの定義ができますね。
もう一つが、ブロックパラメータも使えるみたいですね。
簡潔に試してみます。
Point = Struct.new(:x, :y) do |struct_class|
# struct_classは作成されたStructクラス自体
puts struct_class == self # => true
end
selfと同じようにクラス自身として使用できました。
クラスとの使い分け
う〜ん。使い方的には、調べた感じだと、
- ただのクラス:ビジネスロジック多めな時
- Structクラス:クラスにするまでもない時
とありました。
ただ、あんまり納得できなかった・・・
パフォーマンスなのでしょうか?
パフォーマンスはどうなん?
うーん。とりあえず、時間を測ってみます。
計測系はrubyだと何が良いのでしょうか?知っている方居ましたらコメントください・・・
今回はここにある通り、benchmark-ipsを使ってみます。
検証コードはclaudeに出してもらってます。
長いので詳細はこちら
#!/usr/bin/env ruby
require 'benchmark/ips'
# Struct版の定義
PersonStruct = Struct.new(:name, :age, :email) do
def adult?
age >= 18
end
def greeting
"こんにちは、#{name}です(#{age}歳)"
end
def contact_info
"#{name} <#{email}>"
end
end
# Class版の定義
class PersonClass
attr_accessor :name, :age, :email
def initialize(name, age, email)
@name = name
@age = age
@email = email
end
def adult?
age >= 18
end
def greeting
"こんにちは、#{name}です(#{age}歳)"
end
def contact_info
"#{name} <#{email}>"
end
def ==(other)
return false unless other.is_a?(PersonClass)
name == other.name && age == other.age && email == other.email
end
def to_h
{ name: name, age: age, email: email }
end
end
puts "=== Struct vs Class パフォーマンス比較 ==="
puts "Ruby #{RUBY_VERSION}"
puts
# テスト用データ
test_data = [
["田中太郎", 30, "tanaka@example.com"],
["佐藤花子", 25, "sato@example.com"],
["山田次郎", 40, "yamada@example.com"]
]
puts "📊 1. インスタンス作成"
puts "-" * 50
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Struct作成") do
PersonStruct.new("田中太郎", 30, "tanaka@example.com")
end
x.report("Class作成") do
PersonClass.new("田中太郎", 30, "tanaka@example.com")
end
x.compare!
end
puts "\n📊 2. 属性アクセス(読み取り)"
puts "-" * 50
struct_person = PersonStruct.new("田中太郎", 30, "tanaka@example.com")
class_person = PersonClass.new("田中太郎", 30, "tanaka@example.com")
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Struct読み取り") do
struct_person.name
struct_person.age
struct_person.email
end
x.report("Class読み取り") do
class_person.name
class_person.age
class_person.email
end
x.compare!
end
puts "\n📊 3. 属性アクセス(書き込み)"
puts "-" * 50
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Struct書き込み") do
person = PersonStruct.new("", 0, "")
person.name = "田中太郎"
person.age = 30
person.email = "tanaka@example.com"
end
x.report("Class書き込み") do
person = PersonClass.new("", 0, "")
person.name = "田中太郎"
person.age = 30
person.email = "tanaka@example.com"
end
x.compare!
end
puts "\n📊 4. メソッド呼び出し"
puts "-" * 50
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Structメソッド") do
struct_person.adult?
struct_person.greeting
struct_person.contact_info
end
x.report("Classメソッド") do
class_person.adult?
class_person.greeting
class_person.contact_info
end
x.compare!
end
puts "\n📊 5. 等価性比較"
puts "-" * 50
struct1 = PersonStruct.new("田中太郎", 30, "tanaka@example.com")
struct2 = PersonStruct.new("田中太郎", 30, "tanaka@example.com")
class1 = PersonClass.new("田中太郎", 30, "tanaka@example.com")
class2 = PersonClass.new("田中太郎", 30, "tanaka@example.com")
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Struct等価性") do
struct1 == struct2
end
x.report("Class等価性") do
class1 == class2
end
x.compare!
end
puts "\n📊 6. ハッシュ変換"
puts "-" * 50
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Struct to_h") do
struct_person.to_h
end
x.report("Class to_h") do
class_person.to_h
end
x.compare!
end
puts "\n📊 7. 配列変換・インデックスアクセス"
puts "-" * 50
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Struct配列変換") do
struct_person.values
end
x.report("Struct[0]アクセス") do
struct_person[0] # name
struct_person[1] # age
struct_person[2] # email
end
x.report("Struct[:name]アクセス") do
struct_person[:name]
struct_person[:age]
struct_person[:email]
end
# Classには対応するメソッドがないのでスキップ
end
puts "\n📊 8. 大量作成テスト"
puts "-" * 50
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Struct大量作成") do
100.times do |i|
PersonStruct.new("Person#{i}", 20 + i % 50, "person#{i}@example.com")
end
end
x.report("Class大量作成") do
100.times do |i|
PersonClass.new("Person#{i}", 20 + i % 50, "person#{i}@example.com")
end
end
x.compare!
end
puts "\n📊 9. 複雑な操作チェーン"
puts "-" * 50
Benchmark.ips do |x|
x.config(time: 3, warmup: 1)
x.report("Struct操作チェーン") do
person = PersonStruct.new("田中太郎", 30, "tanaka@example.com")
person.adult? && person.greeting.length > 10 && person.to_h.keys.size == 3
end
x.report("Class操作チェーン") do
person = PersonClass.new("田中太郎", 30, "tanaka@example.com")
person.adult? && person.greeting.length > 10 && person.to_h.keys.size == 3
end
x.compare!
end
puts "\n📈 メモリ使用量比較"
puts "-" * 50
require 'objspace'
struct_size = ObjectSpace.memsize_of(PersonStruct.new("田中太郎", 30, "tanaka@example.com"))
class_size = ObjectSpace.memsize_of(PersonClass.new("田中太郎", 30, "tanaka@example.com"))
puts "Structインスタンス: #{struct_size} bytes"
puts "Classインスタンス: #{class_size} bytes"
puts "差分: #{class_size - struct_size} bytes (#{((class_size.to_f / struct_size - 1) * 100).round(1)}%)"
パフォーマンス結果
=== Struct vs Class パフォーマンス比較 ===
Ruby 3.4.2
📊 1. インスタンス作成
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Struct作成 457.486k i/100ms
Class作成 174.534k i/100ms
Calculating -------------------------------------
Struct作成 4.692M (± 2.0%) i/s (213.12 ns/i) - 14.182M in 3.023652s
Class作成 4.787M (± 2.0%) i/s (208.92 ns/i) - 14.486M in 3.027741s
Comparison:
Class作成: 4786532.4 i/s
Struct作成: 4692254.1 i/s - same-ish: difference falls within error
📊 2. 属性アクセス(読み取り)
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Struct読み取り 2.566M i/100ms
Class読み取り 2.260M i/100ms
Calculating -------------------------------------
Struct読み取り 26.267M (± 2.5%) i/s (38.07 ns/i) - 79.545M in 3.030304s
Class読み取り 22.036M (± 2.8%) i/s (45.38 ns/i) - 67.810M in 3.079599s
Comparison:
Struct読み取り: 26266596.6 i/s
Class読み取り: 22036134.7 i/s - 1.19x slower
📊 3. 属性アクセス(書き込み)
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Struct書き込み 300.097k i/100ms
Class書き込み 303.446k i/100ms
Calculating -------------------------------------
Struct書き込み 3.007M (± 1.1%) i/s (332.60 ns/i) - 9.303M in 3.094546s
Class書き込み 3.031M (± 1.2%) i/s (329.95 ns/i) - 9.103M in 3.004089s
Comparison:
Class書き込み: 3030801.3 i/s
Struct書き込み: 3006597.5 i/s - same-ish: difference falls within error
📊 4. メソッド呼び出し
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Structメソッド 341.149k i/100ms
Classメソッド 328.570k i/100ms
Calculating -------------------------------------
Structメソッド 3.373M (± 4.4%) i/s (296.43 ns/i) - 10.234M in 3.041214s
Classメソッド 3.292M (± 1.9%) i/s (303.77 ns/i) - 10.186M in 3.095194s
Comparison:
Structメソッド: 3373431.9 i/s
Classメソッド: 3292012.3 i/s - same-ish: difference falls within error
📊 5. 等価性比較
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Struct等価性 821.528k i/100ms
Class等価性 876.417k i/100ms
Calculating -------------------------------------
Struct等価性 8.154M (± 0.8%) i/s (122.64 ns/i) - 24.646M in 3.022675s
Class等価性 8.662M (± 1.5%) i/s (115.44 ns/i) - 26.293M in 3.035905s
Comparison:
Class等価性: 8662426.7 i/s
Struct等価性: 8154169.9 i/s - 1.06x slower
📊 6. ハッシュ変換
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Struct to_h 782.176k i/100ms
Class to_h 819.761k i/100ms
Calculating -------------------------------------
Struct to_h 7.832M (± 1.4%) i/s (127.68 ns/i) - 24.247M in 3.096635s
Class to_h 8.209M (± 2.7%) i/s (121.81 ns/i) - 25.413M in 3.098026s
Comparison:
Class to_h: 8209207.2 i/s
Struct to_h: 7831814.2 i/s - 1.05x slower
📊 7. 配列変換・インデックスアクセス
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Struct配列変換 1.280M i/100ms
Struct[0]アクセス 1.672M i/100ms
Struct[:name]アクセス 1.202M i/100ms
Calculating -------------------------------------
Struct配列変換 12.656M (± 0.7%) i/s (79.02 ns/i) - 38.398M in 3.034228s
Struct[0]アクセス 16.402M (± 1.8%) i/s (60.97 ns/i) - 50.172M in 3.059824s
Struct[:name]アクセス 11.317M (± 7.4%) i/s (88.36 ns/i) - 34.868M in 3.105206s
📊 8. 大量作成テスト
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Struct大量作成 2.500k i/100ms
Class大量作成 2.620k i/100ms
Calculating -------------------------------------
Struct大量作成 26.399k (± 1.5%) i/s (37.88 μs/i) - 80.000k in 3.031100s
Class大量作成 26.201k (± 2.3%) i/s (38.17 μs/i) - 78.600k in 3.001524s
Comparison:
Struct大量作成: 26399.1 i/s
Class大量作成: 26200.8 i/s - same-ish: difference falls within error
📊 9. 複雑な操作チェーン
--------------------------------------------------
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
Warming up --------------------------------------
Struct操作チェーン 141.532k i/100ms
Class操作チェーン 144.466k i/100ms
Calculating -------------------------------------
Struct操作チェーン 1.405M (± 1.6%) i/s (711.79 ns/i) - 4.246M in 3.023058s
Class操作チェーン 1.425M (± 2.1%) i/s (701.98 ns/i) - 4.334M in 3.043805s
Comparison:
Class操作チェーン: 1424534.8 i/s
Struct操作チェーン: 1404900.5 i/s - same-ish: difference falls within error
📈 メモリ使用量比較
--------------------------------------------------
Structインスタンス: 40 bytes
Classインスタンス: 40 bytes
差分: 0 bytes (0.0%)
うーん。パフォーマンスにおいて、特にStructが優れてするわけではなさそうですね。
なんならClassの方が良い。
Gemini回答
色々調べていましたが、Deep Researchのgeminiの回答が参考になりそうなので、貼り付けます
特徴 | Struct | Class |
---|---|---|
主な用途 | 単純なデータ集約、値オブジェクト | 複雑なオブジェクト、ドメインモデル、振る舞いのカプセル化 |
定型コード | 最小限(initialize 、attr_accessor が自動生成) |
明示的(initialize 、attr_accessor/reader/writer を手動定義) |
属性定義 |
Struct.new の引数(シンボル)経由 |
インスタンス変数(@variable_name )経由 |
アクセサメソッド | 自動生成されるattr_accessor
|
手動でattr_accessor/reader/writer を定義 |
初期化 | 位置引数またはキーワード引数(自動生成) | カスタムinitialize メソッド(完全な制御) |
デフォルトの等価性 (== ) |
値の等価性(メンバーの値を比較) | 参照等価性(オブジェクトの同一性を比較) |
内部データストレージ | インデックス付きコレクション(@ivars ではない) |
インスタンス変数(@ivars ) |
カスタムメソッドの追加 |
Struct.new へのブロック経由(シンプル、データ関連の振る舞い) |
標準のメソッド定義経由(あらゆる複雑な振る舞い) |
継承の適合性 |
Struct のサブクラス;深く複雑な階層には不向き |
完全なOOP継承サポート;階層に最適 |
パフォーマンス (Ruby 3.4.1) | Classよりわずかに遅い | Structよりわずかに速い |
この表はわかりやすいですね。
初期化や属性定義など、単にデータを一時的に持ちたいなら、クラスより有効な理由がなんとなく理解できました。
VSクラス まとめ
Structは、シンプルなデータを扱う場合や、振る舞いよりも「値」が重要な場面で特に有効
- 値によって定義され、固有の識別子や複雑な動作を必要としないオブジェクト
- 一時的にスコープ内で使うデータ(フォーム送信値、APIレスポンス、計算結果など)を手軽にまとめたいとき
- テスト用に簡易的なオブジェクトが欲しいとき
- 小さなデータ型をたくさん定義するような状況
Hashとの使い分けは?
hashの場合、未定義の属性も、簡単に追加できてしまいます。
厳格に特定の構造を定義したいときがあると思いますので、StructとHashの使い分けは簡単ですね。
使い分けれるのでここは調べるのはやめておきます。。。(力尽きた)
Discussion