我正在阅读一篇关于元编程的文章,它表明你可以在另一个方法中定义一个方法。这是我已经知道了一段时间的事情,但它让我问自己一个问题:这有什么实际应用吗?在方法中定义方法是否有任何实际用途?
例如:
def outer_method
def inner_method
# ...
end
# ...
end
答案 0 :(得分:11)
我最喜欢的元编程示例是动态构建一个方法,然后您将在循环中使用它。例如,我有一个用Ruby编写的查询引擎,其中一个操作是过滤。有许多不同形式的过滤器(子串,等号,< =,> =,交叉点等)。天真的方法是这样的:
def process_filter(working_set,filter_type,filter_value)
working_set.select do |item|
case filter_spec
when "substring"
item.include?(filter_value)
when "equals"
item == filter_value
when "<="
item <= filter_value
...
end
end
end
但是如果你的工作集可以变大,你就会为每个操作执行1000s或1000000次的大案例语句,即使它在每次迭代时都会采用相同的分支。在我的情况下,逻辑比只是一个case语句更复杂,所以开销更大。相反,你可以这样做:
def process_filter(working_set,filter_type,filter_value)
case filter_spec
when "substring"
def do_filter(item,filter_value)
item.include?(filter_value)
end
when "equals"
def do_filter(item,filter_value)
item == filter_value
end
when "<="
def do_filter(item,filter_value)
item <= filter_value
end
...
end
working_set.select {|item| do_filter(item,filter_value)}
end
现在,一次性分支是在前面完成的,并且生成的单用途函数是内循环中使用的函数。
事实上,我的真实例子有三个层次,因为工作集和过滤器值的解释存在差异,而不仅仅是实际测试的形式。所以我构建了一个item-prep函数和一个filter-value-prep函数,然后构建一个使用它们的do_filter函数。
(我实际上使用的是lambdas,而不是defs。)
答案 1 :(得分:5)
是的,有。事实上,我敢打赌你每天至少使用一种定义另一种方法的方法:attr_accessor
。如果您使用Rails,则会持续使用Rails,例如belongs_to
和has_many
。它通常也适用于AOP风格的结构。
答案 2 :(得分:5)
我认为使用内在方法有另一个好处,即清晰度。想一想:一个包含方法列表的类是一个扁平的,非结构化的方法列表。如果你关心关注点的分离并将东西保持在相同的抽象层次中,并且这段代码只在一个地方使用,那么内部方法会有所帮助,同时强烈暗示它们仅用于封闭方法。
假设您在课程中使用此方法:
class Scoring
# other code
def score(dice)
same, rest = split_dice(dice)
set_score = if same.empty?
0
else
die = same.keys.first
case die
when 1
1000
else
100 * die
end
end
set_score + rest.map { |die, count| count * single_die_score(die) }.sum
end
# other code
end
现在,这是一种简单的数据结构转换和更高级别的代码,添加形成一组的骰子的分数和不属于该组的那些。但不是很清楚发生了什么。让我们更具描述性。下面是一个简单的重构:
class Scoring
# other methods...
def score(dice)
same, rest = split_dice(dice)
set_score = same.empty? ? 0 : get_set_score(same)
set_score + get_rest_score(rest)
end
def get_set_score(dice)
die = dice.keys.first
case die
when 1
1000
else
100 * die
end
end
def get_rest_score(dice)
dice.map { |die, count| count * single_die_score(die) }.sum
end
# other code...
end
get_set_score()和get_rest_score()的想法是通过使用描述性(虽然在这个炮制的例子中不是很好)来记录这些部分的作用。但是如果你有很多像这样的方法,得分()中的代码并不容易理解,如果你重构任何一种方法,你可能需要检查其他方法使用它们(即使它们是私有的 - 其他同一类的方法可以使用它们。)
相反,我开始更喜欢这个:
class Scoring
# other code
def score(dice)
def get_set_score(dice)
die = dice.keys.first
case die
when 1
1000
else
100 * die
end
end
def get_rest_score(dice)
dice.map { |die, count| count * single_die_score(die) }.sum
end
same, rest = split_dice(dice)
set_score = same.empty? ? 0 : get_set_score(same)
set_score + get_rest_score(rest)
end
# other code
end
在这里,更明显的是get_rest_score()和get_set_score()被包装到方法中以保持score()的逻辑本身处于相同的抽象级别,不干涉哈希等。
请注意,从技术上讲,你可以调用Scoring#get_set_score和Scoring#get_rest_score,但在这种情况下它会是错误的样式IMO,因为在语义上它们只是单个方法得分的私有方法()< / p>
所以,有了这个结构,你总是可以阅读得分()的整个实现,而无需查看在得分#得分之外定义的任何其他方法。即使我没有经常看到这样的Ruby代码,我想我会用内部方法将更多内容转换为这种结构化样式。
注意:另一个看起来不干净但避免名称冲突问题的选项就是简单地使用lambdas,它在Ruby中一直存在。使用该示例,它将变为
get_rest_score = -> (dice) do
dice.map { |die, count| count * single_die_score(die) }.sum
end
...
set_score + get_rest_score.call(rest)
它不是那么漂亮 - 有人在看代码时可能想知道为什么所有这些lambdas,而使用内部方法是非常自我记录的。我仍然更倾向于lambdas,因为他们没有将潜在冲突的名称泄露到当前范围的问题。
答案 3 :(得分:3)
不使用def
。没有实际的应用程序,编译器可能会引发错误。
有理由在执行另一个方法的过程中动态定义方法。考虑使用C实现的attr_reader
,但可以在Ruby中等效地实现:
class Module
def attr_reader(name)
define_method(name) do
instance_variable_get("@#{name}")
end
end
end
在这里,我们使用#define_method
来定义方法。 #define_method
是一种实际的方法; def
不是。这给了我们两个重要的属性。首先,它需要一个参数,它允许我们将name
变量传递给它来命名方法。其次,它需要一个块,它关闭我们的变量name
,允许我们在方法定义中使用它。
那么如果我们使用def
会发生什么呢?
class Module
def attr_reader(name)
def name
instance_variable_get("@#{name}")
end
end
end
这根本不起作用。首先,def
关键字后跟一个文字名称,而不是表达式。这意味着我们正在定义一个名为#name
的方法,这根本不是我们想要的。其次,方法的主体引用一个名为name
的局部变量,但Ruby不会将它识别为与#attr_reader
的参数相同的变量。 def
构造不使用块,因此它不再关闭变量name
。
def
构造不允许您“传入”任何信息来参数化您定义的方法的定义。这使得它在动态环境中无用。没有理由在方法中使用def
定义方法。您始终可以将相同的内部def
构造从外部def
移出,并以相同的方法结束。
此外,动态定义方法会产生成本。 Ruby缓存方法的内存位置,从而提高性能。当您从类中添加或删除方法时,Ruby必须抛弃该缓存。 (在Ruby 2.1之前,该缓存是全局。从2.1开始,缓存是每个类。)
如果在另一个方法中定义一个方法,则每次调用外部方法时,它都会使缓存失效。这对于像attr_reader
和Rails'belongs_to
这样的顶级宏来说很好,因为这些都是在程序启动时调用,然后(希望)再也不会调用。在程序的持续执行过程中定义方法会让你慢下来。
答案 4 :(得分:0)
我在想一个递归的情况,但我认为它没有足够的意义。