🤨

rubyのStruct Classについて調べる

に公開

きっかけ

railsのコードを見ているときに、Structが使われていました。
全然構造体知らなかったので、この機に気になるところをまとめてみたいと思います。

Structとは何か?

まずはドキュメントを読んでみて、理解してみます。
https://docs.ruby-lang.org/ja/latest/class/Struct.html

  • 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
主な用途 単純なデータ集約、値オブジェクト 複雑なオブジェクト、ドメインモデル、振る舞いのカプセル化
定型コード 最小限(initializeattr_accessorが自動生成) 明示的(initializeattr_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