💎

【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}

注意点とベストプラクティス

注意点

  1. 初期化を忘れない
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"]
  1. 継承時の動作を理解する
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(独立している)
  1. アクセサーの定義
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

ベストプラクティス

  1. クラス変数より優先する
# 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
# 各クラスが独立した設定を持つ
  1. 明示的な初期化
# 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"]
  1. 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