CrackedRuby logo

CrackedRuby

Instance Variables and Methods

Overview

Instance variables store object state and exist independently for each instance of a class. Ruby prefixes instance variables with @ and creates them dynamically upon first assignment. Instance methods define object behavior and operate within the context of a specific object instance, accessing that object's instance variables and other methods.

Ruby creates instance variables when first assigned, regardless of whether they appear in initialize or other methods. The variable exists only within that specific object instance and remains accessible to all instance methods of that object.

class User
  def initialize(name)
    @name = name
    @created_at = Time.now
  end

  def profile
    "#{@name} joined #{@created_at.strftime('%Y-%m-%d')}"
  end
end

user = User.new("Alice")
user.profile
# => "Alice joined 2025-08-26"

Instance methods execute within the object's context, accessing instance variables and other methods through implicit self. Ruby resolves method calls by searching the object's class, then included modules, then the superclass chain.

class Account
  def initialize(balance)
    @balance = balance
  end

  def deposit(amount)
    @balance += amount
    log_transaction("deposit", amount)
  end

  private

  def log_transaction(type, amount)
    puts "#{type.capitalize}: #{amount}"
  end
end

account = Account.new(100)
account.deposit(50)
# Deposit: 50

Ruby distinguishes between instance variables (object state) and instance methods (object behavior). Instance variables remain private to the object, while instance methods can have different visibility levels through private, protected, and public modifiers.

Basic Usage

Instance variables store data specific to each object instance. Ruby creates them upon first assignment and initializes unassigned instance variables to nil. Instance variables persist throughout the object's lifetime and remain accessible to all instance methods.

class Product
  def initialize(name, price)
    @name = name
    @price = price
    @discount = 0
  end

  def apply_discount(percentage)
    @discount = percentage
    @discounted_price = @price * (1 - percentage / 100.0)
  end

  def final_price
    @discounted_price || @price
  end

  def description
    base = "#{@name}: $#{@price}"
    @discount > 0 ? "#{base} (#{@discount}% off: $#{final_price})" : base
  end
end

product = Product.new("Laptop", 1000)
puts product.description
# => "Laptop: $1000"

product.apply_discount(15)
puts product.description
# => "Laptop: $1000 (15% off: $850.0)"

Instance methods define object behavior and access instance variables through the @ prefix. Methods can modify instance variables, call other instance methods, and return values based on the object's current state.

class Temperature
  def initialize(celsius)
    @celsius = celsius
  end

  def celsius
    @celsius
  end

  def fahrenheit
    (@celsius * 9.0 / 5.0) + 32
  end

  def kelvin
    @celsius + 273.15
  end

  def update(new_celsius)
    @celsius = new_celsius
    log_update
  end

  private

  def log_update
    puts "Temperature updated to #{@celsius}°C"
  end
end

temp = Temperature.new(25)
puts temp.fahrenheit  # => 77.0
puts temp.kelvin      # => 298.15

temp.update(30)
# Temperature updated to 30°C

Accessor methods provide controlled access to instance variables. Ruby generates getter and setter methods through attr_reader, attr_writer, and attr_accessor declarations, which create methods that read or modify instance variables.

class Book
  attr_reader :title, :author
  attr_accessor :available

  def initialize(title, author)
    @title = title
    @author = author
    @available = true
    @checkout_count = 0
  end

  def checkout
    return false unless @available

    @available = false
    @checkout_count += 1
    @last_checkout = Time.now
    true
  end

  def return_book
    @available = true
    @last_return = Time.now
  end

  def checkout_history
    "Checked out #{@checkout_count} times"
  end
end

book = Book.new("1984", "George Orwell")
puts book.title       # => "1984"
puts book.available   # => true

book.checkout
puts book.available   # => false
book.return_book
puts book.available   # => true

Ruby allows dynamic instance variable assignment within any instance method. Variables created this way become available to all other instance methods of that object, but unassigned variables return nil rather than raising errors.

class DynamicData
  def store_value(key, value)
    instance_variable_set("@#{key}", value)
  end

  def retrieve_value(key)
    instance_variable_get("@#{key}")
  end

  def has_value?(key)
    instance_variable_defined?("@#{key}")
  end

  def all_variables
    instance_variables.map { |var| var.to_s.delete('@') }
  end
end

data = DynamicData.new
data.store_value('name', 'Ruby')
data.store_value('version', 3.0)

