💎
【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
注意点とベストプラクティス
注意点
- クラス変数の継承問題
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(独立している)
- インスタンス変数の未初期化
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
- クラス変数のスレッドセーフティ
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
ベストプラクティス
- インスタンス変数は常に初期化する
# 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
- クラス変数の使用は慎重に
# 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
- 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