为什么要使用Ruby的attr_accessor,attr_reader和attr_writer?

Why use Ruby's attr_accessor, attr_reader and attr_writer?

Ruby有这样一种简便的方法,可以通过使用键来共享实例变量,比如

1
2
3
attr_accessor :var
attr_reader :var
attr_writer :var

如果我可以简单地使用attr_accessor,为什么我要选择attr_readerattr_writer?有没有像表演这样的东西(我怀疑)?我想是有原因的,否则他们就不会做这样的钥匙了。


您可以使用不同的访问器向阅读代码的人传达您的意图,并使编写类变得更容易,这些类无论如何调用其公共API都能正常工作。

1
2
3
4
class Person
  attr_accessor :age
  ...
end

在这里,我可以看到我既读又写的年龄。

1
2
3
4
class Person
  attr_reader :age
  ...
end

在这里,我可以看到我只能读年龄。假设它是由这个类的构造函数设置的,并且在这之后保持不变。如果有一个针对年龄的变异体(编写器),并且类是在假定年龄(一旦设置)不变的情况下编写的,那么一个bug可能是由调用该变异体的代码引起的。

但幕后发生了什么?

如果你写:

1
attr_writer :age

翻译成:

1
2
3
def age=(value)
  @age = value
end

如果你写:

1
attr_reader :age

翻译成:

1
2
3
def age
  @age
end

如果你写:

1
attr_accessor :age

翻译成:

1
2
3
4
5
6
7
def age=(value)
  @age = value
end

def age
  @age
end

知道了这一点,这里有另一种思考方法:如果你没有属性…帮助者,并且必须自己编写访问器,您会编写比您的类需要的访问器更多的访问器吗?例如,如果只需要读年龄,你是否也会写一个允许写年龄的方法?


以上答案都是正确的;与手工输入他们的简写方法相比,attr_readerattr_writer更便于书写。除此之外,它们提供了比自己编写方法定义更好的性能。欲了解更多信息,请参阅Aaron Patterson的本次演讲(pdf)之后的幻灯片152。


并非所有对象属性都是从类外部直接设置的。拥有所有实例变量的编写器通常是弱封装的标志,并且警告您在类之间引入了太多耦合。

作为一个实际的例子:我编写了一个设计程序,您可以在其中将项目放入容器中。这个项目有attr_reader :container,但是提供一个作者是没有意义的,因为项目的容器唯一应该改变的时间是当它被放置在一个新的容器中时,这也需要定位信息。


您并不总是希望实例变量可以从类外部完全访问。在许多情况下,允许对实例变量进行读访问是有意义的,但写入实例变量可能没有意义(例如,从只读源中检索数据的模型)。有些情况下,你想要的是相反的,但我想不出有什么不是我头脑中虚构出来的。


重要的是要理解访问器限制对变量的访问,而不是其内容的访问。在Ruby中,和其他一些OO语言一样,每个变量都是指向实例的指针。因此,例如,如果您有一个哈希的属性,并且将其设置为"只读",那么您总是可以更改它的内容,但不能更改指针的内容。看看这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
irb(main):024:0> class A
irb(main):025:1> attr_reader :a
irb(main):026:1> def initialize
irb(main):027:2> @a = {a:1, b:2}
irb(main):028:2> end
irb(main):029:1> end
=> :initialize
irb(main):030:0> a = A.new
=> #<A:0x007ffc5a10fe88 @a={:a=>1, :b=>2}>
irb(main):031:0> a.a
=> {:a=>1, :b=>2}
irb(main):032:0> a.a.delete(:b)
=> 2
irb(main):033:0> a.a
=> {:a=>1}
irb(main):034:0> a.a = {}
NoMethodError: undefined method `a=' for #<A:0x007ffc5a10fe88 @a={:a=>1}>
        from (irb):34
        from /usr/local/bin/irb:11:in `
<main>'

如您所见,可以从hash@a中删除一个键/值对,如添加新键、更改值和eccerta。但不能指向新对象,因为它是只读实例变量。