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

547
Ruby有一种方便的方法可以使用类似键的方式来共享实例变量:
attr_accessor :var
attr_reader :var
attr_writer :var

如果我可以使用 attr_accessor,为什么还要选择 attr_reader 或者 attr_writer? 是否有像性能这样的原因(我觉得不太可能)? 我猜一定是有其原因的,否则就不会有这样的关键字。


2
可能是什么是Ruby中的attr_accessor?的重复问题。 - sschuberth
5个回答

778

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

class Person
  attr_accessor :age
  ...
end

在这里,我可以看到我既可以读取年龄也可以写入年龄。

class Person
  attr_reader :age
  ...
end

在这里,我只能读取年龄。假设它是通过该类的构造函数设置并保持不变的。如果存在一个用于年龄的更改器(writer),并且类被编写为假定年龄一旦设置就不会改变,那么调用该更改器的代码可能会导致错误。

但是背后到底发生了什么?

如果您编写:

attr_writer :age

那将被翻译成:

def age=(value)
  @age = value
end

如果你写:

attr_reader :age

那会被翻译成:

def age
  @age
end

如果你写:

attr_accessor :age

这会被翻译成:

def age=(value)
  @age = value
end

def age
  @age
end

了解了这一点,这是另一种思考方式:如果没有attr_...帮助器,并且必须自己编写访问器,您会编写比类所需更多的访问器吗?例如,如果age只需要被读取,您是否还会编写允许它被写入的方法?


59
使用 attr_reader :adef a; return a; end 相比,前者有明显的性能优势。视频链接:http://confreaks.net/videos/427-rubyconf2010-zomg-why-is-this-code-so-slow - Nitrodist
85
对于Ruby 1.8.7版本,使用attr_reader定义的访问器所占用的时间是手动定义的访问器的86%。对于Ruby 1.9.0版本,使用attr_reader定义的访问器所占用的时间是手动定义的访问器的94%。无论如何,在我的所有测试中,访问器都很快:一个访问器大约需要820纳秒(Ruby 1.8.7)或440纳秒(Ruby 1.9)。在这些速度下,您需要调用数亿次访问器才能使attr_accessor的性能优势提高1秒以上的运行时间。 - Wayne Conrad
24
大概是由这个类的构造函数设置并保持不变。但实例变量(带有读取器)可能会经常更改。然而,它的目的是让它们的值只能由类私下进行更改。 - mlibby
12
可以使用逗号来添加超过2个属性,例如:attr_accessor :a, :b - Andrew_1510
3
多年以后,这是否值得一提:https://github.com/JuanitoFatas/fast-ruby#attr_accessor-vs-getter-and-setter-code 根据最新的基准测试结果,在 Ruby 2.2.0 中, attr_* 比 getter 和 setter 更快。 - molli
显示剩余5条评论

28

以上所有答案都是正确的;attr_readerattr_writer比手动输入它们所代表的方法更方便。除此之外,使用它们可以获得比手写方法定义更好的性能。欲了解更多信息,请参阅此演讲PDF)第152页及以后的内容,演讲者为Aaron Patterson。


18

重要的是要明白,访问器限制对变量的访问,但不限制它们的内容。在Ruby中,就像在其他一些面向对象语言中一样,每个变量都是指向实例的指针。因此,如果您有一个指向哈希表的属性,并将其设置为“只读”,则始终可以更改其内容,但不能更改指针的内容。 看这个例子:

> class A
>   attr_reader :a
>   def initialize
>     @a = {a:1, b:2}
>   end
> end
=> :initialize
> a = A.new
=> #<A:0x007ffc5a10fe88 @a={:a=>1, :b=>2}>
> a.a
=> {:a=>1, :b=>2}
> a.a.delete(:b)
=> 2
> a.a
=> {:a=>1}
> a.a = {}
NoMethodError: undefined method `a=' for #<A:0x007ffc5a10fe88 @a={:a=>1}>
        from (irb):34
        from /usr/local/bin/irb:11:in `<main>'

正如您所看到的,可以从哈希表@a中删除键/值对,添加新键,更改值等。但是无法指向新对象,因为它是只读实例变量。


17
并非所有对象的属性都应该直接从类外部进行设置。为所有实例变量编写设置器通常是弱封装的标志,并且警示您正在引入过多的类之间耦合。
举个实际的例子:我编写了一个设计程序,您可以将物品放入容器中。该物品具有attr_reader:container,但是没有提供编写器是有道理的,因为仅当物品被放置在新位置时,其容器才会更改,这也需要位置信息。

14

您并不总是希望类外部完全访问实例变量。有很多情况下,允许读取实例变量是有意义的,但可能不允许写入它(例如从只读源检索数据的模型)。当然也有相反的情况,但我暂时想不到任何不牵强附会的例子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接