💎

【Ruby 41日目】オブジェクト指向 - クラス変数とインスタンス変数

に公開

はじめに

Rubyのクラス変数とインスタンス変数について、Ruby 3.4の仕様に基づいて詳しく解説します。

この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。

基本概念

クラス変数とインスタンス変数は、オブジェクト指向プログラミングにおける重要な概念です:

  • インスタンス変数(@) - 各インスタンスが個別に持つ変数
  • クラス変数(@@) - クラスとそのすべてのインスタンスで共有される変数
  • スコープの違い - インスタンス変数はインスタンス内、クラス変数はクラス全体
  • 継承の影響 - クラス変数は継承ツリー全体で共有される

この2つの変数を適切に使い分けることで、効果的なオブジェクト設計ができます。

基本的な使い方

インスタンス変数の基本

class Person
  def initialize(name, age)
    @name = name  # インスタンス変数
    @age = age    # インスタンス変数
  end

  def introduce
    "I'm #{@name}, #{@age} years old"
  end
end

person1 = Person.new("Alice", 25)
person2 = Person.new("Bob", 30)

puts person1.introduce  #=> I'm Alice, 25 years old
puts person2.introduce  #=> I'm Bob, 30 years old
# 各インスタンスが独自の値を持つ

クラス変数の基本

class Counter
  @@count = 0  # クラス変数

  def initialize
    @@count += 1
  end

  def self.total_count
    @@count
  end
end

puts Counter.total_count  #=> 0

counter1 = Counter.new
counter2 = Counter.new
counter3 = Counter.new

puts Counter.total_count  #=> 3
# すべてのインスタンスで共有される

インスタンス変数とクラス変数の組み合わせ

class Student
  @@total_students = 0  # クラス変数

  def initialize(name, grade)
    @name = name    # インスタンス変数
    @grade = grade  # インスタンス変数
    @@total_students += 1
  end

  def info
    "#{@name}: Grade #{@grade}"
  end

  def self.total
    @@total_students
  end
end

student1 = Student.new("Alice", 3)
student2 = Student.new("Bob", 2)

puts student1.info      #=> Alice: Grade 3
puts student2.info      #=> Bob: Grade 2
puts Student.total      #=> 2

継承とクラス変数

class Animal
  @@count = 0

  def initialize
    @@count += 1
  end

  def self.count
    @@count
  end
end

class Dog < Animal
end

class Cat < Animal
end

dog1 = Dog.new
dog2 = Dog.new
cat1 = Cat.new

# クラス変数は継承ツリー全体で共有される
puts Animal.count  #=> 3
puts Dog.count     #=> 3
puts Cat.count     #=> 3

attr_accessorとインスタンス変数

class Product
  attr_accessor :name, :price  # getter/setterを自動生成

  def initialize(name, price)
    @name = name
    @price = price
  end

  def discount(rate)
    @price = (@price * (1 - rate)).round(2)
  end
end

product = Product.new("Laptop", 1000.0)
puts product.name   #=> Laptop
puts product.price  #=> 1000.0

product.price = 900.0
puts product.price  #=> 900.0

product.discount(0.1)
puts product.price  #=> 810.0

よくあるユースケース

ケース1: インスタンスカウンターの実装

生成されたインスタンスの数を追跡します。

class Connection
  @@connection_count = 0
  @@connections = []

  def initialize(host, port)
    @host = host
    @port = port
    @connected_at = Time.now
    @@connection_count += 1
    @@connections << self
  end

  def info
    "#{@host}:#{@port} (connected at #{@connected_at})"
  end

  def self.total_connections
    @@connection_count
  end

  def self.active_connections
    @@connections.size
  end

  def self.list_all
    @@connections.map(&:info)
  end
end

conn1 = Connection.new("localhost", 3000)
conn2 = Connection.new("example.com", 443)
conn3 = Connection.new("api.example.com", 8080)

puts Connection.total_connections  #=> 3
puts Connection.active_connections #=> 3

Connection.list_all.each { |info| puts info }
#=> localhost:3000 (connected at ...)
#   example.com:443 (connected at ...)
#   api.example.com:8080 (connected at ...)

ケース2: デフォルト設定の管理

クラスレベルの設定とインスタンスレベルの設定を組み合わせます。

class Logger
  @@default_level = :info
  @@default_format = "%{time} [%{level}] %{message}"

  def initialize(name, level: nil, format: nil)
    @name = name
    @level = level || @@default_level
    @format = format || @@default_format
  end

  def log(message, level: @level)
    return if severity(level) < severity(@level)

    formatted = @format
      .gsub("%{time}", Time.now.to_s)
      .gsub("%{level}", level.to_s.upcase)
      .gsub("%{message}", message)

    puts "[#{@name}] #{formatted}"
  end

  def self.default_level=(level)
    @@default_level = level
  end

  def self.default_format=(format)
    @@default_format = format
  end

  private

  def severity(level)
    { debug: 0, info: 1, warn: 2, error: 3 }[level] || 1
  end
end

# デフォルト設定を変更
Logger.default_level = :warn

logger1 = Logger.new("App")
logger2 = Logger.new("DB", level: :debug)

