【问题标题】:Ruby "CONSTANTS" seem to be INVISIBLY ALTERABLE?Ruby“CONSTANTS”似乎是不可见的改变?
【发布时间】:2014-12-19 16:19:44
【问题描述】:

我知道 Ruby 中的“常量”按惯例称为常量,但实际上是可变的。但是,我的印象是,当它们“变异”时会发出警告:

class Z2
 M = [0,1]
end

Z2::M # => [0, 1]
Z2::M = [0,3]
(irb):warning: already initialized constant Z2::M
(irb):warning: previous definition of M was here

但我发现并非总是如此:

a = Z2::M
a[1] = 2

Z2::M # => [0,2] and no warning

这是“警告”系统中的一个漏洞吗?我推断一个常量的赋值会重复它,但我猜这也不正确,因为常量和变量似乎指向同一个对象?这是否意味着所有所谓的“常量”都需要被冻结,以防止它们在没有警告的情况下被更改?

【问题讨论】:

  • 如果您想防止更改该值,您可以尝试M = [0,1].freezeZ2::M[1] = 2 => RuntimeError: can't modify frozen array.
  • freeze 并非在所有情况下都有效,因为它只会冻结引用,但不会冻结被引用的对象。示例:M=['hello'].freeze; M[0] << '!' - 没有错误,字符串已修改。
  • 当然,字符串没有被冻结:)
  • 这越来越可怕了。有没有办法确保常量的内容永远不会改变?我认为冻结意味着它没有被改变,但由于一切都是参考,冻结它甚至可能不会阻止参考改变?
  • 几乎没有任何东西可以保证在 Ruby 中是不可变的。如果你足够努力,你可以进入任何事情并改变它。 :)

标签: ruby class-constants


【解决方案1】:

TL;DR

没有猴子修补内核#warn(请参阅https://stackoverflow.com/a/662436/1301972)来引发异常,您将无法阻止重新分配给常量本身。在惯用的 Ruby 代码中,这通常不是一个实用的问题,其中一个期望能够执行诸如重新打开类之类的操作,即使类名也是常量。

Ruby 常量实际上不是不可变的,您不能freeze 变量。但是,您可以在尝试修改常量引用的冻结对象的内容时引发异常。

使用纯 Ruby 深度冻结对象

冻结数组很容易:

CONSTANT_ONE = %w[one two three].freeze

但是存储在这个 Array 中的字符串实际上是对 String 对象的引用。因此,虽然您不能修改此 Array,但您仍然可以(例如)修改索引 0 引用的 String 对象。要解决此问题,您不仅需要冻结 Array,还需要冻结它所包含的对象。例如:

CONSTANT = %w[one two three].map(&:freeze).freeze

CONSTANT[2] << 'four'
# RuntimeError: can't modify frozen String

CONSTANT << 'five'
# RuntimeError: can't modify frozen Array

使用 Gem 递归地冻结对象

由于冻结递归引用可能有点笨拙,很高兴知道there's a gem for that。您可以使用 ice_nine 来深度冻结大多数对象:

require 'ice_nine'
require 'ice_nine/core_ext/object'

OTHER_CONST = %w[a b c]
OTHER_CONST.deep_freeze

OTHER_CONST << 'd'
# RuntimeError: can't modify frozen Array

OTHER_CONST[2] = 'z'
# RuntimeError: can't modify frozen Array

使用 Ruby 常量的更好方法

另一个要考虑的选项是在将常量的值分配给另一个变量(例如类初始化程序中的实例变量)时调用Object#dup,以确保不会意外改变常量的引用。例如:

class Foo
  CONSTANT = 'foo'
  attr_accessor :variable

  def initialize
    @variable = CONSTANT.dup
  end
end

foo = Foo.new
foo.variable << 'bar'
#=> "foobar"

Foo::CONSTANT
#=> "foo"

【讨论】:

  • 彻底的编译——我很感激。我对这个问题的所有贡献都更加了解。
【解决方案2】:

没有间隙,因为您没有更改常数。事实上,Ruby 常量只是带有额外警告的变量。

常量,就像每个变量一样,只是一个指向内存中对象的指针。当您执行M = [0,3] 时,您正在创建一个新数组并将常量重新指向这个新对象,这会触发警告。

但是,当您运行 M[0] = 1 时,您只是在修改引用的对象,而不会更改常量,因为它仍然指向同一个对象。

这里要意识到的重要一点是,Ruby 中的所有类都只是内存中的对象,用常量引用,所以当你这样做时:

class Z2
end

它等价于(如果 Z2 没有定义或者没有指向一个类对象):

Z2 = Class.new

自然类是一个非常动态的对象,因为我们不断向它添加方法等等 - 我们绝对不希望这会触发任何警告。

【讨论】:

  • 我明白其中的一些。可以说我得到的唯一警告是当我更改对象 ID(又名指针)而不是当我更改对象(常量)内容时?似乎警告是关于“初始化”而不是“改变价值”。这是一个正确的说法吗?
  • 这不是我的措辞,但它具有相似的含义。由于常量指向对象,并且方法直接发送给方法,而不涉及常量,因此对象不知道它应该是常量。另外,如果你做m = M,你首先期望它们都指向同一个对象,因此它们都必须是常量,这绝对没有意义。
【解决方案3】:

如果您使用Z2::M[1] = 2,您也不会收到消息。我相信缺少警告是因为您正在更改 Array 本身而不是参考 Z2::M
如果M 是一个整数,例如:

class Z2
  M = 1
end

a = Z2::M
a = 2
a # => 2
Z2::M # => 1

要从常量修改Array 而不修改原始值,您可以这样做:

class Z2
  M = [0,1]
end

a = Z2::M.dup
a[0] = 1
a # => [1,1]
Z2::M # => [0,1]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-09-28
    • 2018-06-04
    • 1970-01-01
    • 1970-01-01
    • 2018-11-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多