puts data.retrieve_value('name')     # => "Ruby"
puts data.has_value?('version')      # => true
puts data.all_variables              # => ["name", "version"]

Advanced Usage

Instance variables support complex object composition and state management patterns. Objects can contain other objects as instance variables, creating nested data structures and enabling sophisticated object relationships.

class Address
  attr_reader :street, :city, :zipcode

  def initialize(street, city, zipcode)
    @street = street
    @city = city
    @zipcode = zipcode
  end

  def full_address
    "#{@street}, #{@city} #{@zipcode}"
  end
end

class Person
  attr_reader :name

  def initialize(name)
    @name = name
    @addresses = {}
    @phone_numbers = []
    @metadata = {}
  end

  def add_address(type, address)
    @addresses[type] = address
    @metadata[:last_address_update] = Time.now
  end

  def add_phone(number, type = :mobile)
    @phone_numbers << { number: number, type: type, added: Time.now }
  end

  def primary_address
    @addresses[:home] || @addresses[:work] || @addresses.values.first
  end

  def contact_summary
    addr = primary_address&.full_address || "No address"
    phones = @phone_numbers.map { |p| "#{p[:number]} (#{p[:type]})" }.join(", ")
    "#{@name}\n#{addr}\n#{phones.empty? ? "No phone" : phones}"
  end

  def recent_activity
    updates = []
    updates << "Address: #{@metadata[:last_address_update]}" if @metadata[:last_address_update]
    updates << "Phone: #{@phone_numbers.last[:added]}" if @phone_numbers.any?
    updates.empty? ? "No recent updates" : updates.join(", ")
  end
end

person = Person.new("Jane Doe")
person.add_address(:home, Address.new("123 Oak St", "Portland", "97201"))
person.add_address(:work, Address.new("456 Pine Ave", "Portland", "97202"))
person.add_phone("555-0123", :mobile)
person.add_phone("555-0456", :work)

puts person.contact_summary
# Jane Doe
# 123 Oak St, Portland 97201
# 555-0123 (mobile), 555-0456 (work)

Instance methods can implement complex business logic by coordinating multiple instance variables and calling other methods. This enables sophisticated object behavior while maintaining encapsulation.

class BankAccount
  attr_reader :account_number, :balance

  def initialize(account_number, initial_balance = 0)
    @account_number = account_number
    @balance = initial_balance
    @transaction_history = []
    @daily_withdrawal_limit = 500
    @daily_withdrawn = 0
    @last_withdrawal_date = nil
    @frozen = false
  end

  def deposit(amount)
    return false if @frozen || amount <= 0

    @balance += amount
    record_transaction(:deposit, amount, @balance)
    reset_daily_limits_if_new_day
    true
  end

  def withdraw(amount)
    return false if @frozen || amount <= 0 || insufficient_funds?(amount) || exceeds_daily_limit?(amount)

    @balance -= amount
    update_daily_withdrawal(amount)
    record_transaction(:withdrawal, amount, @balance)
    check_low_balance_warning
    true
  end

  def transfer_to(other_account, amount)
    return false unless withdraw(amount)

    if other_account.deposit(amount)
      record_transaction(:transfer_out, amount, @balance, other_account.account_number)
      true
    else
      # Rollback the withdrawal
      @balance += amount
      @transaction_history.pop
      false
    end
  end

  def freeze_account
    @frozen = true
    record_transaction(:freeze, 0, @balance)
  end

  def account_status
    status = @frozen ? "FROZEN" : "ACTIVE"
    daily_remaining = @daily_withdrawal_limit - @daily_withdrawn

    {
      status: status,
      balance: @balance,
      daily_withdrawal_remaining: daily_remaining,
      transaction_count: @transaction_history.size,
      last_transaction: @transaction_history.last&.dig(:timestamp)
    }
  end

  private

  def insufficient_funds?(amount)
    @balance < amount
  end

  def exceeds_daily_limit?(amount)
    reset_daily_limits_if_new_day
    (@daily_withdrawn + amount) > @daily_withdrawal_limit
  end

  def update_daily_withdrawal(amount)
    reset_daily_limits_if_new_day
    @daily_withdrawn += amount
    @last_withdrawal_date = Date.today
  end

  def reset_daily_limits_if_new_day
    if @last_withdrawal_date != Date.today
      @daily_withdrawn = 0
      @last_withdrawal_date = nil
    end
  end

  def record_transaction(type, amount, balance_after, reference = nil)
    @transaction_history << {
      type: type,
      amount: amount,
      balance_after: balance_after,
      timestamp: Time.now,
      reference: reference
    }
  end

  def check_low_balance_warning
    puts "WARNING: Low balance ($#{@balance})" if @balance < 100
  end