logger1.log("This is info", level: :info)   # 出力されない(warn未満)
logger1.log("This is warning", level: :warn)
#=> [App] ... [WARN] This is warning

logger2.log("Debug message", level: :debug)
#=> [DB] ... [DEBUG] Debug message

ケース3: オブジェクトの状態管理

各インスタンスが独自の状態を持ちます。

class BankAccount
  @@total_accounts = 0
  @@total_balance = 0

  attr_reader :account_number, :balance

  def initialize(account_number, initial_balance = 0)
    @account_number = account_number
    @balance = initial_balance
    @transactions = []

    @@total_accounts += 1
    @@total_balance += initial_balance
  end

  def deposit(amount)
    return false if amount <= 0

    @balance += amount
    @@total_balance += amount
    @transactions << { type: :deposit, amount: amount, timestamp: Time.now }
    true
  end

  def withdraw(amount)
    return false if amount <= 0 || amount > @balance

    @balance -= amount
    @@total_balance -= amount
    @transactions << { type: :withdraw, amount: amount, timestamp: Time.now }
    true
  end

  def transaction_history
    @transactions
  end

  def self.total_accounts
    @@total_accounts
  end

  def self.total_balance
    @@total_balance
  end
end

account1 = BankAccount.new("001", 1000)
account2 = BankAccount.new("002", 500)

account1.deposit(200)
account1.withdraw(100)
account2.deposit(300)

puts "Account 1 balance: $#{account1.balance}"  #=> Account 1 balance: $1100
puts "Account 2 balance: $#{account2.balance}"  #=> Account 2 balance: $800

puts "Total accounts: #{BankAccount.total_accounts}"    #=> Total accounts: 2
puts "Total balance: $#{BankAccount.total_balance}"     #=> Total balance: $1900

puts "\nAccount 1 transactions:"
account1.transaction_history.each do |tx|
  puts "  #{tx[:type]}: $#{tx[:amount]}"
end
#=> deposit: $200
#   withdraw: $100

ケース4: ファクトリーパターンの実装

オブジェクト生成を管理します。

class User
  @@users = {}
  @@next_id = 1

  attr_reader :id, :username, :email

  def initialize(username, email)
    @id = @@next_id
    @username = username
    @email = email
    @created_at = Time.now

    @@next_id += 1
    @@users[@id] = self
  end

  def update_email(new_email)
    @email = new_email
  end

  def self.find(id)
    @@users[id]
  end

  def self.find_by_username(username)
    @@users.values.find { |user| user.username == username }
  end

  def self.all
    @@users.values
  end

  def self.count
    @@users.size
  end

  def info
    "User ##{@id}: #{@username} (#{@email})"
  end
end

user1 = User.new("alice", "alice@example.com")
user2 = User.new("bob", "bob@example.com")
user3 = User.new("charlie", "charlie@example.com")

puts User.count  #=> 3

found = User.find(2)
puts found.info  #=> User #2: bob (bob@example.com)

user = User.find_by_username("alice")
puts user.info   #=> User #1: alice (alice@example.com)

puts "\nAll users:"
User.all.each { |u| puts u.info }
#=> User #1: alice (alice@example.com)
#   User #2: bob (bob@example.com)
#   User #3: charlie (charlie@example.com)

ケース5: キャッシュの実装

クラスレベルでデータをキャッシュします。

class APIClient
  @@cache = {}
  @@cache_ttl = 300  # 5分

  def initialize(api_key)
    @api_key = api_key
    @request_count = 0
  end

  def fetch(endpoint)
    @request_count += 1

    cache_key = "#{@api_key}:#{endpoint}"
    cached = @@cache[cache_key]

    if cached && Time.now - cached[:timestamp] < @@cache_ttl
      puts "[Cache hit] #{endpoint}"
      return cached[:data]
    end

    puts "[API request] #{endpoint}"
    data = perform_request(endpoint)

    @@cache[cache_key] = {
      data: data,
      timestamp: Time.now
    }

    data
  end

  def request_count
    @request_count
  end

  def self.cache_size
    @@cache.size
  end

  def self.clear_cache
    @@cache.clear
  end

  def self.set_ttl(seconds)
    @@cache_ttl = seconds
  end

  private

  def perform_request(endpoint)
    # 実際のAPI呼び出しをシミュレート
    "Data from #{endpoint}"
  end
end

client1 = APIClient.new("key1")
client2 = APIClient.new("key2")

# 初回はAPI呼び出し
data1 = client1.fetch("/users")
#=> [API request] /users

# 2回目はキャッシュから取得
data2 = client1.fetch("/users")
#=> [Cache hit] /users

# 別のクライアントだが、キャッシュは共有される
data3 = client2.fetch("/users")
#=> [Cache hit] /users

puts "Client 1 requests: #{client1.request_count}"  #=> Client 1 requests: 2
puts "Client 2 requests: #{client2.request_count}"  #=> Client 2 requests: 1
puts "Cache size: #{APIClient.cache_size}"          #=> Cache size: 1

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

注意点

  1. クラス変数の継承問題
