In Ruby, how to count the number of instances created (including subclasses)?

  Kiến thức lập trình

The following class Point contains a class instance variable @count that counts the number of instances created. It seems to work fine:

class Point
    @count = 0
    class << self
        attr_accessor :count
    end

    def initialize(position)
        @position = position
        self.class.count += 1
    end
end

However, if I add a subclass ColoredPoint and I want @count to include the number of these created too:

class ColoredPoint < Point
    def initialize(position, color)
        super(position)
        @color = color
    end
end

This gives an error when calling ColoredPoint.new(1, 'red'):

undefined method '+' for nil (NoMethodError)
    self.class.count += 1

How can I make this work? Do I have to use a class variable (@@count), even though I’ve read that “you should avoid them at all costs”?

6

Use this way. Remember Point objects are counted separately from ColoredPoint objects.

class Point
  @count = 0

  class << self
    attr_accessor :count
  end

  def initialize(position)
    @position = position
    self.class.count += 1
  end
end

class ColoredPoint < Point
  @count = 0

  class << self
    attr_accessor :count
  end

  def initialize(position, color)
    super(position)
    @color = color
  end
end

Point.new([0, 0])
Point.new([0, 1])
ColoredPoint.new([1, 1], 'red')
ColoredPoint.new([2, 2], 'blue')

puts Point.count # Output: 2
puts ColoredPoint.count # Output: 2

4

Another option would be to use a separate class or module to track the count.

For instance something like this might work for you:

module ClassCounter 

  def self.extended(base)
    base.prepend(InstanceMethods)
    base.extend(ClassMethods)
    _counter[base.counter_name] = 0
  end

  def self.counter
    _counter.dup
  end 

  module ClassMethods 
    def count
      ClassCounter.counter[counter_name]
    end 
    def counter_name 
      self.name 
    end
    def counter_names
      @counter_names ||= ancestors.map(&:name) & ClassCounter.counter.keys
    end
    def inherited(subclass)
      ClassCounter._counter[subclass.counter_name] = 0
    end
  end

  module InstanceMethods 
    def initialize(*)
      self.class.counter_names.each do |name|
        ClassCounter._counter[name] += 1
      end 
      super(*)
    end
  end

  def self._counter
    @_counter ||= {} 
  end
end 

This will count subclasses separately while tracking all of the subclass instances as instances of the parental class as well:

class Point
    extend ClassCounter 
end
class ColoredPoint < Point; end
class OpaquePoint < ColoredPoint; end 

Result:

Point.new
Point.count 
#=> 1 
ColoredPoint.new
ColoredPoint.count
#=> 1 
Point.count
#=> 2
OpaquePoint.new
OpaquePoint.count 
#=> 1
ColoredPoint.count
#=> 2
Point.count
#=> 3 

You could approach it like this:

class Point
  class << self
    def count
      @count ||= 0
    end

    def increment_count
      superclass.increment_count if superclass <= Point
      @count = count + 1
    end
  end

  def initialize(position)
    self.class.increment_count
    @position = position
  end
end

The line superclass.increment_count if superclass <= Point is for subclasses of Point. It will call increment_count on the superclass if the superclass is Point or one of its subclasses. This way, the count gets propagated up the inheritance chain:

Point.new(123)

Point.count        #=> 1
ColoredPoint.count #=> 0

ColoredPoint.new(456, "red")

Point.count        #=> 2 
ColoredPoint.count #=> 1

If you want to handle things a little more fundamentally, you could override the .new class method and do the counting there. This is a bit unusual but if you want to have the class keep track of its instances, it might be a good fit.

With this approach the explicit call inside initialize isn’t needed anymore. Likewise, it makes super calls from initialize in other subclasses optional. You can also declare the counting method protected which limits the caller to subclasses of Point (the classes, not their instances):

class Point
  class << self
    def count
      @count ||= 0
    end

    def new(...)
      increment_count
      super
    end

    protected

    def increment_count
      superclass.increment_count if superclass <= Point
      @count = count + 1
    end
  end

  def initialize(position)
    @position = position # <- no counting logic here
  end
end

class ColoredPoint < Point
  def initialize(position, color)
    @position = position # <- no call to super needed (although recommended)
    @color = color
  end
end

1

Try to define class variable in the Point:

@@count = 0

And increment it in the Point constructor:

@@count += 1

Full example:

class Point
    @@count = 0
    
    def self.number
        @@count
    end

    def initialize(position)
        @position = position
        @@count += 1
    end
end


class ColoredPoint < Point
    def initialize(position, color)
        super(position)
        @color = color
    end
end

Point.new(1)
ColoredPoint.new(1, 'red')
ColoredPoint.new(1, 'blue')

# ptint 3
puts Point.number

Theme wordpress giá rẻ Theme wordpress giá rẻ Thiết kế website

LEAVE A COMMENT