end

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

account1.withdraw(200)
account1.transfer_to(account2, 150)
puts account1.account_status
# {:status=>"ACTIVE", :balance=>650, :daily_withdrawal_remaining=>150, :transaction_count=>3, :last_transaction=>...}

Metaprogramming with instance variables enables dynamic behavior creation. Objects can define methods at runtime, modify their own structure, and respond to method calls that don't exist through method_missing.

class ConfigurableObject
  def initialize
    @config = {}
    @computed_values = {}
    @value_dependencies = {}
  end

  def configure(key, value = nil, &block)
    if block_given?
      @config[key] = block
      define_singleton_method(key) { compute_value(key) }
    else
      @config[key] = value
      define_singleton_method(key) { @config[key] }
    end

    define_singleton_method("#{key}=") do |new_value|
      @config[key] = new_value
      invalidate_dependents(key)
    end
  end

  def depends_on(key, *dependencies)
    @value_dependencies[key] = dependencies
  end

  def method_missing(method_name, *args)
    key = method_name.to_s.gsub(/[=?]$/, '').to_sym

    if method_name.to_s.end_with?('?')
      @config.key?(key)
    elsif method_name.to_s.end_with?('=')
      configure(key, args.first)
    elsif @config.key?(key)
      compute_value(key)
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    key = method_name.to_s.gsub(/[=?]$/, '').to_sym
    @config.key?(key) || super
  end

  private

  def compute_value(key)
    return @computed_values[key] if @computed_values.key?(key)

    if @config[key].is_a?(Proc)
      @computed_values[key] = instance_eval(&@config[key])
    else
      @computed_values[key] = @config[key]
    end
  end

  def invalidate_dependents(changed_key)
    @value_dependencies.each do |dependent_key, dependencies|
      if dependencies.include?(changed_key)
        @computed_values.delete(dependent_key)
        invalidate_dependents(dependent_key)
      end
    end
    @computed_values.delete(changed_key)
  end
end

obj = ConfigurableObject.new
obj.configure(:base_price, 100)
obj.configure(:tax_rate, 0.08)
obj.configure(:total_price) { base_price * (1 + tax_rate) }

obj.depends_on(:total_price, :base_price, :tax_rate)

puts obj.total_price    # => 108.0
obj.base_price = 200
puts obj.total_price    # => 216.0
puts obj.tax_rate?      # => true

Common Pitfalls

Instance variable initialization timing creates common confusion. Ruby creates instance variables only when assigned, not when declared, and uninitialized instance variables return nil instead of raising errors.

class ProblematicCounter
  def initialize
    # @count is not initialized here
  end

  def increment
    @count += 1  # Error: nil can't be coerced into Integer
  end

  def current_count
    @count  # Returns nil, not 0
  end
end

# Fix: Initialize all instance variables
class CorrectCounter
  def initialize
    @count = 0
  end

  def increment
    @count += 1
  end

  def current_count
    @count
  end
end

counter = ProblematicCounter.new
puts counter.current_count  # => nil
# counter.increment         # TypeError

fixed_counter = CorrectCounter.new
puts fixed_counter.current_count  # => 0
fixed_counter.increment
puts fixed_counter.current_count  # => 1

Instance variables lack automatic accessor generation, requiring explicit method definitions for external access. Forgetting to create accessors while expecting external access leads to NoMethodError.

class HiddenData
  def initialize(value)
    @secret_value = value
  end

  # No accessor methods defined
end

data = HiddenData.new(42)
# puts data.secret_value  # NoMethodError: undefined method

# Fix: Add accessor methods as needed
class AccessibleData
  def initialize(value)
    @secret_value = value
  end

  def secret_value
    @secret_value
  end

  def secret_value=(new_value)
    @secret_value = new_value
  end
end

accessible = AccessibleData.new(42)
puts accessible.secret_value        # => 42
accessible.secret_value = 24
puts accessible.secret_value        # => 24

Method visibility affects instance variable access patterns. Private methods can access instance variables, but calling private methods externally raises NoMethodError, even when the method exists.

class ConfusingVisibility
  def initialize(data)
    @data = data
  end

  def process
    validate_data
    transform_data
  end

  private

  def validate_data
    raise "Invalid data" if @data.nil?
  end

  def transform_data
    @data = @data.upcase
  end