class Parent
  @@shared = "parent"

  def self.shared
    @@shared
  end

  def self.shared=(value)
    @@shared = value
  end
end

class Child < Parent
end

# BAD: 継承ツリー全体で共有される
puts Parent.shared  #=> parent
puts Child.shared   #=> parent

Child.shared = "child"
puts Parent.shared  #=> child(親クラスも変更される!)
puts Child.shared   #=> child

# GOOD: クラスインスタンス変数を使う(次の記事で詳しく解説)
class BetterParent
  @shared = "parent"

  class << self
    attr_accessor :shared
  end
end

class BetterChild < BetterParent
  @shared = "child"
end

puts BetterParent.shared  #=> parent
puts BetterChild.shared   #=> child(独立している)
  1. インスタンス変数の未初期化
class Example
  def set_value(value)
    @value = value
  end

  def get_value
    @value
  end
end

# BAD: 未初期化のインスタンス変数はnil
obj = Example.new
puts obj.get_value.inspect  #=> nil(初期化されていない)

# GOOD: コンストラクタで初期化
class BetterExample
  def initialize(value = 0)
    @value = value
  end

  def value
    @value
  end
end

obj = BetterExample.new
puts obj.value  #=> 0
  1. クラス変数のスレッドセーフティ
class ThreadUnsafe
  @@counter = 0

  def self.increment
    # BAD: スレッドセーフではない
    @@counter += 1
  end

  def self.counter
    @@counter
  end
end

# GOOD: Mutexで保護
class ThreadSafe
  @@counter = 0
  @@mutex = Mutex.new

  def self.increment
    @@mutex.synchronize do
      @@counter += 1
    end
  end

  def self.counter
    @@mutex.synchronize do
      @@counter
  end
  end
end

ベストプラクティス

  1. インスタンス変数は常に初期化する
# GOOD: すべてのインスタンス変数をコンストラクタで初期化
class Person
  def initialize(name, age = 0, email = nil)
    @name = name
    @age = age
    @email = email
    @verified = false
  end

  attr_reader :name, :age, :email

  def verified?
    @verified
  end
end

person = Person.new("Alice", 25)
puts person.name      #=> Alice
puts person.age       #=> 25
puts person.email.inspect  #=> nil(明示的にnil)
puts person.verified? #=> false
  1. クラス変数の使用は慎重に
# GOOD: クラス変数は本当に必要な場合のみ使用
class RequestCounter
  @@total_requests = 0
  @@mutex = Mutex.new

  def initialize(endpoint)
    @endpoint = endpoint
    @requests = 0
  end

  def make_request
    @requests += 1

    @@mutex.synchronize do
      @@total_requests += 1
    end

    "Request to #{@endpoint}"
  end

  def request_count
    @requests
  end

  def self.total_requests
    @@mutex.synchronize do
      @@total_requests
    end
  end
end

counter1 = RequestCounter.new("/api/users")
counter2 = RequestCounter.new("/api/posts")

counter1.make_request
counter1.make_request
counter2.make_request

puts counter1.request_count           #=> 2
puts counter2.request_count           #=> 1
puts RequestCounter.total_requests    #=> 3
  1. attr_accessorを活用する
# GOOD: attr_accessor/reader/writerで可読性向上
class Product
  attr_reader :id, :created_at
  attr_accessor :name, :price, :description

  def initialize(id, name, price)
    @id = id
    @name = name
    @price = price
    @description = ""
    @created_at = Time.now
  end

  def discount(rate)
    @price = (@price * (1 - rate)).round(2)
  end
end

product = Product.new(1, "Laptop", 1000.0)
product.description = "High-performance laptop"

puts product.name         #=> Laptop
puts product.description  #=> High-performance laptop
# product.id = 2  # NoMethodError(readonlyなので変更不可)

Ruby 3.4での改善点

  • Prismパーサーによる最適化 - インスタンス変数アクセスの解析が高速化
  • YJITの最適化 - インスタンス変数の読み書きがさらに高速化
  • メモリ管理の改善 - インスタンス変数のメモリ配置が最適化
  • Object Shapesの導入 - インスタンス変数の構造が似たオブジェクトでメモリ効率が向上
# Ruby 3.4では、同じ構造のオブジェクトが効率的に管理される
class Point
  def initialize(x, y)
    @x = x
    @y = y
  end

  attr_reader :x, :y
end

# これらのオブジェクトは同じ"shape"を持つため効率的
points = 1000.times.map { |i| Point.new(i, i * 2) }

まとめ

この記事では、クラス変数とインスタンス変数について以下の内容を学びました:

  • 基本概念と重要性 - インスタンス変数(@)とクラス変数(@@)の違い
  • 基本的な使い方と構文 - 定義方法、スコープ、継承における振る舞い
  • 実践的なユースケース - カウンター、設定管理、状態管理、ファクトリー、キャッシュ
  • 注意点とベストプラクティス - 継承問題、初期化、スレッドセーフティ、attr_accessorの活用

インスタンス変数とクラス変数を適切に使い分けることで、効果的なオブジェクト指向設計が可能になります。

参考資料

Discussion