Ruby: как предотвратить изменение переменной экземпляра массива через считыватель атрибутов - PullRequest
9 голосов
/ 13 февраля 2012

извините за этот вопрос noob ... допустим, у нас есть:

class TestMe
 attr_reader :array

 def initialize
   @array = (1..10).to_a
 end

end

тогда можно сделать:

>> a = TestMe.new
=> #<TestMe:0x00000005567228 @x=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]>
>> a.array.map! &:to_s
=> ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
>> a.array
=> ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
  • это явно идет против инкапсуляции, не так ли?
  • Есть ли способ быстро защитить переменную массива от изменения?
  • ... или мне нужно реализовать глубокое копирование каждый раз, когда моя переменная экземпляра имеет "деструктивные" методы?

РЕДАКТИРОВАТЬ Я где-то читал, что "плохой ОО" - выставлять переменную экземпляра массива.Если это правда, почему?

Ответы [ 7 ]

11 голосов
/ 13 февраля 2012

Вы не можете многое сделать с attr_reader, потому что attr_reader :array генерирует следующий код:

def array; @array; end

Если вы не хотите предоставлять экземпляр массива, вы можете вернуть Enumerator этого массива (внешний итератор). Enumerator является хорошей абстракцией итератора и не позволяет вам изменять исходный массив.

def array; @array.to_enum; end

Что хорошо для инкапсуляции и что не зависит от абстракции, которую представляет ваш класс.Как правило, это плохо для инкапсуляции, чтобы раскрыть внутреннее состояние объекта, включая внутренний массив.Возможно, вы захотите предоставить некоторые методы, которые работают с @array вместо того, чтобы показывать @array (или даже его итератор).Иногда это нормально для выставления массива - всегда смотрите на абстракцию, которую представляет ваш класс.

5 голосов
/ 13 февраля 2012

Как насчет возврата копии исходного массива из getter:

class TestMe

  attr_writer :array

  def initialize
    @array = (1..10).to_a
  end

  def array
    @array.dup
  end

end

В этом случае вы не можете напрямую изменить исходный массив, но с помощью атрибута Writer вы можете заменить его новым (если вынеобходимо).

1 голос
/ 24 июля 2016

Создание определенного метода чтения атрибутов, который копирует исходный атрибут, работает хорошо, но имейте в виду, что ни @array.dup , ни Array.new(@array) не выполнят глубокое копирование .Это означает, что если у вас есть массив массивов (например, [[1, 2], [3, 4]]), ни одно из значений массива не будет защищено от изменений.Чтобы выполнить глубокую копию в ruby, я нашел самый простой способ:

return Marshal.load( Marshal.dump(@array) )

Marshall.dump преобразует любой объект в строку, которую впоследствии можно декодировать, чтобы вернуть объект обратно (процесс называется сериализацией).,Таким образом, вы получаете глубокую копию данного объекта.Легко, но немного грязно, я должен признать.

1 голос
/ 13 февраля 2012

Если вы хотите, чтобы массив оставался изменяемым, но не при возврате через считыватель, не возвращайте массив, а просто обертку, которая предоставляет «безопасные» методы.

require 'forwardable'
class SafeArray
  extend Forwardable
  def initialize(array); @array = array; end
  # add the other methods you want to expose to the following line
  def_delegators :@array, :size, :each, :[], :map
end

class TestMe
  def initialize
    @array = (1..10).to_a
  end
  def array
    @wrapper ||= SafeArray.new(@array)
  end
end
1 голос
/ 13 февраля 2012

Любой экземпляр может стать неизменным, вызвав замораживание:

class TestMe
 attr_reader :array

 def initialize
   @array = (1..10).to_a
   @array.freeze
 end
end

a = TestMe.new
a.array << 11
# Error: can't modify frozen array
0 голосов
/ 13 февраля 2012

Вы инкапсулируете, создавая метод с тем же именем, что и у вашей переменной экземпляра, но заканчивайте его знаком равенства. В вашем примере это будет:

def array=
..
end

В этом методе вы делаете все, что хотите, прежде чем присваивать новые значения массиву

0 голосов
/ 13 февраля 2012

Это против инкапсуляции, но мы можем решить проблему, правильно настроив getter method этого атрибута.

class TestMe

 def initialize
   @array = (1..10).to_a
 end

 def array
   Array.new(@array)
 end

end
...