为构造函数外部的实例变量指定默认值



我的目标是在不使用初始化方法的情况下初始化实例变量。 我有这个代码:

class Animal
attr_reader :age
def initialize(age)
@age = age
end
end
class Sheep < Animal
attr_accessor :likes
def initialize(age)
super
@likes = []
end
end
sheep = Sheep.new(5)
sheep.likes << "grass"

此子类中的initialize方法调用super。这不能很好地扩展:如果我更改超类的签名,我也必须在所有子类中调整它。

如果我能像许多其他 OO-语言一样在Sheep类范围内初始化一个实例变量,例如initialize方法之外的@likes = [],那就更好了。但是,这将使我的变量成为类对象的实例变量。

这是我发现的一种不覆盖构造函数的方法:

class Sheep < Animal
attr_accessor :likes
def likes
@likes || @likes = []
end
end

这要优雅得多,因为不需要重新调整签名,但它仍然不完美:当我访问该实例变量时,Ruby 不会检查likes的非nil性吗?有没有办法在不牺牲运行时或代码优雅的情况下做到这一点?

在最后一个示例中:

class Sheep < Animal
attr_accessor :likes
def likes
@likes || @likes = []
end
end

你本质上是在使用记忆,尽管你的语法与规范略有不同,看起来像:

def likes
@likes ||= []
end

此外,由于您现在likes是记忆方法,而不是实例的属性,因此不需要attr_accessor(或attr_reader等(。

class Sheep < Animal
def likes
@likes ||= []
end
end

你很好去。

编辑:根据您对性能的关注:

[1] pry(main)> require 'benchmark'
=> true
[2] pry(main)> @hello = []
=> []
[3] pry(main)> def hello
[3] pry(main)*   @hello
[3] pry(main)* end
=> :hello
[4] pry(main)> def likes
[4] pry(main)*   @likes ||= []
[4] pry(main)* end
=> :likes
[5] pry(main)> puts Benchmark.measure { 1_000_000.times { hello } }
0.070000   0.000000   0.070000 (  0.071330)
=> nil
[6] pry(main)> puts Benchmark.measure { 1_000_000.times { likes } }
0.100000   0.000000   0.100000 (  0.097388)
=> nil 

你可以做的一件事是从Animalinitialize调用一个方法,为子类提供一个钩子来添加自定义功能:

class Animal
attr_reader :age
def initialize(age)
@age = age
setup_defaults
end
private
def setup_defaults
# NOOP by default
end
end
class Sheep < Animal
attr_accessor :likes
private
def setup_defaults
@likes = []
end
end

您在帖子中提到的第二种方法是使用自定义def likes而不是attr_reader/attr_accessor

def likes
@likes ||= [] # shorter way of doing what you have
end

作为第三种选择,如果您不介意使用initialize(您主要关心的似乎是可能更改超类的签名(,因为您不关心初始化的任何参数Sheep您可以覆盖initialize,如下所示:

class Sheep < Animal
attr_accessor :likes
def initialize(*)
super
@likes = []
end
end

这与执行类似def initialize(*args)的操作相同,只是您不命名变量,并且由于默认情况下super传递原始参数而起作用。现在,如果你回过头来改变动物,比如说,对它的initialize有一个name论据:

class Animal
attr_reader :age, :name
def initialize(name, age)
@name = name
@age = age
end
end

Sheep仍然可以工作,无需任何更改。

使用实验性补丁,您可以执行以下操作:

class Zaloop
attr_accessor var1: :default_value, var2: 2
def initialize
self.initialize_default_values
end
end
puts Zaloop.new.var1 # :default_value
  • 请注意,下面的补丁 - 是实验性解决方案,如果您决定在生产中使用它,请小心

模块补丁:

Module.module_eval do
alias _original_attr_accessor attr_accessor
def attr_accessor(*args)
@default_values ||= {}
attr_names = []
args.map do |arg|
if arg.is_a? Hash
arg.each do |key, value|
define_default_initializer if @default_values.empty?
@default_values[key] = value
attr_names << key
end
else
attr_names << arg
end
end
_original_attr_accessor *attr_names
end
def define_default_initializer
default_values = @default_values
self.send :define_method, :initialize_default_values do
default_values.each do |key, value|
instance_variable_set("@#{key}".to_sym, value)
end
end
end
end