Ruby “CONSTANTS” seem to be INVISIBLY ALTERABLE?

ぐ巨炮叔叔 提交于 2019-12-12 15:14:18

问题


I understand that "constants" in Ruby are by convention called constants but are in fact mutable. However I was under the impression that when they were "mutated" that there was a warning:

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

However I found this is not the case all the time:

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

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

Is this a gap in the "warning" system? I am inferring that assignment of a constant would duplicate it, but I guess that is not true either as it appears that constants and variables point to the same object? Does this mean that all so-called "constants" need to be frozen in order to prevent them from being changed without warning?


回答1:


TL;DR

Short of monkey-patching Kernel#warn (see https://stackoverflow.com/a/662436/1301972) to raise an exception, you won't be able to prevent reassignment to the constant itself. This is generally not a pragmatic concern in idiomatic Ruby code where one expects to be able to do things like reopen classes, even though class names are also constants.

A Ruby constant isn't actually immutable, and you can't freeze a variable. However, you can get an exception to be raised when something attempts to modify the contents of a frozen object referenced by the constant.

Freezing Objects Deeply with Plain Ruby

Freezing an Array is easy:

CONSTANT_ONE = %w[one two three].freeze

but the strings stored in this Array are really references to String objects. So, while you can't modify this Array, you can still (for example) modify the String object referenced by index 0. To solve this problem, you need to freeze not just the Array, but the objects it holds, too. For example:

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

Freezing Objects Recursively with a Gem

Since freezing recursive references can be a bit unwieldy, it's good to know there's a gem for that. You can use ice_nine to deep-freeze most objects:

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

A Better Way to Use Ruby Constants

Another option to consider is calling Object#dup when assigning the value of a constant to another variable, such as instance variables in your class initializers, in order to ensure you don't mutate your constant's references by accident. For example:

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:


There is no gap, as you are not altering a constant. And the fact is that Ruby constants are just variables with extra warnings.

Constant, just as every variable, is merely a pointer to the object in memory. When you doM = [0,3] you are creating a new array and re-pointing constant to this new object, which triggers a warning.

However, when you run M[0] = 1 you are just modifying referenced object, but you do not change the constant, as it still points to the same object.

Important thing to realize here is that all classes in Ruby are just objects in memory, referenced with constants, so when you do:

class Z2
end

it is equivalent to (if Z2 is not defined or is not pointing onto a class object already):

Z2 = Class.new

Naturally class is a very dynamic object, as we keep adding methods to it and so on - we definitively don't want this to trigger any warnings.




回答3:


If you do Z2::M[1] = 2 you won´t get the message either. I believe the lack of warning occours because you are changing the Array itself and not the reference Z2::M.
If M was an integer, for exemple:

class Z2
  M = 1
end

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

To modify an Array from a constant without modify the original you can do:

class Z2
  M = [0,1]
end

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


来源:https://stackoverflow.com/questions/26537564/ruby-constants-seem-to-be-invisibly-alterable

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!