end

processor = ConfusingVisibility.new("hello")
processor.process
# processor.validate_data  # NoMethodError: private method called

# Access instance variables through public methods only
class ClearVisibility
  def initialize(data)
    @data = data
  end

  def process
    validate_data
    transform_data
    @data
  end

  def valid?
    !@data.nil? && !@data.empty?
  end

  private

  def validate_data
    raise "Invalid data" unless valid?
  end

  def transform_data
    @data = @data.upcase
  end
end

Instance variable scope differs from local variable scope, creating confusion when variables shadow each other or when expecting automatic initialization behaviors.

class ScopeConfusion
  def initialize(name)
    @name = name
  end

  def problematic_method(name)
    # Local variable 'name' shadows parameter access
    name = @name.upcase
    puts name  # Prints the uppercased @name
    puts @name # Still the original @name

    # Later code expects 'name' parameter but gets modified local variable
    process_name(name)  # Gets uppercased version, not original parameter
  end

  def process_name(input_name)
    puts "Processing: #{input_name}"
  end

  def clear_method(input_name)
    # Use different variable names to avoid confusion
    processed_name = @name.upcase
    puts "Instance variable: #{@name}"
    puts "Processed: #{processed_name}"
    puts "Parameter: #{input_name}"

    process_name(input_name)  # Clear which value gets passed
  end
end

confusing = ScopeConfusion.new("ruby")
confusing.problematic_method("parameter")
# RUBY
# ruby
# Processing: RUBY

confusing.clear_method("parameter")
# Instance variable: ruby
# Processed: RUBY
# Parameter: parameter
# Processing: parameter

Reference

Instance Variable Operations

Operation Syntax Returns Description
instance_variable_get(symbol) :"@name" Value or nil Retrieves instance variable value
instance_variable_set(symbol, value) :"@name", any value Set value Assigns instance variable value
instance_variable_defined?(symbol) :"@name" Boolean Checks if instance variable exists
instance_variables None Array of symbols Lists all instance variables
remove_instance_variable(symbol) :"@name" Removed value Removes instance variable

Method Definition and Access

Method Type Syntax Visibility Access Pattern
public def method_name External and internal object.method_name
protected protected; def method_name Same class/subclass object.method_name within class
private private; def method_name Internal only method_name (no receiver)
attr_reader attr_reader :name Public getter object.name
attr_writer attr_writer :name Public setter object.name = value
attr_accessor attr_accessor :name Public getter/setter Both patterns above

Common Instance Variable Patterns

Pattern Example Use Case
Lazy initialization @value ||= expensive_computation Computed values
Boolean flags @active = true State tracking
Collections @items = [] Aggregated data
Nested objects @address = Address.new Composition
Caching @cached_result = nil Performance optimization

Method Definition Modifiers

Modifier Effect Example
alias_method :new, :old Creates method alias Access original method
undef_method :method Removes method definition Prevent method calls
define_method(:name) { block } Dynamic method creation Runtime method definition
method_defined?(:name) Checks method existence Conditional method calls
private_method_defined?(:name) Checks private method Visibility verification

Initialization Patterns

Pattern Code Example Behavior
Required parameters def initialize(name) Must provide value
Optional parameters def initialize(name = "Default") Uses default if omitted
Keyword arguments def initialize(name:, age: 18) Named parameters
Variable arguments def initialize(*args) Accepts any number
Hash options def initialize(**opts) Named option hash

Error Conditions

Error Type Cause Solution
TypeError nil + value operations Initialize variables properly
NoMethodError Missing accessor methods Add attr_reader/attr_accessor
NoMethodError Private method external call Use public interface
ArgumentError Wrong parameter count Check method signature
NameError Uninitialized constant/variable Define before use

Visibility Rules

Context Public Methods Protected Methods Private Methods
External objects ✓ Accessible ✗ NoMethodError ✗ NoMethodError
Same class instance ✓ Accessible ✓ Accessible ✗ NoMethodError with receiver
Subclass instance ✓ Accessible ✓ Accessible ✗ NoMethodError with receiver
Internal method calls ✓ Accessible ✓ Accessible ✓ Accessible without receiver

Performance Characteristics

Operation Time Complexity Notes
Instance variable access O(1) Direct memory reference
Method call O(1) Method lookup cached
instance_variables O(n) Scans all instance variables
Dynamic method definition O(1) Adds to method table
Method visibility change O(1) Updates method metadata