我刚刚读了一篇博客文章,发现作者tap
在摘要中使用了以下内容:
user = User.new.tap do |u|
u.username = "foobar"
u.save!
end
我的问题是使用它的好处或好处到底是什么tap
?我不能只是做:
user = User.new
user.username = "foobar"
user.save!
或更好:
user = User.create! username: "foobar"
我刚刚读了一篇博客文章,发现作者tap
在摘要中使用了以下内容:
user = User.new.tap do |u|
u.username = "foobar"
u.save!
end
我的问题是使用它的好处或好处到底是什么tap
?我不能只是做:
user = User.new
user.username = "foobar"
user.save!
或更好:
user = User.create! username: "foobar"
Answers:
当读者遇到:
user = User.new
user.username = "foobar"
user.save!
他们必须遵循这三行代码,然后认识到它只是在创建名为的实例user
。
如果是:
user = User.new.tap do |u|
u.username = "foobar"
u.save!
end
那将立即清楚。读者不必阅读块中的内容即可知道user
已创建实例。
user = User.create!(username: 'foobar')
在这种情况下,类似的东西将是最清晰最短的:)-问题的最后一个例子。
user
。还有一个论点,即“读者不必知道要user
创建的实例,也不必阅读块中的内容”。它没有权重,因为在第一个代码块中,读者也只需要阅读第一行“就知道实例user
已创建”。
使用tap的另一种情况是在返回对象之前对其进行操作。
所以代替这个:
def some_method
...
some_object.serialize
some_object
end
我们可以节省额外的行:
def some_method
...
some_object.tap{ |o| o.serialize }
end
在某些情况下,此技术可以节省多于一行的内容,并使代码更紧凑。
some_object.tap(&:serialize)
就像博客作者一样,使用tap只是一种便捷的方法。在您的示例中,这可能是过高的,但是如果您想与用户一起做很多事情,则可以说tap可以提供更简洁的界面。因此,也许在以下示例中可能会更好:
user = User.new.tap do |u|
u.build_profile
u.process_credit_card
u.ship_out_item
u.send_email_confirmation
u.blahblahyougetmypoint
end
使用上述方法,可以轻松快速地将所有这些方法归为一组,因为它们都引用相同的对象(在此示例中为用户)。替代方法是:
user = User.new
user.build_profile
user.process_credit_card
user.ship_out_item
user.send_email_confirmation
user.blahblahyougetmypoint
同样,这值得商--但可以证明第二个版本看起来有些混乱,需要更多的人工分析才能看到所有方法都在同一个对象上被调用。
user = User.new, user.do_something, user.do_another_thing
... ...您能否解释为什么有人可以这样做?
tap
从未给我的经验带来任何好处。user
在我看来,创建和使用局部变量更加简洁,而且可读性强。
u = user = User.new
,然后将其u
用于设置调用,则它将与第一个示例更加一致。
这对于调试一系列链接的作用域很有用。ActiveRecord
User
.active .tap { |users| puts "Users so far: #{users.size}" }
.non_admin .tap { |users| puts "Users so far: #{users.size}" }
.at_least_years_old(25) .tap { |users| puts "Users so far: #{users.size}" }
.residing_in('USA')
这使得在链中的任何位置进行调试变得非常容易,而无需将任何内容存储在局部变量中,也无需对原始代码进行大量更改。
最后,使用它作为一种快速,简便的调试方式,而又不会破坏正常的代码执行:
def rockwell_retro_encabulate
provide_inverse_reactive_current
synchronize_cardinal_graham_meters
@result.tap(&method(:puts))
# Will debug `@result` just before returning it.
end
在函数中可视化您的示例
def make_user(name)
user = User.new
user.username = name
user.save!
end
这种方法存在很大的维护风险,基本上是隐式的返回值。
在该代码中,您确实需要save!
返回已保存的用户。但是,如果您使用其他鸭子(或当前的鸭子进化),则可能会得到其他信息,例如完成状态报告。因此,对鸭子的更改可能会破坏代码,如果您确保使用简单的返回值,则不会发生这种情况user
使用或使用tap。
我经常看到这样的事故,特别是在函数中,除了一个黑暗的越野车角,通常不使用返回值。
隐式的返回值往往是新手往往在不注意效果的情况下在最后一行之后添加新代码破坏事物的情况之一。他们看不到上面的代码真正的含义:
def make_user(name)
user = User.new
user.username = name
return user.save! # notice something different now?
end
user
吗?
User.new.tap{ |u| u.username = name; u.save! }
如果要在设置用户名后返回用户,则需要执行此操作
user = User.new
user.username = 'foobar'
user
有了tap
你,你可以节省那尴尬的回报
User.new.tap do |user|
user.username = 'foobar'
end
Object#tap
对我来说,这是最常见的用例。
user = User.new.tap {|u| u.username = 'foobar' }
由于变量的范围仅限于实际需要的部分,因此它使代码更整洁。同样,通过将相关代码保持在一起,该块内的缩进使代码更具可读性。
将self屈服于块,然后返回self。此方法的主要目的是“进入”方法链,以便对链中的中间结果执行操作。
如果我们在rails源代码中搜索tap
用法,我们会发现一些有趣的用法。以下是一些项目(并非详尽列表),这些内容将使我们对如何使用它们有一些想法:
根据某些条件将元素追加到数组
%w(
annotations
...
routes
tmp
).tap { |arr|
arr << 'statistics' if Rake.application.current_scope.empty?
}.each do |task|
...
end
初始化数组并返回它
[].tap do |msg|
msg << "EXPLAIN for: #{sql}"
...
msg << connection.explain(sql, bind)
end.join("\n")
至于语法糖使代码更易读-可以说,在下面的例子,使用的变量hash
,并server
使得意图的代码更清晰。
def select(*args, &block)
dup.tap { |hash| hash.select!(*args, &block) }
end
在新创建的对象上初始化/调用方法。
Rails::Server.new.tap do |server|
require APP_PATH
Dir.chdir(Rails.application.root)
server.start
end
以下是来自测试文件的示例
@pirate = Pirate.new.tap do |pirate|
pirate.catchphrase = "Don't call me!"
pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
pirate.save!
end
根据yield
调用结果采取行动,而不必使用临时变量。
yield.tap do |rendered_partial|
collection_cache.write(key, rendered_partial, cache_options)
end
@sawa答案的变化形式:
如前所述,使用tap
有助于弄清楚代码的意图(尽管不一定使其更紧凑)。
以下两个函数同样长,但是在第一个函数中,您必须通读结尾以弄清楚为什么我在一开始就初始化了一个空哈希。
def tapping1
# setting up a hash
h = {}
# working on it
h[:one] = 1
h[:two] = 2
# returning the hash
h
end
另一方面,您从一开始就知道,初始化的哈希将是块的输出(在这种情况下,是函数的返回值)。
def tapping2
# a hash will be returned at the end of this block;
# all work will occur inside
Hash.new.tap do |h|
h[:one] = 1
h[:two] = 2
end
end
tap
使得说法更具说服力。我同意其他人的看法,当您看到时user = User.new
,意图已经很清楚了。但是,匿名数据结构可以用于任何事物,并且该tap
方法至少表明数据结构是该方法的重点。
def tapping1; {one: 1, two: 2}; end
显示使用.tap
速度相比降低了约50%
它是呼叫链接的助手。它将其对象传递到给定的块中,并在该块完成后返回该对象:
an_object.tap do |o|
# do stuff with an_object, which is in o #
end ===> an_object
好处是,tap总是返回调用的对象,即使该块返回了其他结果。因此,您可以在不中断流程的情况下将水龙头块插入现有方法管道的中间。
我会说使用并没有优势tap
。正如@sawa指出的那样,唯一的潜在好处是,我引用:“读者不必知道要创建实例用户的情况,就不必阅读块中的内容。” 但是,此时可以说,如果您正在执行非简单记录创建逻辑,则可以通过将逻辑提取到自己的方法中来更好地传达您的意图。
我认为这tap
对代码的可读性是不必要的负担,可以不使用或使用更好的技术(例如提取方法)来完成。
虽然这tap
是一种方便的方法,但它也是个人喜好。给tap
一试。然后编写一些代码而不用点击,看看是否喜欢一种方式。
可能有许多用途和地方,我们可以使用tap
。到目前为止,我仅发现以下2种用途tap
。
1)此方法的主要目的是进入方法链,以便对链中的中间结果执行操作。即
(1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
tap { |x| puts "array: #{x.inspect}" }.
select { |x| x%2 == 0 }.
tap { |x| puts "evens: #{x.inspect}" }.
map { |x| x*x }.
tap { |x| puts "squares: #{x.inspect}" }
2)您是否曾经发现自己在某个对象上调用方法,而返回值却不是您想要的值?也许您想向存储在哈希中的一组参数添加一个任意值。您可以使用Hash。[]更新它,但是会返回bar而不是params哈希,因此必须显式返回它。即
def update_params(params)
params[:foo] = 'bar'
params
end
为了克服这种情况,tap
方法开始发挥作用。只需在对象上调用它,然后使用您要运行的代码通过点击一个块即可。该对象将屈服于该块,然后返回。即
def update_params(params)
params.tap {|p| p[:foo] = 'bar' }
end
还有许多其他用例,请尝试自己找到它们:)
来源:
1)API Dock Object tap
2)您应该使用的五种红宝石方法
您说对了:使用 tap
在您的示例中,使用毫无意义,而且可能不如其他方法那么干净。
正如瑞比泽勒所说, tap
这只是一种便捷的方法,通常用于创建对当前对象的较短引用。
一个很好的用例tap
是进行调试:您可以修改对象,打印当前状态,然后继续在同一块中修改对象。例如,请参见此处:http : //moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions。
我偶尔喜欢使用tap
内部方法有条件地早返回,否则返回当前对象。
有一个名为flog的工具,可以测量读取方法的难度。“分数越高,代码就越痛苦。”
def with_tap
user = User.new.tap do |u|
u.username = "foobar"
u.save!
end
end
def without_tap
user = User.new
user.username = "foobar"
user.save!
end
def using_create
user = User.create! username: "foobar"
end
根据flog的结果,该方法tap
最难读(我同意)
4.5: main#with_tap temp.rb:1-4
2.4: assignment
1.3: save!
1.3: new
1.1: branch
1.1: tap
3.1: main#without_tap temp.rb:8-11
2.2: assignment
1.1: new
1.1: save!
1.6: main#using_create temp.rb:14-16
1.1: assignment
1.1: create!
在rails中,我们可以使用tap
显式将参数列入白名单:
def client_params
params.require(:client).permit(:name).tap do |whitelist|
whitelist[:name] = params[:client][:name]
end
end
我将再举一个我使用过的例子。我有一个方法user_params,它返回为用户保存所需的参数(这是一个Rails项目)
def user_params
params.require(:user).permit(
:first_name,
:last_name,
:email,
:address_attributes
)
end
您可以看到我没有返回任何东西,但ruby返回了最后一行的输出。
然后,一段时间后,我需要有条件地添加一个新属性。因此,我将其更改为如下所示:
def user_params
u_params = params.require(:user).permit(
:first_name,
:last_name,
:email,
:address_attributes
)
u_params[:time_zone] = address_timezone if u_params[:address_attributes]
u_params
end
在这里,我们可以使用tap删除局部变量并删除返回值:
def user_params
params.require(:user).permit(
:first_name,
:last_name,
:email,
:address_attributes
).tap do |u_params|
u_params[:time_zone] = address_timezone if u_params[:address_attributes]
end
end
在函数式编程模式已成为最佳实践的世界中(https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming),您确实可以将tap
视为map
单个值,以修改转换链上的数据。
transformed_array = array.map(&:first_transformation).map(&:second_transformation)
transformed_value = item.tap(&:first_transformation).tap(&:second_transformation)
无需item
在此处多次声明。
代码可读性方面的差异纯粹是风格上的。
user = User.new.tap do |u|
u.username = "foobar"
u.save!
end
关键点:
u
变量现在用作块参数?user
变量现在应指向用户(用户名:'foobar',并且还将保存谁)。这是一个易于阅读的源代码版本:
class Object
def tap
yield self
self
end
end
有关更多信息,请参见以下链接:
User.new.tap &:foobar