💎
【Ruby 42日目】オブジェクト指向 - クラスインスタンス変数
はじめに
Rubyのクラスインスタンス変数について、Ruby 3.4の仕様に基づいて詳しく解説します。
この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。
基本概念
クラスインスタンス変数は、クラス自体がオブジェクトとして持つインスタンス変数です:
- クラスのインスタンス変数 - クラスオブジェクト自身が持つ@変数
- クラス変数との違い - 継承ツリーで共有されず、各クラスが独立した値を持つ
- 通常のインスタンス変数との違い - クラスのインスタンスではなく、クラス自体に属する
- アクセス方法 - クラスメソッド経由でアクセス
クラスインスタンス変数は、クラス変数の継承問題を回避する強力な手段です。
基本的な使い方
クラスインスタンス変数の基本
class Counter
@count = 0 # クラスインスタンス変数
def self.increment
@count += 1
end
def self.count
@count
end
end
puts Counter.count #=> 0
Counter.increment
Counter.increment
Counter.increment
puts Counter.count #=> 3
クラス変数との違い
# クラス変数(@@)の場合
class ParentWithClassVar
@@count = 0
def self.increment
@@count += 1
end
def self.count
@@count
end
end
class ChildWithClassVar < ParentWithClassVar
end
ParentWithClassVar.increment
ChildWithClassVar.increment
# 継承ツリー全体で共有される
puts ParentWithClassVar.count #=> 2
puts ChildWithClassVar.count #=> 2
# クラスインスタンス変数(@)の場合
class ParentWithClassInstanceVar
@count = 0
def self.increment
@count ||= 0
@count += 1
end
def self.count
@count || 0
end
end
class ChildWithClassInstanceVar < ParentWithClassInstanceVar
@count = 0
end
ParentWithClassInstanceVar.increment
ChildWithClassInstanceVar.increment
# 各クラスが独立した値を持つ
puts ParentWithClassInstanceVar.count #=> 1
puts ChildWithClassInstanceVar.count #=> 1
attr_accessorでのアクセス
class Configuration
@settings = {}
class << self
attr_accessor :settings
end
end
Configuration.settings = { host: "localhost", port: 3000 }
puts Configuration.settings.inspect
#=> {:host=>"localhost", :port=>3000}
継承とクラスインスタンス変数
class Animal
@species_count = 0
def self.increment_count
@species_count ||= 0
@species_count += 1
end
def self.species_count
@species_count || 0
end
end
class Dog < Animal
@species_count = 0
end
class Cat < Animal
@species_count = 0
end
Animal.increment_count
Dog.increment_count
Dog.increment_count
Cat.increment_count
# 各クラスが独立したカウンタを持つ
puts Animal.species_count #=> 1
puts Dog.species_count #=> 2
puts Cat.species_count #=> 1
initialize時の設定
class Model
@attributes = []
def self.attribute(name)
@attributes ||= []
@attributes << name
# getter
define_method(name) do
instance_variable_get("@#{name}")
end
# setter
define_method("#{name}=") do |value|
instance_variable_set("@#{name}", value)
end
end
def self.attributes
@attributes || []
end
end
class User < Model
attribute :name
attribute :email
end
class Product < Model
attribute :title
attribute :price
end
puts User.attributes.inspect #=> [:name, :email]
puts Product.attributes.inspect #=> [:title, :price]
user = User.new
user.name = "Alice"
user.email = "alice@example.com"
puts user.name #=> Alice
puts user.email #=> alice@example.com
よくあるユースケース
ケース1: 設定の管理
各クラスが独自の設定を持ちます。
class BaseService
@timeout = 30
@retry_count = 3
class << self
attr_accessor :timeout, :retry_count
end
def self.configure
yield self
end
def initialize
@timeout = self.class.timeout
@retry_count = self.class.retry_count
end
def execute
"Executing with timeout: #{@timeout}s, retries: #{@retry_count}"
end
end
class FastService < BaseService
configure do |config|
config.timeout = 10
config.retry_count = 1
end
end
class SlowService < BaseService
configure do |config|
config.timeout = 60
config.retry_count = 5
end
end
puts BaseService.timeout #=> 30
puts FastService.timeout #=> 10
puts SlowService.timeout #=> 60
fast = FastService.new
puts fast.execute
#=> Executing with timeout: 10s, retries: 1
slow = SlowService.new
puts slow.execute
#=> Executing with timeout: 60s, retries: 5
ケース2: レジストリパターン
各クラスがサブクラスを登録・管理します。
class Plugin
@plugins = []
def self.register(plugin)
@plugins ||= []
@plugins << plugin
end
def self.plugins
@plugins || []
end
def self.inherited(subclass)
register(subclass)
end
def self.plugin_name
name.split("::").last
end
def self.find_by_name(name)
plugins.find { |p| p.plugin_name == name }
end
end
class AuthPlugin < Plugin
def self.execute
"Auth plugin executed"
end
end
class LoggingPlugin < Plugin
def self.execute
"Logging plugin executed"
end
end
class CachePlugin < Plugin
def self.execute
"Cache plugin executed"
end
end
puts "Registered plugins:"
Plugin.plugins.each do |plugin|
puts " - #{plugin.plugin_name}"
end
#=> Registered plugins:
# - AuthPlugin
# - LoggingPlugin
# - CachePlugin
auth = Plugin.find_by_name("AuthPlugin")
puts auth.execute #=> Auth plugin executed
ケース3: 独立したカウンター
各クラスが独自のカウンターを持ちます。
class BaseModel
@instance_count = 0
@instances = []
def self.instance_count
@instance_count || 0
end
def self.instances
@instances || []
end
def initialize
# サブクラスのクラスインスタンス変数に直接アクセス
self.class.instance_variable_set(:@instance_count,
(self.class.instance_variable_get(:@instance_count) || 0) + 1)
instances = self.class.instance_variable_get(:@instances) || []
instances << self
self.class.instance_variable_set(:@instances, instances)
end
def self.reset_count
@instance_count = 0
@instances = []
end
end
class User < BaseModel
attr_accessor :name
def initialize(name)
@name = name
super()
end
end
class Product < BaseModel
attr_accessor :title
def initialize(title)
@title = title
super()
end
end
user1 = User.new("Alice")
user2 = User.new("Bob")
product1 = Product.new("Laptop")
product2 = Product.new("Mouse")
product3 = Product.new("Keyboard")
puts "Users created: #{User.instance_count}" #=> Users created: 2
puts "Products created: #{Product.instance_count}" #=> Products created: 3
puts "Base models: #{BaseModel.instance_count}" #=> Base models: 0
puts "\nAll users:"
User.instances.each { |u| puts " - #{u.name}" }
#=> - Alice
# - Bob
puts "\nAll products:"
Product.instances.each { |p| puts " - #{p.title}" }
#=> - Laptop
# - Mouse
# - Keyboard
ケース4: DSLの実装
クラスレベルでDSLを構築します。
class Validator
@validations = []
class << self
attr_reader :validations
end
def self.validates(field, **options)
@validations ||= []
@validations << { field: field, options: options }
end
def self.inherited(subclass)
# 継承時に親のvalidationsをコピーしない(独立させる)
subclass.instance_variable_set(:@validations, [])
end
def initialize(data)
@data = data
@errors = []
end
def valid?
@errors = []
self.class.validations.each do |validation|
field = validation[:field]
value = @data[field]
if validation[:options][:presence] && (value.nil? || value.empty?)
@errors << "#{field} is required"
end
if validation[:options][:min] && value.length < validation[:options][:min]
@errors << "#{field} is too short"
end
if validation[:options][:format] && !value.match?(validation[:options][:format])
@errors << "#{field} format is invalid"
end
end
@errors.empty?
end
def errors
@errors
end
end
class UserValidator < Validator
validates :name, presence: true, min: 3
validates :email, presence: true, format: /@/
end
class ProductValidator < Validator
validates :title, presence: true, min: 5
validates :price, presence: true
end
user_data = { name: "Al", email: "invalid" }
user_validator = UserValidator.new(user_data)
unless user_validator.valid?
puts "User validation errors:"
user_validator.errors.each { |error| puts " - #{error}" }
end
#=> User validation errors:
# - name is too short
# - email format is invalid
product_data = { title: "Laptop", price: "1000" }
product_validator = ProductValidator.new(product_data)
if product_validator.valid?
puts "Product is valid"
end
#=> Product is valid
ケース5: シングルトンのようなパターン
クラスレベルで状態を管理します。
class Cache
@store = {}
@hits = 0
@misses = 0
class << self
def set(key, value, ttl: nil)
@store ||= {}
@store[key] = {
value: value,
expires_at: ttl ? Time.now + ttl : nil
}
end
def get(key)
@store ||= {}
@hits ||= 0
@misses ||= 0
entry = @store[key]
if entry.nil?
@misses += 1
return nil
end
if entry[:expires_at] && Time.now > entry[:expires_at]
@store.delete(key)
@misses += 1
return nil
end
@hits += 1
entry[:value]
end
def clear
@store = {}
end
def stats
{
size: @store&.size || 0,
hits: @hits || 0,
misses: @misses || 0,
hit_rate: (@hits || 0).to_f / ((@hits || 0) + (@misses || 0))
}
end
end
end
class UserCache < Cache
end
class ProductCache < Cache
end
# 各キャッシュは独立している
Cache.set("app_config", { version: "1.0" })
UserCache.set("user:1", { name: "Alice" })
ProductCache.set("product:1", { title: "Laptop" })
puts Cache.get("app_config").inspect #=> {:version=>"1.0"}
puts UserCache.get("user:1").inspect #=> {:name=>"Alice"}
puts ProductCache.get("product:1").inspect #=> {:title=>"Laptop"}
# 存在しないキーへのアクセス
UserCache.get("user:999")
UserCache.get("user:1")
puts "\nCache stats:"
puts " Main: #{Cache.stats}"
puts " User: #{UserCache.stats}"
puts " Product: #{ProductCache.stats}"
#=> Cache stats:
# Main: {:size=>1, :hits=>1, :misses=>0, :hit_rate=>1.0}
# User: {:size=>1, :hits=>1, :misses=>1, :hit_rate=>0.5}
# Product: {:size=>1, :hits=>1, :misses=>0, :hit_rate=>1.0}
注意点とベストプラクティス
注意点
- 初期化を忘れない
class Example
# BAD: 初期化を忘れると各クラスでnilになる
def self.add_item(item)
@items << item # @itemsがnilの場合エラー
end
end
# GOOD: 初期化を確実に行う
class BetterExample
@items = []
def self.add_item(item)
@items ||= []
@items << item
end
def self.items
@items || []
end
end
BetterExample.add_item("item1")
puts BetterExample.items.inspect #=> ["item1"]
- 継承時の動作を理解する
class Parent
@config = { timeout: 30 }
class << self
attr_accessor :config
end
end
class Child < Parent
end
# BAD: 親のクラスインスタンス変数を共有しようとする
puts Child.config.inspect #=> nil(継承されない)
# GOOD: 各クラスで明示的に初期化
class BetterChild < Parent
@config = { timeout: 10 }
end
puts BetterChild.config.inspect #=> {:timeout=>10}
# または、inherited フックで初期化
class SmartParent
@config = { timeout: 30 }
class << self
attr_accessor :config
end
def self.inherited(subclass)
subclass.instance_variable_set(:@config, @config.dup)
end
end
class SmartChild < SmartParent
end
puts SmartChild.config.inspect #=> {:timeout=>30}(コピーされた)
SmartChild.config[:timeout] = 10
puts SmartParent.config[:timeout] #=> 30(独立している)
- アクセサーの定義
class Example
@value = 100
# BAD: アクセサーがない
# puts Example.value # NoMethodError
end
# GOOD: アクセサーを定義
class BetterExample
@value = 100
class << self
attr_accessor :value
end
end
puts BetterExample.value #=> 100
# または、個別にメソッド定義
class AnotherExample
@value = 100
def self.value
@value
end
def self.value=(new_value)
@value = new_value
end
end
puts AnotherExample.value #=> 100
ベストプラクティス
- クラス変数より優先する
# GOOD: クラスインスタンス変数を使う
class Service
@config = { timeout: 30 }
class << self
attr_accessor :config
end
end
class FastService < Service
@config = { timeout: 10 }
end
puts Service.config[:timeout] #=> 30
puts FastService.config[:timeout] #=> 10
# 各クラスが独立した設定を持つ
- 明示的な初期化
# GOOD: クラス定義時に初期化
class Counter
@count = 0
@items = []
class << self
def increment
@count += 1
end
def add_item(item)
@items << item
end
def count
@count
end
def items
@items.dup # 外部からの変更を防ぐ
end
end
end
Counter.increment
Counter.add_item("test")
puts Counter.count #=> 1
puts Counter.items.inspect #=> ["test"]
- inheritedフックの活用
# GOOD: 継承時に適切に初期化
class BaseModel
@fields = []
class << self
attr_reader :fields
def field(name, type)
@fields << { name: name, type: type }
end
end
def self.inherited(subclass)
# 親のfieldsをコピーせず、新しい配列を作成
subclass.instance_variable_set(:@fields, [])
end
end
class User < BaseModel
field :name, :string
field :age, :integer
end
class Product < BaseModel
field :title, :string
field :price, :decimal
end
puts "User fields:"
User.fields.each { |f| puts " #{f[:name]}: #{f[:type]}" }
#=> name: string
# age: integer
puts "\nProduct fields:"
Product.fields.each { |f| puts " #{f[:name]}: #{f[:type]}" }
#=> title: string
# price: decimal
Ruby 3.4での改善点
- Prismパーサーによる最適化 - クラスインスタンス変数の解析が高速化
- YJITの最適化 - クラスメソッド呼び出しとクラスインスタンス変数アクセスが高速化
- メモリ効率の改善 - クラスオブジェクトのメモリ管理が最適化
- デバッグ情報の改善 - クラスインスタンス変数の可視化が向上
# Ruby 3.4では、クラスインスタンス変数の使用がより効率的に
class OptimizedService
@cache = {}
@counter = 0
class << self
def process(key)
@counter += 1
@cache[key] ||= expensive_operation(key)
end
def stats
{ cache_size: @cache.size, operations: @counter }
end
private
def expensive_operation(key)
"Processed: #{key}"
end
end
end
result = OptimizedService.process("data1")
puts result #=> Processed: data1
puts OptimizedService.stats
#=> {:cache_size=>1, :operations=>1}
まとめ
この記事では、クラスインスタンス変数について以下の内容を学びました:
- 基本概念と重要性 - クラスオブジェクト自身が持つインスタンス変数
- クラス変数との違い - 継承ツリーで共有されず、各クラスが独立
- 基本的な使い方と構文 - 定義方法、アクセサー、継承時の動作
- 実践的なユースケース - 設定管理、レジストリ、カウンター、DSL、キャッシュ
- 注意点とベストプラクティス - 初期化、継承時の動作、クラス変数との使い分け
クラスインスタンス変数を使うことで、クラス変数の継承問題を回避し、より安全で予測可能なコードを書くことができます。
Discussion