Ruby Structでデータ構造を簡単に定義する方法:シンプルさと効率性の探求
導入:データ構造の必要性とRubyでの選択肢
ソフトウェア開発において、データを扱うことは避けられません。ユーザー情報、製品情報、設定値、APIからの応答など、様々な種類のデータを効率的に管理し、操作する必要があります。これらのデータを整理するための「器」となるのがデータ構造です。
Rubyは非常に柔軟な言語であり、データ構造を表現するための様々な手段を提供しています。最も基本的なのは、キーと値のペアを扱うHash
です。シンプルなデータの集まりには便利ですが、キーの名前を常に文字列やシンボルで指定する必要があり、タイプミスが発生しやすいという欠点があります。
もう少し構造化されたデータを扱いたい場合、多くの開発者はクラス(Class
)を定義します。クラスを使えば、属性(インスタンス変数)とそれに対応するアクセサメソッド(attr_accessor
, attr_reader
, attr_writer
)を明確に定義でき、さらにデータを操作するためのメソッド(振る舞い)を追加することも可能です。これは非常に強力で柔軟な方法ですが、単純にデータのまとまりだけを表現したい場合には、やや大げさで定義が冗長になりがちです。特に、たくさんの小さなデータ構造が必要な場合、そのたびにクラスを定義し、attr_accessor
を並べるのは手間がかかります。
ここで登場するのが、Rubyの標準ライブラリに含まれるStruct
です。Struct
は、シンプルなデータ構造を、クラスを定義するよりもはるかに少ないコード量で、しかもタイプセーフに(属性名の間違いを防ぎやすく)定義することを可能にします。まるで軽量なクラス定義ツールのようなものです。
この記事では、RubyのStruct
に焦点を当て、その基本的な使い方から、応用的な機能、そして他のデータ構造(Hash
、OpenStruct
、Class
)との比較、パフォーマンスに関する考察、さらには利用する際の注意点やベストプラクティスまで、詳細に掘り下げていきます。約5000語という十分な量を使って、Struct
の魅力と使い方を網羅的に解説します。
シンプルなデータ構造をRubyで扱いたいと考えている方、既存のHash
やClass
の使い方に疑問を感じている方、あるいは単にStruct
について深く学びたい方にとって、この記事が役立つことを願っています。
Structの基本:シンプルなデータ構造の定義
Struct
は、複数の属性を持つオブジェクトを簡単に定義・生成するためのクラスです。基本的な使い方は非常にシンプルで、Struct.new
メソッドに、作成したい構造体のメンバー(属性)名をシンボルまたは文字列で指定するだけです。
Structクラスの生成とインスタンスの作成
Struct.new
を呼び出すと、新しいStruct
のサブクラスが生成されて返されます。この返されたクラスを使って、具体的なデータを持つインスタンスを作成します。
例:ユーザー情報を表すシンプルな構造体
“`ruby
Userという名前のStructクラスを定義
メンバーは :name, :age, :email
User = Struct.new(:name, :age, :email)
定義したUserクラスを使ってインスタンスを作成
user1 = User.new(“Alice”, 30, “[email protected]”)
user2 = User.new(“Bob”, 25, “[email protected]”)
“`
この例では、User = Struct.new(:name, :age, :email)
によって、name
, age
, email
という3つのメンバーを持つUser
という名前の新しいクラスが定義されています。このクラスは、あたかも以下のように定義されたクラスと似た振る舞いをします。
“`ruby
Structを使わない場合の類似クラス定義
ただし、Structはより簡潔で効率的
class UserManual
attr_accessor :name, :age, :email
def initialize(name, age, email)
@name = name
@age = age
@email = email
end
end
“`
Structを使うことで、initialize
メソッドやattr_accessor
の記述を省略できるため、非常に簡潔になります。
Struct.new
に渡すメンバー名は、シンボル(:name
, :age
, :email
)でも文字列("name"
, "age"
, "email"
)でも構いません。ただし、Rubyの慣習としてシンボルがよく使われます。また、渡された順序がインスタンス作成時の引数の順序に対応します。
属性へのアクセス
Structのインスタンスは、定義されたメンバーに対応するアクセサメソッド(ゲッターとセッター)を自動的に持ちます。これにより、ドット記法(.
)を使って属性の値を取得したり設定したりできます。
“`ruby
属性の値を取得 (ゲッター)
puts user1.name # => Alice
puts user1.age # => 30
puts user1.email # => [email protected]
属性の値を変更 (セッター)
user1.age = 31
puts user1.age # => 31
“`
また、Structのインスタンスは配列やハッシュのように[]
演算子を使って属性にアクセスすることもできます。ゲッターとしてはメンバー名(シンボルまたは文字列)、セッターとしてはメンバー名と新しい値を指定します。インデックスでアクセスすることも可能ですが、可読性の観点からはメンバー名を使う方が推奨されます。
“`ruby
[] 演算子を使ったアクセス (ゲッター)
puts user2[:name] # => Bob
puts user2[“age”] # => 25 (文字列でも可)
puts user2[2] # => [email protected] (インデックスでも可)
[]= 演算子を使った値の設定 (セッター)
user2[:email] = “[email protected]”
puts user2.email # => [email protected]
インデックスを使った設定 (非推奨)
user2[0] = “Bobby”
puts user2.name # => Bobby
“`
インデックスによるアクセスは、メンバーの定義順序に依存するため、コードの変更に対して脆弱になりやすいです。通常はメンバー名(シンボルまたは文字列)を使ったアクセスが望ましいでしょう。
Structクラスの名前付け
上記の例では、User = Struct.new(...)
のように、Struct.new
が返した無名クラスを定数User
に代入することで名前を付けています。これは最も一般的なStruct
の利用方法です。
もし、Struct.new
に最初の引数として文字列を与える場合、その文字列がクラス名として使われます。この場合、Struct.new
は名前付きのクラスを返しますが、それを定数に代入するのは引き続き開発者の責任です。
“`ruby
クラス名文字列を指定してStructクラスを生成
BookStructClass = Struct.new(“Book”, :title, :author, :isbn)
BookStructClassという定数に、クラス名が”Book”のStructクラスが代入される
puts BookStructClass.name # => Book
book1 = BookStructClass.new(“The Lord of the Rings”, “J.R.R. Tolkien”, “978-061826027
“`
クラス名文字列を指定するメリットは、デバッグ時などにオブジェクトのクラス名が表示される際に、より分かりやすくなることです。指定しない場合は、#<struct name="..." age="..." ...>
のように、クラス名が表示されません(あるいはRubyの内部的な無名クラス名が表示される場合があります)。
どちらの方法でクラスに名前を付けるかは、開発者の好みやプロジェクトの規約によりますが、定数に代入して使うのが一般的です。
immutableなStructインスタンスの生成
Structはデフォルトではmutable(変更可能)ですが、インスタンス生成時にfreeze
メソッドを呼び出すことでimmutable(変更不可能)にすることができます。
“`ruby
User = Struct.new(:name, :age)
user = User.new(“Charlie”, 40)
インスタンスは変更可能
user.age = 41
puts user.age # => 41
インスタンスをimmutableにする
user.freeze
変更しようとするとエラーが発生
user.name = “Charles” # => FrozenError: can’t modify frozen User: “Charlie”
“`
immutableなデータ構造は、特に複数のスレッド間でデータを共有する場合や、意図しないデータ変更を防ぎたい場合に役立ちます。ただし、freeze
はインスタンス自体をimmutableにするだけであり、そのインスタンスが参照しているオブジェクト(例えば配列やハッシュ)がmutableであれば、その内部のオブジェクトは変更可能です。
Structの詳細:カスタマイズと機能
Structは単に属性を定義するだけでなく、ブロックを使ってメソッドを追加したり、初期化ロジックをカスタマイズしたりといった、より高度な使い方も可能です。
ブロックを使ったクラスのカスタマイズ
Struct.new
メソッドにブロックを渡すと、そのブロックは生成されたStructクラスのコンテキストで実行されます。これにより、Structクラスにメソッドを定義したり、クラス変数やクラスメソッドを追加したりすることができます。
“`ruby
ブロックを使ってメソッドを追加
User = Struct.new(:name, :age, :email) do
# インスタンスメソッドを追加
def greeting
“Hello, my name is #{name} and I am #{age} years old.”
end
# フルネームを返すメソッド (例として複雑なロジックは避ける)
def full_name
name # この例ではシンプルに名前だけ
end
# クラスメソッドを追加 (Struct.newで定義されたクラスに対して)
def self.default_user
new(“Guest”, 0, “[email protected]”)
end
end
user = User.new(“David”, 22, “[email protected]”)
puts user.greeting # => Hello, my name is David and I am 22 years old.
guest_user = User.default_user
puts guest_user.name # => Guest
“`
ブロック内では、self
は生成されたStructクラス自体を参照します。そのため、インスタンスメソッドは通常のクラス定義と同様にdef method_name ... end
で定義し、クラスメソッドはdef self.method_name ... end
で定義します。
初期化ロジックのカスタマイズ
デフォルトでは、Structインスタンスはnew
メソッドに渡された引数を定義順にメンバーに割り当てて初期化されます。しかし、initialize
メソッドをブロック内で定義することで、この初期化プロセスをカスタマイズできます。
例えば、特定のメンバーにデフォルト値を設定したい場合や、入力値を加工したい場合にinitialize
を使います。
“`ruby
Product = Struct.new(:name, :price, :stock) do
def initialize(name:, price: 0, stock: 0) # キーワード引数とデフォルト値を使用
super(name: name, price: price, stock: stock) # Structのデフォルト初期化を呼び出す
# または self.name = name; self.price = price; self.stock = stock
# 初期化時に何か特別な処理を行う場合
puts “新しいプロダクトが作成されました: #{name}”
end
# メンバーへのアクセスを調整する例 (Structでは直接アクセスが推奨される)
# def price
# @price.to_f # 例: 必ずFloatとして返す
# end
end
デフォルト値を使ってインスタンスを作成
product1 = Product.new(name: “Laptop”) # priceとstockはデフォルト値0になる
puts product1.price # => 0
puts product1.stock # => 0
全ての値を指定してインスタンスを作成
product2 = Product.new(name: “Mouse”, price: 2500, stock: 50)
puts product2.price # => 2500
puts product2.stock # => 50
Struct.newのデフォルト初期化を使わない場合(非推奨)
class MyStruct < Struct.new(:a, :b)
def initialize(x, y)
# superを呼ばない場合、@a, @bは初期化されない
@a = x * 2
@b = y * 3
end
end
s = MyStruct.new(1, 2)
puts s.a # nil (accessorがあるがインスタンス変数がない)
puts s.b # nil
Structではsuperを呼んでデフォルトの初期化を完了させるのが一般的
“`
initialize
メソッドを定義した場合、Structのデフォルトの初期化(引数を順番にメンバーに割り当てる処理)は自動的には行われません。カスタマイズしたinitialize
メソッド内で、super(...)
を呼び出すことで、Structのデフォルト初期化ロジックを実行できます。super
に渡す引数は、Struct.new
で定義したメンバーの順番に従う必要があります。また、Ruby 2.5以降では、keyword_init: true
オプションと組み合わせて、キーワード引数としてsuper
に渡すことも可能です。
注意点: ブロック内でインスタンス変数(@name
など)に直接アクセスすることは、Structの推奨されるスタイルではありません。Structはあくまでメンバー(アクセサメソッドを通じてアクセスされる属性)を定義するためのものであり、複雑な状態管理やインスタンス変数への直接アクセスが必要な場合は、通常のClass
を使う方が適切です。メンバーの値は、定義されたアクセサメソッド(name
, name=
など)を通じて取得・設定するのがStructの正しい使い方です。
デフォルト値の設定 (Struct.new
の新しい機能)
Ruby 2.5以降では、Struct.new
のメンバー定義時にデフォルト値を指定できるようになりました。これはinitialize
をカスタマイズするよりも簡潔な方法です。
“`ruby
デフォルト値を指定
Item = Struct.new(:name, :price, :quantity, keyword_init: true) do
# initializeメソッドは不要(デフォルト値は自動で設定される)
# ただし、価格がマイナスにならないようにチェックするなどのロジックは initialize で追加可能
def initialize(name:, price: 0.0, quantity: 0)
# ここでsuperを呼び出す必要はない(またはsuper(…)とキーワード引数で呼ぶ)
# デフォルト値は Struct.new の定義で自動設定される
# ここでは追加のバリデーションや加工を行う
raise ArgumentError, “Price cannot be negative” if price < 0
raise ArgumentError, “Quantity cannot be negative” if quantity < 0
super(name: name, price: price, quantity: quantity) # ここで super を呼ぶのが正しい
end
end
デフォルト値を使ったインスタンス生成 (keyword_init が便利)
item1 = Item.new(name: “Book”) # priceとquantityはデフォルト値
puts item1.price # => 0.0
puts item1.quantity # => 0
item2 = Item.new(name: “Pen”, price: 1.5) # quantityはデフォルト値
puts item2.quantity # => 0
全て指定
item3 = Item.new(name: “Eraser”, price: 0.8, quantity: 10)
puts item3.price # => 0.8
puts item3.quantity # => 10
例外が発生するケース
item4 = Item.new(name: “Error Item”, price: -10) # => ArgumentError
“`
Struct.new(:name, :price: 0.0, :quantity: 0)
のように、メンバー名の後に : デフォルト値
の形式で指定します。この機能を使う場合、インスタンス生成は通常、デフォルト値を考慮した形(多くの場合キーワード引数)で行われるため、keyword_init: true
オプションと組み合わせて使うのが一般的です。
initialize
メソッドをカスタム定義した場合でも、super
を適切に呼ぶことで、Struct.new
で指定したデフォルト値を活かすことができます。
キーワード引数を使ったインスタンス生成 (keyword_init: true
)
Ruby 2.5以降では、Struct.new
の最後のオプションとしてkeyword_init: true
を指定できます。これにより、Structのインスタンスを生成する際に、位置引数ではなくキーワード引数を使うことが必須(または推奨)になります。
“`ruby
keyword_init を有効にする
Address = Struct.new(:street, :city, :zip_code, keyword_init: true)
キーワード引数でインスタンスを作成
address1 = Address.new(street: “123 Main St”, city: “Anytown”, zip_code: “12345”)
キーワード引数の順序は問わない
address2 = Address.new(city: “Otherville”, zip_code: “67890”, street: “456 Oak Ave”)
puts address1.street # => 123 Main St
puts address2.city # => Otherville
位置引数で作成しようとするとエラー (keyword_init: true の場合)
address3 = Address.new(“789 Pine Ln”, “Smallville”, “11223”) # => ArgumentError (wrong number of arguments)
“`
keyword_init: true
を使うメリットは、インスタンス生成時の可読性が向上することと、メンバーの定義順序を気にせずに引数を渡せるようになることです。特にメンバーが多いStructの場合、どの値がどのメンバーに対応するのかが一目でわかるため、コードが読みやすくなります。
デフォルト値とkeyword_init: true
は非常によく一緒に使われます。
“`ruby
デフォルト値と keyword_init を組み合わせる
Settings = Struct.new(:timeout_seconds, :retries, :log_level, keyword_init: true) do
def initialize(timeout_seconds: 30, retries: 3, log_level: “info”)
# ここで super を呼んでデフォルト値やキーワード引数の処理を Struct に任せる
super(timeout_seconds: timeout_seconds, retries: retries, log_level: log_level)
# 必要に応じて追加のバリデーションなど
raise ArgumentError, "Timeout must be positive" if self.timeout_seconds <= 0
end
end
デフォルト値のみ使用
settings1 = Settings.new
puts settings1.timeout_seconds # => 30
puts settings1.retries # => 3
puts settings1.log_level # => info
一部を上書き
settings2 = Settings.new(timeout_seconds: 60, log_level: “debug”)
puts settings2.timeout_seconds # => 60
puts settings2.retries # => 3
puts settings2.log_level # => debug
“`
デフォルト値とカスタムinitialize
を組み合わせる場合、カスタムinitialize
のシグネチャ(引数の定義)は、デフォルト値を含むメンバー全てをカバーするように定義し、その中でsuper
を使ってStructのデフォルト初期化を呼び出すのが一般的で安全な方法です。
Structクラスのサブクラス化
Struct.new
で生成されたクラスは、他のクラスと同様に継承してサブクラスを作成することができます。これにより、基本的なStruct構造に加えて、さらにメンバーを追加したり、既存のメソッドをオーバーライドしたり、新しいメソッドを定義したりすることが可能です。
“`ruby
基本のStructクラス
Person = Struct.new(:name, :age, keyword_init: true)
Personを継承したStudentクラス
class Student < Person
attr_accessor :student_id, :major
def initialize(name:, age:, student_id:, major:)
super(name: name, age: age) # 親クラスの初期化を呼び出す
@student_id = student_id
@major = major
end
def greeting
“Hello, I’m #{name}, a #{major} student.”
end
# 親クラスで定義されていないメソッドを追加
def student_info
“Student ID: #{student_id}, Major: #{major}”
end
end
Studentインスタンスの作成
student1 = Student.new(name: “Eve”, age: 20, student_id: “S123”, major: “Computer Science”)
puts student1.name # => Eve (Personから継承)
puts student1.age # => 20 (Personから継承)
puts student1.student_id # => S123 (Studentで追加)
puts student1.major # => Computer Science (Studentで追加)
puts student1.greeting # => Hello, I’m Eve, a Computer Science student. (Studentでオーバーライド)
puts student1.student_info # => Student ID: S123, Major: Computer Science (Studentで追加)
“`
サブクラスでメンバーを追加する場合、attr_accessor
などで明示的に定義する必要があります。また、サブクラスのinitialize
メソッドを定義する場合は、親クラスのinitialize
(つまりStructのデフォルト初期化部分)をsuper
で適切に呼び出す必要があります。super
には親クラスのメンバーに対応する引数を渡します。keyword_init: true
を使っている場合は、super
にもキーワード引数で渡すのが自然です。
Structのサブクラス化は可能ですが、Structの主な目的はシンプルなデータ構造の定義です。複雑な継承階層を構築する必要がある場合は、最初から通常のClassを使う方が、より柔軟で設計しやすい場合があります。
Structインスタンスの比較
Structインスタンスは、定義されたメンバーの値がすべて等しい場合に==
演算子で等しいと判断されます。メンバーの順序やインスタンス自体が同一であるかは関係ありません。
“`ruby
Point = Struct.new(:x, :y)
p1 = Point.new(1, 2)
p2 = Point.new(1, 2)
p3 = Point.new(3, 4)
puts p1 == p2 # => true (メンバーの値が同じ)
puts p1 == p3 # => false (メンバーの値が異なる)
StructインスタンスとHashを比較することも可能 (Hashのキー/値とメンバー名/値が一致するか)
puts p1 == { x: 1, y: 2 } # => true
puts p1 == { y: 2, x: 1 } # => true (Hashは順序を問わない)
puts p1 == { x: 1, y: 2, z: 3 } # => false (HashのキーがStructのメンバーより多い場合は false)
puts p1 == { x: 1 } # => false (HashのキーがStructのメンバーより少ない場合は false)
“`
Structインスタンスはeql?
メソッドも定義しており、これも==
と同様にメンバーの値による比較を行います。これにより、Hash
のキーとしてStructインスタンスを使用した場合に、値が等しい異なるインスタンスが同じキーとして扱われるようになります(ただし、StructインスタンスをHashのキーとして使うことはあまり一般的ではありません)。
Structインスタンスのイテレーションと情報取得
StructインスタンスはEnumerable
モジュールをインクルードしていませんが、データへのアクセスや情報の取得に便利なメソッドをいくつか持っています。
each
: 各メンバーの値をブロックに渡して繰り返します。each_pair
: 各メンバー名と値のペアを配列[member_name, value]
としてブロックに渡して繰り返します。members
: Structのメンバー名(シンボル)の配列を返します。values
: 各メンバーの値の配列を返します。to_h
: StructインスタンスをHashに変換します。メンバー名がキー、メンバーの値が値になります。length
,size
: メンバーの数を返します。
“`ruby
Color = Struct.new(:red, :green, :blue)
color = Color.new(255, 128, 64)
各メンバーの値を取得
color.each do |value|
puts value
end
=> 255
=> 128
=> 64
各メンバー名と値のペアを取得
color.each_pair do |name, value|
puts “#{name}: #{value}”
end
=> red: 255
=> green: 128
=> blue: 64
メンバー名のリスト
puts color.members # => [:red, :green, :blue]
値のリスト
puts color.values # => [255, 128, 64]
Hashへの変換
puts color.to_h # => {:red=>255, :green=>128, :blue=>64}
メンバーの数
puts color.length # => 3
puts color.size # => 3
“`
これらのメソッドは、Structインスタンスのデータをまとめて処理したり、他の形式(Hashや配列)に変換したりする場合に非常に便利です。特にto_h
は、Structを一時的なデータホルダーとして使い、最終的にHashとして出力する場合によく利用されます。
Structの応用例
Structは、そのシンプルさと効率性から、様々な場面で役立ちます。ここではいくつかの具体的な応用例を紹介します。
1. 設定オブジェクトとして
アプリケーションの設定値を保持するためにStructを使うのは良い方法です。設定値は固定されたキーと値のペアであり、通常は読み取り専用で扱われます。
“`ruby
config.yml (例)
database:
host: localhost
port: 5432
username: app_user
password: secret
server:
port: 8080
env: production
require ‘yaml’
設定構造体の定義 (ネストした構造も表現しやすい)
DatabaseConfig = Struct.new(:host, :port, :username, :password, keyword_init: true)
ServerConfig = Struct.new(:port, :env, keyword_init: true)
AppConfig = Struct.new(:database, :server, keyword_init: true)
YAMLファイルを読み込み、Structインスタンスに変換する関数
def load_config(filename)
config_hash = YAML.load_file(filename, symbolize_names: true)
# ハッシュからStructインスタンスを生成
db_config = DatabaseConfig.new(config_hash[:database])
server_config = ServerConfig.new(config_hash[:server])
AppConfig.new(database: db_config, server: server_config)
end
設定ファイルの読み込み
config = load_config(‘config.yml’)
設定値へのアクセス
puts config.database.host # => localhost
puts config.server.port # => 8080
“`
この例では、ネストした設定もStructを使って構造化しています。キーワード引数 (**hash
) を使うことで、HashからStructへの変換が簡潔になります。このようにStructを使うと、設定値にドット記法でアクセスでき、どのような設定項目があるかがコードから一目で分かります。
2. DTO (Data Transfer Object) として
DTOは、異なるプロセス、境界、あるいはレイヤー間でデータをやり取りするために使用されるオブジェクトです。DTOは通常、データとそのアクセサメソッドのみを持ち、ビジネスロジックは含まれません。StructはまさにDTOの定義にぴったり合致します。
“`ruby
APIからのレスポンスデータを表現するDTO
例: GET /products/1 の応答
{
“id”: 1,
“name”: “Wireless Mouse”,
“price”: 25.00,
“in_stock”: true
}
ProductDTO = Struct.new(:id, :name, :price, :in_stock, keyword_init: true)
APIクライアントの例 (実際のAPI呼び出しは省略)
class ApiClient
def get_product(product_id)
# ここでAPIを呼び出し、JSON応答を受け取ると仮定
response_hash = {
id: product_id,
name: “Wireless Mouse”,
price: 25.00,
in_stock: true
}
# ハッシュからProductDTOインスタンスを作成
ProductDTO.new(**response_hash)
end
end
client = ApiClient.new
product = client.get_product(1)
puts product.id # => 1
puts product.name # => Wireless Mouse
puts product.price # => 25.0
puts product.in_stock # => true
“`
APIの応答やデータベースのレコードなどをStructにマッピングすることで、コード全体でそのデータの構造が明確になり、タイプミスによるエラーを防ぎやすくなります。また、Hashでデータを引き回すよりも、Structインスタンスとして扱う方が、そのデータが何を表しているのかがより分かりやすくなります。
3. シンプルなレコードやタプルとして
データベースの1行や、CSVファイルの1行など、複数の関連する値をまとめて扱う必要がある場合に、Structは非常に便利です。
“`ruby
CSVファイルから読み込んだデータを保持する構造体
Example CSV:
name,city,country
Alice,Tokyo,Japan
Bob,Paris,France
require ‘csv’
TravelerRecord = Struct.new(:name, :city, :country, keyword_init: true)
records = []
CSV.foreach(“travelers.csv”, headers: true, skip_blanks: true) do |row|
# CSV::Row オブジェクトから Struct インスタンスを生成
# row.to_h を使うとシンプル
records << TravelerRecord.new(**row.to_h)
end
読み込んだレコードを表示
records.each do |traveler|
puts “#{traveler.name} is in #{traveler.city}, #{traveler.country}”
end
=> Alice is in Tokyo, Japan
=> Bob is in Paris, France
“`
この例のように、外部データソースからの入力を一旦Structインスタンスに変換することで、その後の処理が構造化され、コードの可読性と保守性が向上します。
4. パターンマッチングとの組み合わせ (Ruby 2.7+)
Ruby 2.7で導入されたパターンマッチング(実験的な機能を含む)は、Structと非常に相性が良いです。Structインスタンスの構造をパターンとして記述し、データがそのパターンに一致するかどうかをチェックしたり、メンバーの値を抽出したりできます。
“`ruby
シンプルなメッセージ構造体
Message = Struct.new(:type, :payload, keyword_init: true)
様々なメッセージタイプをシミュレート
messages = [
Message.new(type: :greeting, payload: “Hello!”),
Message.new(type: :error, payload: { code: 500, message: “Internal Server Error” }),
Message.new(type: :notification, payload: “System maintenance scheduled.”),
Message.new(type: :greeting, payload: “Hi!”)
]
パターンマッチングを使ってメッセージを処理
messages.each do |message|
case message
# typeが:greetingで、payloadがStringの場合
in Message[type: :greeting, payload: String => greeting_text]
puts “Greeting: #{greeting_text}”
# typeが:errorで、payloadがHashで、codeがIntegerの場合
in Message[type: :error, payload: { code: Integer => error_code, message: String => error_message }]
puts “Error (Code #{error_code}): #{error_message}”
# その他のメッセージ
in Message[type: message_type, payload: payload_data]
puts “Other message (#{message_type}): #{payload_data.inspect}”
end
end
=> Greeting: Hello!
=> Error (Code 500): Internal Server Error
=> Other message (notification): “System maintenance scheduled.”
=> Greeting: Hi!
“`
Structを使うことで、データの「形」が明確になるため、パターンマッチングの記述がより直感的になります。Struct[...]
の形式で、メンバーの名前と対応する値のパターンを指定することで、複雑な条件分岐を簡潔に記述できます。これは特に、様々な種類の構造化されたデータが混在する場合に強力なパターンです。
Struct vs OpenStruct vs Class:使い分けの基準
Rubyでデータ構造を扱う際、Struct
以外にもOpenStruct
や通常のClass
といった選択肢があります。それぞれに特徴があり、適した場面が異なります。ここでは、これらの違いを明確にし、どのように使い分けるべきかを解説します。
OpenStruct (ostruct
ライブラリ)
OpenStruct
は、実行時に動的に属性を追加・変更できるデータ構造です。Structとは異なり、事前にメンバーを定義する必要がありません。ostruct
ライブラリをrequireして使用します。
“`ruby
require ‘ostruct’
属性を事前に定義する必要がない
data = OpenStruct.new
data.name = “Frank”
data.job = “Engineer”
data.city = “Berlin”
puts data.name # => Frank
puts data.job # => Engineer
存在しない属性にアクセスすると nil を返す
puts data.country # => nil
属性の追加も自由
data.country = “Germany”
puts data.country # => Germany
Hashからの変換も容易
hash_data = { id: 101, status: “active” }
ostruct_data = OpenStruct.new(hash_data)
puts ostruct_data.id # => 101
puts ostruct_data.status # => active
“`
OpenStructの利点:
- 柔軟性: 事前に構造を定義する必要がなく、実行時に動的に属性を追加・変更できます。これは、データの構造が実行時まで分からない場合(例:任意フォーマットの外部データを読み込む場合)に便利です。
- Hashからの変換が容易: Hashをそのまま
OpenStruct.new
に渡すだけでインスタンスを作成できます。
OpenStructの欠点:
- パフォーマンス: 内部的に
Hash
を使って属性を管理しているため、Structや通常のClassに比べて属性へのアクセスが遅く、メモリ使用量も多くなる傾向があります。 - 静的なチェックが不可能: 存在しない属性にアクセスしてもエラーにならないため、タイプミスに気づきにくいです。構造が動的であるがゆえに、コードを読んだだけではどのような属性が存在し得るか分かりにくいことがあります。
- 定義されていない属性への代入: 存在しない属性に値を代入すると、新しい属性として追加されます。これは柔軟であると同時に、意図しない属性の追加を招く可能性があります。
Class
通常のClass
定義は、最も一般的で柔軟な方法です。属性(インスタンス変数とアクセサ)と振る舞い(メソッド)を自由に定義できます。継承やモジュールのインクルードなど、オブジェクト指向プログラミングの全ての機能を利用できます。
“`ruby
class ProductClass
attr_accessor :id, :name, :price, :in_stock
def initialize(id:, name:, price:, in_stock: true)
@id = id
@name = name
@price = price
@in_stock = in_stock
end
def display_price_with_currency(currency = “USD”)
“#{currency} #{‘%.2f’ % @price}”
end
def available?
@in_stock
end
end
product = ProductClass.new(id: 10, name: “Tablet”, price: 300.00)
puts product.name # => Tablet
puts product.available? # => true
puts product.display_price_with_currency(“EUR”) # => EUR 300.00
“`
Classの利点:
- 完全な制御: 属性、メソッド、初期化ロジックなど、オブジェクトの全てを完全に制御できます。
- 複雑なロジックの包含: データだけでなく、そのデータを操作する複雑なビジネスロジックをメソッドとしてクラス内に含めることができます(Structはデータ保持に特化すべき)。
- オブジェクト指向機能の活用: 継承、モジュールのインクルード、プライベートメソッドなど、オブジェクト指向の豊かな機能を利用できます。
Classの欠点:
- 定義の冗長性: 単純にデータのまとまりを表現したいだけであれば、
attr_accessor
やinitialize
メソッドを記述する必要があり、Structに比べてコード量が多くなります。
Struct
Structは、これらの間のバランスを取る存在です。
Structの利点:
- 定義の簡潔性: シンプルなデータ構造(複数の属性の集まり)を非常に少ないコード量で定義できます。
Struct.new
にメンバー名を渡すだけで、必要なアクセサメソッドと初期化メソッドを持つクラスが生成されます。 - タイプセーフ: 事前にメンバーを定義するため、存在しないメンバーにアクセスしようとすると
NoMethodError
が発生し、タイプミスに気づきやすいです。 - パフォーマンス: Cで実装された固定構造体であるため、属性へのアクセス速度やメモリ効率は
Hash
やOpenStruct
に比べて優れています(通常のClass
と同等か、ごくわずかに劣る程度)。 - 構造の明確さ: コードを読んだ人が、そのStructインスタンスがどのような属性を持つべきかをすぐに理解できます。
to_h
などの便利なメソッド: Hashや配列への変換、イテレーションなど、データ操作に便利なメソッドが標準で用意されています。
Structの欠点:
- 定義後のメンバー変更不可: 一度
Struct.new
でStructクラスを定義すると、後からそのクラスにメンバーを追加したり削除したりすることはできません(OpenStructのような動的な属性の追加はできない)。 - メソッド定義の手間: メソッドを追加したい場合は、ブロックを使うかサブクラス化する必要があります(Classでは自然にメソッドを定義できる)。ただし、Structは基本的にデータホルダーとして使うのが望ましいので、これは大きな欠点ではないかもしれません。
使い分けの基準まとめ
- 最もシンプルで固定されたデータ構造:
Struct
を使います。属性の数が少なく、メソッドを追加する必要がほとんどない場合に最適です。DTO、設定オブジェクト、一時的なレコードなど。keyword_init: true
やデフォルト値を活用するとさらに便利です。 - 構造が動的であるか、事前に分からないデータ:
OpenStruct
を検討します。ただし、パフォーマンスが問題にならないか、または動的な構造が必要なごく限られた範囲での利用に留めるのが賢明です。 - データに加えて複雑なビジネスロジックや振る舞いが必要: 通常の
Class
を使います。データとそれを操作するメソッドが密接に関連している場合に適しています。継承などオブジェクト指向機能を積極的に利用する場合もClassを使います。 - Hashで十分か?: 単純なキーと値のペアの集まりであればHashで十分です。ただし、Hashのキーを文字列でアクセスするのはタイプミスを招きやすく、またキーの名前がコード上で分かりにくいという欠点があります。構造が固定されている場合はStructの方が望ましいことが多いです。
多くの場合、Structは「軽量なClass」あるいは「型付きのHash」として位置づけられます。Classにするほどではないが、Hashよりは構造を明確にしたい、タイプセーフにしたいという場合にStructが強力な選択肢となります。
パフォーマンスに関する考察
StructがHash
やOpenStruct
に比べてパフォーマンス面で有利であると述べましたが、ここでは簡単な例でその違いを見てみましょう。
簡単なベンチマーク
以下のベンチマークは、Struct、OpenStruct、およびHashのインスタンスを多数作成し、それぞれの属性にアクセスする速度を比較するものです。
“`ruby
require ‘benchmark’
require ‘ostruct’
ITERATIONS = 100_000 # インスタンス作成回数
Structの定義
PersonStruct = Struct.new(:name, :age, :city)
Classの定義 (比較用)
class PersonClass
attr_accessor :name, :age, :city
def initialize(name, age, city); @name, @age, @city = name, age, city; end
end
元となるデータ
data = { name: “Test User”, age: 30, city: “Test City” }
puts “— Creation Benchmark (#{ITERATIONS} iterations) —”
Benchmark.bm(10) do |x|
x.report(“Struct:”) { ITERATIONS.times { PersonStruct.new(data[:name], data[:age], data[:city]) } }
x.report(“OpenStruct:”) { ITERATIONS.times { OpenStruct.new(data) } }
x.report(“Hash:”) { ITERATIONS.times { { name: data[:name], age: data[:age], city: data[:city] } } }
x.report(“Class:”) { ITERATIONS.times { PersonClass.new(data[:name], data[:age], data[:city]) } }
end
作成したインスタンス群 (アクセス速度比較用)
struct_instances = Array.new(ITERATIONS) { PersonStruct.new(data[:name], data[:age], data[:city]) }
ostruct_instances = Array.new(ITERATIONS) { OpenStruct.new(data) }
hash_instances = Array.new(ITERATIONS) { { name: data[:name], age: data[:age], city: data[:city] } }
class_instances = Array.new(ITERATIONS) { PersonClass.new(data[:name], data[:age], data[:city]) }
puts “\n— Access Benchmark (#{ITERATIONS} iterations) —”
Benchmark.bm(10) do |x|
x.report(“Struct:”) { ITERATIONS.times { struct_instances.sample.age } }
x.report(“OpenStruct:”) { ITERATIONS.times { ostruct_instances.sample.age } }
x.report(“Hash:”) { ITERATIONS.times { hash_instances.sample[:age] } }
x.report(“Class:”) { ITERATIONS.times { class_instances.sample.age } }
end
“`
実行結果例 (環境により異なります):
“`
— Creation Benchmark (100000 iterations) —
user system total real
Struct: 0.040000 0.000000 0.040000 ( 0.040820)
OpenStruct: 0.340000 0.010000 0.350000 ( 0.353690)
Hash: 0.040000 0.000000 0.040000 ( 0.042065)
Class: 0.050000 0.000000 0.050000 ( 0.051867)
— Access Benchmark (100000 iterations) —
user system total real
Struct: 0.010000 0.000000 0.010000 ( 0.008782)
OpenStruct: 0.130000 0.000000 0.130000 ( 0.125777)
Hash: 0.010000 0.000000 0.010000 ( 0.009023)
Class: 0.010000 0.000000 0.010000 ( 0.008941)
“`
この簡単なベンチマーク結果からわかること:
- インスタンス作成: StructとHash、通常のClassは同程度の速度でインスタンスを作成できます。OpenStructはそれらに比べて数倍遅いです。これは、OpenStructがインスタンス作成時に動的な処理を多く行っているためと考えられます。
- 属性アクセス: StructとHash、通常のClassは属性アクセスも非常に高速です。OpenStructはやはりそれらに比べて数倍遅い傾向があります。これは、OpenStructが内部Hashのルックアップを介して属性にアクセスするのに対し、StructやClassはより直接的な方法でインスタンス変数や構造体のメンバにアクセスするためです。
この結果は、大量のデータオブジェクトを扱ったり、属性への高速なアクセスが求められるようなシナリオ(例:データ処理パイプライン、パフォーマンスクリティカルな部分)では、StructやClassがOpenStructよりも適していることを示唆しています。Hashも高速ですが、キーがシンボルであること、ドット記法でアクセスできないこと、構造が保証されないことなど、Structとは異なる性質を持ちます。
パフォーマンスが重要なケース
- 大量のレコード処理: CSVやデータベースから大量のレコードを読み込み、それぞれをオブジェクトとして扱う場合、Structはメモリ効率が良く、高速なアクセスが可能です。
- 短期的なデータ保持: 一時的なデータ構造として頻繁に生成・破棄されるオブジェクトの場合、作成とアクセスが高速なStructが有利です。
- パフォーマンスが重視されるライブラリやフレームワーク内部: 高速なデータ処理が必要な内部実装でStructが使われることがあります。
ただし、アプリケーション全体のパフォーマンスにおいて、データ構造の選択がボトルネックになることは比較的稀です。多くの場合は、データベースアクセス、ネットワーク通信、複雑なアルゴリズムなどに時間がかかります。したがって、データ構造の選択は、パフォーマンスだけでなく、コードの可読性、保守性、開発速度といった他の要素も考慮して行うべきです。
「シンプルで固定された構造のデータを扱う」というStructの本来の目的に合致する場面であれば、パフォーマンス上の懸念はほとんどなく、むしろOpenStructよりもパフォーマンスが良いことが期待できます。
注意点とベストプラクティス
Structを効果的に使うために、いくつかの注意点とベストプラクティスがあります。
1. メンバー名の選び方
Rubyの慣習に従い、Structのメンバー名にはsnake_case
(例: first_name
, zip_code
)を使用しましょう。これは通常の変数名やメソッド名と同じルールです。CamelCase
(例: FirstName
)はクラス名やモジュール名に、@instance_variable
はインスタンス変数に、@@class_variable
はクラス変数に、$global_variable
はグローバル変数に使用するのが一般的です。Structのメンバーはインスタンス変数に対応しますが、アクセサメソッドを通じてアクセスするため、メソッド名と同様のsnake_case
が適切です。
“`ruby
良い例
Address = Struct.new(:street_address, :city, :postal_code)
避けるべき例
Address = Struct.new(:StreetAddress, :city, :PostalCode) # CamelCase は避ける
“`
2. 複雑なロジックを含めない
Structはデータの「器」として設計されています。複雑なビジネスロジックや副作用を伴う操作は、Structクラスの内部に定義するのではなく、別途サービスクラスやロジックを担うオブジェクトに分離するのがベストプラクティスです。
“`ruby
Product Struct (データホルダー)
Product = Struct.new(:name, :price, :quantity, keyword_init: true) do
# 単純な派生値やフォーマットのメソッドはOK
def total_price
price * quantity
end
end
注文処理ロジックを担うクラス (外部に分離)
class OrderProcessor
def process_order(user, products)
# … 注文処理の複雑なロジック …
total = products.sum(&:total_price)
# …
end
end
避けるべき例:Structに複雑なロジックを含める
Product = Struct.new(:name, :price, :quantity) do
def process_purchase(user)
# データベース更新、外部API呼び出しなど、Structの責務ではないロジック
end
end
“`
Structにメソッドを追加することは可能ですが、それは主にデータの派生値計算(例: total_price
)、データの簡単なフォーマット(例: full_name
)、あるいは初期化時の簡単な検証などに留めるべきです。Structのインスタンスの状態を変更するようなメソッドや、外部との連携を行うメソッドは、Structの責務から外れると考えましょう。
3. 大規模なアプリケーションでの利用(DTOとして)
大規模なアプリケーションでは、異なるモジュールやレイヤー間でデータを安全かつ明確に受け渡すためにDTOパターンがよく使われます。Structは、このようなDTOの実装に非常に適しています。
- メソッドの引数や戻り値としてStructインスタンスを使用することで、どのようなデータがやり取りされるのかを明確にする。
- APIクライアントが受け取ったJSONレスポンスをStructに変換し、アプリケーション内部ではStructインスタンスとして扱う。
- データベースアクセス層が取得したレコードをStructにマッピングし、ビジネスロジック層に渡す。
これにより、コードの各部分がデータの具体的な構造に依存しすぎず、インターフェースが明確になります。
4. Structクラスの再利用
Struct.new
は新しい無名クラスを生成して返します。このクラスは、通常定数に代入して名前を付けて再利用します。一時的にしか使わない場合は、ローカル変数に代入したり、あるいは無名クラスのまま使うことも可能ですが、繰り返し使う構造であれば定数に代入するのが一般的です。
“`ruby
よく使う構造体は定数として定義し、再利用する
Coordinate = Struct.new(:x, :y)
def calculate_distance(p1, p2)
# Coordinate クラスを使ってインスタンスを作成
point1 = Coordinate.new(p1[:x], p1[:y])
point2 = Coordinate.new(p2[:x], p2[:y])
# Struct インスタンスを操作
Math.sqrt((point2.x – point1.x)2 + (point2.y – point1.y)2)
end
無名Structを一時的に使う例 (限定的なケース)
def process_data(data_hash)
# 一時的な構造体として利用
temp_struct = Struct.new(:id, :value).new(data_hash[:id], data_hash[:value])
puts “Processing ID: #{temp_struct.id}, Value: #{temp_struct.value}”
end
“`
Structクラスを定数として定義することで、コード全体で一貫した構造を再利用でき、可読性が向上します。
5. デフォルト値とkeyword_init
の活用
Ruby 2.5以降の機能であるデフォルト値とkeyword_init: true
は、Structのインスタンス生成をより安全かつ表現豊かにします。特にメンバーが多いStructや、オプションのメンバーがあるStructでは、これらの機能を積極的に活用することをお勧めします。これにより、インスタンス作成時の引数の順序間違いを防ぎ、コードの意図が明確になります。
まとめ:Structの活用でコードをシンプルに
この記事では、RubyのStruct
について、その基本的な使い方から、ブロックによるカスタマイズ、デフォルト値やキーワード引数、サブクラス化といった詳細機能、さらには設定オブジェクトやDTOとしての応用例、そしてOpenStruct
や通常のClass
との比較、パフォーマンスに関する考察、そして利用上の注意点やベストプラクティスまで、多角的に解説しました。
Structは、シンプルで固定されたデータ構造を定義するための強力なツールです。Hashのように柔軟すぎず、Classのように大げさでもない、ちょうど良いバランスを提供します。
Structが適しているのは、主に以下のようなケースです:
- 複数の関連する値をまとめて、名前付きの属性として扱いたいとき。
- データの構造が比較的シンプルで固定されているとき。
- データ自体に複雑なビジネスロジックを含める必要がないとき(あるいはロジックを別途分離できるとき)。
- Hashのキーアクセスによるタイプミスを防ぎたいとき。
- Classを定義するほどの構造や振る舞いは不要だが、Hashよりは厳密な構造を定義したいとき。
- DTOや設定オブジェクトなど、純粋なデータホルダーが必要なとき。
- パフォーマンスが重要な部分で、OpenStructの動的な性質によるオーバーヘッドを避けたいとき。
Structを適切に活用することで、Rubyのコードはより読みやすく、保守しやすくなります。データ構造の意図が明確になり、タイプミスによるバグを減らすことができます。
ただし、複雑な状態管理が必要な場合や、データと密接に関連した複雑な振る舞いをオブジェクトに持たせたい場合は、迷わず通常のClass
を選択すべきです。また、データの構造が実行時まで全く分からないような極端に動的なケースでは、OpenStruct
が唯一の選択肢となる可能性もあります(ただし、その場合でも後処理でStructやClassに変換することを検討しましょう)。
Rubyには様々なデータ構造を扱う手段がありますが、Struct
はその中でも「シンプルさと効率性」という特定のニーズに対して非常に優れたソリューションを提供します。ぜひ日々のコーディングでStruct
を活用し、より良いコードを書くためのツールとして役立ててください。
この詳細な解説が、あなたのStructへの理解を深め、Rubyでのデータ構造の扱い方を向上させる一助となれば幸いです。
付録:主要コード例集
以下に、記事中で紹介した主要なコード例をまとめて掲載します。
“`ruby
— 基本的なStructの定義と利用 —
Structクラスの定義
User = Struct.new(:name, :age, :email)
インスタンスの作成 (位置引数)
user1 = User.new(“Alice”, 30, “[email protected]”)
属性へのアクセス (ドット記法)
puts user1.name # => Alice
属性の値の変更
user1.age = 31
puts user1.age # => 31
[] 演算子を使ったアクセス
puts user1[:email] # => [email protected]
クラス名文字列を指定 (任意)
BookStructClass = Struct.new(“Book”, :title, :author, :isbn)
puts BookStructClass.name # => Book
immutable インスタンス
frozen_user = User.new(“Frozen”, 99).freeze
frozen_user.age = 100 # => FrozenError
— ブロックを使ったカスタマイズ —
Person = Struct.new(:first_name, :last_name) do
def full_name
“#{first_name} #{last_name}”
end
# クラスメソッド
def self.create_from_string(name_str)
parts = name_str.split
new(first_name: parts[0], last_name: parts[1]) # keyword_init が前提
end
end
p = Person.new(“John”, “Doe”)
puts p.full_name # => John Doe
— デフォルト値と keyword_init: true —
Config = Struct.new(:host, :port, :timeout, keyword_init: true) do
def initialize(host: “localhost”, port: 5432, timeout: 30)
super(host: host, port: port, timeout: timeout)
end
end
デフォルト値のみ
default_config = Config.new
puts default_config.host # => localhost
puts default_config.port # => 5432
puts default_config.timeout # => 30
一部上書き
custom_config = Config.new(host: “remote.server”, timeout: 60)
puts custom_config.host # => remote.server
puts custom_config.port # => 5432
puts custom_config.timeout # => 60
Struct.new 定義時のデフォルト値 (Ruby 2.5+)
Item = Struct.new(:name, :price, :quantity, keyword_init: true) do
def initialize(name:, price: 0.0, quantity: 0)
raise ArgumentError, “Price must be non-negative” if price < 0
super(name: name, price: price, quantity: quantity)
end
end
item = Item.new(name: “Widget”)
puts item.price # => 0.0
puts item.quantity # => 0
— Structクラスのサブクラス化 —
BaseProduct = Struct.new(:id, :name, keyword_init: true)
class DigitalProduct < BaseProduct
attr_accessor :file_format, :download_url
def initialize(id:, name:, file_format:, download_url:)
super(id: id, name: name)
@file_format = file_format
@download_url = download_url
end
def download
“Downloading #{name} from #{download_url}”
end
end
digital_item = DigitalProduct.new(id: 1, name: “Ebook”, file_format: “PDF”, download_url: “http://example.com/ebook.pdf”)
puts digital_item.name # => Ebook
puts digital_item.download_url # => http://example.com/ebook.pdf
puts digital_item.download # => Downloading Ebook from http://example.com/ebook.pdf
— Structインスタンスの比較 —
Coordinate = Struct.new(:x, :y)
c1 = Coordinate.new(1, 2)
c2 = Coordinate.new(1, 2)
c3 = Coordinate.new(3, 4)
puts c1 == c2 # => true
puts c1 == c3 # => false
puts c1 == { x: 1, y: 2 } # => true (Hashとの比較)
— Structインスタンスのイテレーションと情報取得 —
RGBColor = Struct.new(:red, :green, :blue)
color = RGBColor.new(255, 128, 0)
color.each_pair do |key, value|
puts “#{key}: #{value}”
end
=> red: 255
=> green: 128
=> blue: 0
puts color.members # => [:red, :green, :blue]
puts color.values # => [255, 128, 0]
puts color.to_h # => {:red=>255, :green=>128, :blue=>0}
puts color.size # => 3
— パターンマッチングとの組み合わせ (Ruby 2.7+) —
Point = Struct.new(:x, :y, keyword_init: true)
data = Point.new(x: 10, y: 20)
case data
in Point[x: val_x, y: val_y]
puts “Point coordinates: (#{val_x}, #{val_y})” # => Point coordinates: (10, 20)
end
Message = Struct.new(:type, :data, keyword_init: true)
msg = Message.new(type: :user_created, data: { user_id: 100, name: “Alice” })
case msg
in Message[type: :user_created, data: { user_id: Integer => uid, name: String => username }]
puts “New user #{username} with ID #{uid} created.” # => New user Alice with ID 100 created.
in Message[type: :error, data: { code: Integer => code }]
puts “Error occurred, code: #{code}”
else
puts “Unknown message type”
end
— OpenStruct vs Class vs Struct (例) —
require ‘ostruct’
OpenStruct (動的)
os = OpenStruct.new
os.a = 1
os.b = 2
puts os.a # => 1
Struct (静的、シンプル)
MyStruct = Struct.new(:a, :b)
ms = MyStruct.new(1, 2)
puts ms.a # => 1
ms.c = 3 # => NoMethodError
Class (静的、完全制御)
class MyClass
attr_accessor :a, :b
def initialize(a, b); @a, @b = a, b; end
end
mc = MyClass.new(1, 2)
puts mc.a # => 1
mc.c = 3 # これ自体はエラーにならないが accessor はない
“`
これで、RubyのStruct
に関する約5000語の詳細な記事は完了です。