"The Ruby Programming Language"的单例示例导致"未定义的方法"或未初始化的常量



通过"Ruby编程语言"并尝试实现第7章"对象创建和初始化"部分末尾的Singleton示例

本章使用"点"类,该类逐渐扩展为包括类实例变量和类方法,以允许记录"点统计" - 创建的点数及其平均值。

能够使其正常工作:无需单例即可工作

当我重构为具有一个 PointStats Singleton 类和一个Point类时,我发现当类是第一个类时PointStats我得到一个undefined method x错误,或者当Point类是第一个时出现uninitialized constant PointStats错误。

在OSX上运行1.8,尽管也在另一台通过rvm运行2.0的机器上尝试过 - 感兴趣的行也有相同的结果。

我一定缺少一些关于如何在 Ruby 中让多个类协同工作的非常基本的东西。

我做错了什么?据我所知,我完全遵循了书中的例子,但每个类似乎都要求首先定义另一个类。

首先使用 PointStats 类重构代码,如文本所示:

#!/usr/bin/ruby -w
require 'singleton'
class PointStats
    include Singleton
    def initialize
        @n, @totalX, @totalY = 0, 0.0, 0.0
    end
    def record(point)
        @n += 1
        @totalX += point.x
        @totalY += point.y
    end
    def report
        puts "#{@n} -- points"
        puts "#{@totalX/@n} -- Average x"
        puts "#{@totalY/@n} -- Average y"       
    end
end
class Point
    def initialize(x,y)
        @x,@y = x,y
        PointStats.instance.record(self)
    end
    ORIGIN = Point.new(0,0)
    UNIT_X = Point.new(1,0)
    UNIT_Y = Point.new(0,1)
    include Enumerable
    include Comparable
    attr_reader :x, :y
    def to_s
        "(#{@x},#{@y})"
    end
    def +(other)
        Point.new(@x + other.x, @y + other.y)
    rescue
        raise TypeError,
            "Point like argument expected"
    end
    def -@
        Point.new(-@x, -@y)
    end
    def *(scalar)
        Point.new(@x*scalar, @y*scalar)
    end
    def coerce(other)
        [self, other]
    end
    def [](index)
        case index
        when 0, -2: @x
        when 1, -1: @y
        when :x, "x": @x
        when :y, "y": @y
        else nil
        end
    end
    def each
        yield @x
        yield @y
    end
    def ==(o)
        if 0.is_a? Point
            @x==o.x && @y==o.y
        else
            false
        end
    end
    def eql?(o)
        if o.instance_of? Point
            @x.eql?(o.x) && @y.eql?(o.y)
        else
            false
        end
    end
    def hash
        code = 17
        code = 37*code + @x.hash
        code = 37*code + @y.hash
        code
    end
    def <=>(other)
        return nil unless other.instance_of? Point
        @x**2 + @y**2 <=> other.x**2 + other.y**2
    end
    def self.sum(*points)
        x = y = 0
        points.each { |p| x+=p.x; y+=p.y }
        Point.new(x,y)
    end
end

编辑:谢谢@cozyconemotel - 使用了两种公共方法,让new以更预期的方式运行,并为其他用途添加unrecorded

    class Point
        attr_reader :x, :y
        def initialize(x,y)
            @x,@y = x,y
            PointStats.instance.record(self)
        end
        def self.unrecorded(x,y)
            instance = new(x,y)
        end
        ORIGIN = Point.unrecorded(0,0)
        UNIT_X = Point.unrecorded(1,0)
        UNIT_Y = Point.unrecorded(0,1)
        # and the rest follows ...

正如@guitarman所指出的,因为调用PointStats.instance.record需要Point#x and Point#y,你必须在initialize之前移动到attr_reader :x, :y才能使其工作。

class Point
  attr_reader :x, :y
  def initialize(x,y)
    @x,@y = x,y
    PointStats.instance.record(self)
  end
  #(rest of the class def..)
end

不过,我在您的代码中发现了另一个问题。在Point的定义中,您将创建 3 个实例:

ORIGIN = Point.new(0,0)
UNIT_X = Point.new(1,0)
UNIT_Y = Point.new(0,1)

问题是这些也会触发对PointStats.instance.record的调用,因此会发生这种情况

> p1 = Point.new(10,10) 
=> #<Point:0x007ff7935d6f78 @x=10, @y=10>
> p2 = Point.new(20,20) 
=> #<Point:0x007ff7935f4b68 @x=20, @y=20>
> PointStats.instance.report
5 -- points
6.2 -- Average x
6.2 -- Average y
=> nil

您应该做的一件事是将new设为私有并创建一个调用 record 的工厂方法。喜欢这个:

class Point
  attr_reader :x, :y
  private_class_method :new
  def initialize(x,y)
    @x,@y = x,y
  end
  def self.generate(x,y)
    instance = new(x,y)
    PointStats.instance.record(instance)
    instance
  end
  ORIGIN = new(0,0)
  UNIT_X = new(1,0)
  UNIT_Y = new(0,1)
  #(here goes the rest..)
end

如果您只想同时使用new(创建新实例而不记录(和generate(创建记录的新实例(,则不必将new设为私有。

这是因为您将 Point 类的初始化方法中的新点传递到 PointStats 类的record(self)方法,您可以在点通过 attr_reader :x, :y 定义 getter 方法之前访问point.xattr_reader :x, :y移动到 class Point 内的第一行,因为您的脚本将从上到下解释。

最新更新