【问题标题】:Can't modify frozen Fixnum on Ruby 2.0无法在 Ruby 2.0 上修改冻结的 Fixnum
【发布时间】:2013-04-05 17:46:39
【问题描述】:

我有以下代码:

require 'prime'
class Numeric
  #... math helpers

  def divisors
    return [self] if self == 1
    @divisors ||= prime_division.map do |n,p|
      (0..p).map { |i| n**i }
    end.inject([1]) do |a,f|
      a.product(f)
    end.map { |f| f.flatten.reduce(:*) } - [self]
  end

  def divisors_sum
     @divisors_sum ||= divisors.reduce(:+)
  end

   #... more methods that evaluate code and 'caches' and assigns (||=) to instance variables
end

输出错误:

> 4.divisors
/home/cygnus/Projects/project-euler/stuff/numbers.rb:24:in `divisors_sum': can't modify frozen Fixnum (RuntimeError)

当我将缓存删除到实例变量 @divisors@divisors_sum... 等时,错误消失了。这只发生在 ruby​​ 2.0 上。在 1.9.3 上运行它没有问题。发生了什么事?

【问题讨论】:

  • 已确认代码在 1.9.3 上可以正常工作,也确认在 2.0.0 中对我不起作用。不过,将实例变量添加到 Fixnum 是非常不寻常的。
  • 试图加快进程。有些方法可能需要几秒钟的时间,并且为大量数字一遍又一遍地计算会导致执行时间增加。
  • prime 是宝石吗?第 24 行是哪个语句?
  • 我认为冻结的 fixnums 会一直存在,来自 ruby​​-core 中这样的 cmets:atdot.net/sp/view/4qjkcm/readonly 所以我建议简单地在 Fixnum 之外缓存数据。
  • 这真的很奇怪,我不是要修改 Fixnum 实例本身(这显然是做不到的),而是保存它的变量。

标签: ruby ruby-2.0


【解决方案1】:

@divisors 是 Fixnum 实例上的实例变量,因此您正在尝试更改它。你可能不应该这样做。

这个怎么样?

module Divisors
  def self.for(number)
    @divisors ||= { }
    @divisors[number] ||= begin
      case (number)
      when 1
        [ number ]
      else
        prime_division.map do |n,p|
          (0..p).map { |i| n**i }
        end.inject([1]) do |a,f|
          a.product(f)
        end.map { |f| f.flatten.reduce(:*) } - [ number ]
      end
    end
  end

  def self.sum(number)
     @divisors_sum ||= { }
     @divisors_sum[number] ||= divisors(number).reduce(:+)
  end
end

class Numeric
  #... math helpers

  def divisors
    Divisors.for(self)
  end

  def divisors_sum
     Divisors.sum(self)
  end
end

这意味着 Numeric 中的方法不会修改任何实例,缓存存储在其他地方。

【讨论】:

  • Another approach 可以保持类中的逻辑被扩展,并将实例数据移动到外部模块中。
  • 这种方法有创建更多实例方法的副作用。修改核心类时,最好始终尽可能减小占用空间。
  • 我同意保持占用空间小很重要,尤其是在您编写库代码时。你的方法和我的方法都在Numeric 中定义了divisorsdivisors_sum。唯一的附加方法是mixin fcache
【解决方案2】:

除了@tadman 的回答之外,在1.9.3 而不是2.0.0 中工作的原因是因为2 年前决定冻结Fixnums(和Bignums),正如this 和@987654322 所证明的那样@。

【讨论】:

    【解决方案3】:

    正如其他人指出的那样,ruby 核心已决定 Fixnums 和 Bignums 现在已冻结,因此您无法在这些类的对象中设置实例变量。

    一种解决方法是制作一个外部模块,该模块保留由这些冻结对象的值索引的哈希缓存,并使用这些哈希的元素而不是实例变量:

    require 'prime'
    
    module FrozenCacher
      def FrozenCacher.fcache
        @frozen_cache ||= {}
      end
    
      def fcache
        FrozenCacher.fcache[self] ||= {}
      end
    end
    
    class Numeric
      include FrozenCacher
      #... math helpers
    
      def divisors
        return [self] if self == 1
        fcache[:divisors] ||= prime_division.map do |n,p|
          (0..p).map { |i| n**i }
        end.inject([1]) do |a,f|
          a.product(f)
        end.map { |f| f.flatten.reduce(:*) } - [self]
      end
    
      def divisors_sum
        fcache[:divisors_sum] ||= divisors.reduce(:+)
      end
    
       #... more methods that evaluate code and 'caches' and assigns (||=) to instance variables
    end
    
    puts 4.divisors.inspect           # => [1, 2]
    puts FrozenCacher.fcache.inspect  # => {4=>{:divisors=>[1, 2]}}
    puts 10.divisors.inspect          # => [1, 5, 2]
    puts FrozenCacher.fcache.inspect  # => {4=>{:divisors=>[1, 2]}, 10=>{:divisors=>[1, 5, 2]}}
    

    【讨论】:

    • 不错,我喜欢将逻辑保留在 Numeric 中的想法。
    • 聪明的解决方案。不过,我会担心垃圾收集。我认为循环依赖意味着int永远不会被GC(缓存中的项目也不会)。
    • @mahemoff 简单缓存的本质是不断增长。如果这是一个问题,请用 LRU 或 TTL 或其他过期缓存替换示例中的简单 ruby​​ 哈希。或者使用WeakRef 并在查找中处理可能的异常。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-24
    • 1970-01-01
    • 2022-10-23
    • 1970-01-01
    • 1970-01-01
    • 2021-02-19
    • 1970-01-01
    相关资源
    最近更新 更多