我刚刚阅读了一篇博客文章,并注意到作者在一段代码中使用了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"
答案 0 :(得分:92)
当读者遇到:
user = User.new
user.username = "foobar"
user.save!
他们必须遵循所有三行,然后才能认识到它只是创建一个名为user
的实例。
如果是:
user = User.new.tap do |u|
u.username = "foobar"
u.save!
end
然后就会立即清楚。读者不必阅读块内的内容就知道创建了一个实例user
。
答案 1 :(得分:32)
使用tap的另一种情况是在返回之前对对象进行操作。
所以不要这样:
def some_method
...
some_object.serialize
some_object
end
我们可以节省额外的一行:
def some_method
...
some_object.tap{ |o| o.serialize }
end
在某些情况下,这种技术可以节省多于一行并使代码更紧凑。
答案 2 :(得分:23)
正如博客所做的那样,使用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
同样,这是值得商榷的 - 但是可以证明第二个版本看起来有点混乱,需要更多人工解析才能看到所有方法都在同一个对象上调用。
答案 3 :(得分:14)
这对于调试一系列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
答案 4 :(得分:11)
在函数中可视化您的示例
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
答案 5 :(得分:10)
由于变量的范围仅限于真正需要的部分,因此它会导致代码混乱。此外,块中的缩进通过将相关代码保持在一起使代码更具可读性。
为块产生自我,然后返回自我。主要目的 这种方法的目的是“挖掘”一个方法链,以便执行 对链内中间结果的操作。
如果我们search rails source code for tap
usage,我们可以找到一些有趣的用法。以下几个项目(非详尽列表)将为我们提供有关如何使用它们的一些想法:
根据特定条件将元素附加到数组
%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
答案 6 :(得分:10)
如果您想在设置用户名后返回用户
user = User.new
user.username = 'foobar'
user
使用tap
你可以保存那个尴尬的回报
User.new.tap do |user|
user.username = 'foobar'
end
答案 7 :(得分:8)
@ 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
答案 8 :(得分:8)
这是电话链的帮手。它将其对象传递给给定块,并在块完成后返回对象:
an_object.tap do |o|
# do stuff with an_object, which is in o #
end ===> an_object
好处是tap总是返回它被调用的对象,即使该块返回其他一些结果。因此,您可以在现有方法管道的中间插入一个点击块而不会中断流程。
答案 9 :(得分:8)
我想说使用tap
没有任何好处。唯一的潜在好处,如@sawa points out,我引用:“读者不必阅读块中的内容就知道创建了实例用户。”但是,在这一点上可以提出这样的论点:如果你正在进行非简单化的记录创建逻辑,那么通过将该逻辑提取到自己的方法中可以更好地传达你的意图。
我认为tap
对代码的可读性造成了不必要的负担,并且可以在没有或用更好的技术替代的情况下完成,例如Extract Method。
虽然tap
是一种便利方法,但它也是个人偏好。试试tap
。然后在不使用tap的情况下编写一些代码,看看你是否喜欢另一种方式。
答案 10 :(得分:3)
我们可以使用tap
的用途和地点数量。到目前为止,我只发现了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)你有没有发现自己在某个对象上调用一个方法,并且返回值不是你想要的那个?也许您想为存储在哈希中的一组参数添加任意值。您使用哈希。[] 更新它,但是您返回 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)five-ruby-methods-you-should-be-using
答案 11 :(得分:3)
你是对的:在你的例子中使用tap
是没有意义的,可能不如你的选择那么干净。
正如Rebitzele所说,tap
只是一种方便的方法,通常用于创建对当前对象的较短引用。
tap
的一个好用例是用于调试:您可以修改对象,打印当前状态,然后继续修改同一块中的对象。例如:http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions。
我偶尔喜欢在方法中使用tap
来有条件地在返回当前对象时提前返回。
答案 12 :(得分:1)
在rails中,我们可以使用tap
明确地将参数列入白名单:
def client_params
params.require(:client).permit(:name).tap do |whitelist|
whitelist[:name] = params[:client][:name]
end
end
答案 13 :(得分:1)
我将再举一个我使用过的例子。我有一个方法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
答案 14 :(得分:1)
在功能编程模式已成为最佳实践(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
。
答案 15 :(得分:0)
您可以使用tap使代码更加模块化,并可以更好地管理局部变量。例如,在以下代码中,您不需要在方法范围内将局部变量分配给新创建的对象。请注意,块变量 u 在块中作用域。它实际上是ruby代码的优点之一。
def a_method
...
name = "foobar"
...
return User.new.tap do |u|
u.username = name
u.save!
end
end
答案 16 :(得分:0)
代码可读性方面的差异纯粹是风格上的。
user = User.new.tap do |u|
u.username = "foobar"
u.save!
end
要点:
u
变量现在如何用作块参数?user
变量现在应指向用户(用户名:“ foobar”,并且还会保存谁)。以下是易于阅读的源代码版本:
class Object
def tap
yield self
self
end
end
有关更多信息,请参见以下链接:
答案 17 :(得分:0)
有一个名为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!
答案 18 :(得分:0)
除上述答案外,我在编写RSpec时还使用了tap进行存根和模拟。
场景:当我有一个复杂的查询要进行存根和模拟时,请不要忽略多个参数。这里的替代方法是使用receive_message_chain
(但缺少详细信息)。
# Query
Product
.joins(:bill)
.where("products.availability = ?", 1)
.where("bills.status = ?", "paid")
.select("products.id", "bills.amount")
.first
# RSpecs
product_double = double('product')
expect(Product).to receive(:joins).with(:bill).and_return(product_double.tap do |product_scope|
expect(product_scope).to receive(:where).with("products.availability = ?", 1).and_return(product_scope)
expect(product_scope).to receive(:where).with("bills.status = ?", "paid").and_return(product_scope)
expect(product_scope).to receive(:select).with("products.id", "bills.amount").and_return(product_scope)
expect(product_scope).to receive(:first).and_return({ id: 1, amount: 100 })
end)
# Alternative way by using `receive_message_chain`
expect(Product).to receive_message_chain(:joins, :where, :where, :select).and_return({ id: 1, amount: 100 })