我只是在阅读一篇博客文章,并注意到作者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"
当读者遇到:
user = User.new
user.username = "foobar"
user.save!
他们必须遵循所有三行,然后认识到它只是创建了一个名为 的实例user
。
如果是:
user = User.new.tap do |u|
u.username = "foobar"
u.save!
end
那么这将立即清楚。读者不必阅读块内的内容即可知道创建了一个实例user
。
使用点击的另一种情况是在返回对象之前对其进行操作。
所以代替这个:
def some_method
...
some_object.serialize
some_object
end
我们可以节省额外的行:
def some_method
...
some_object.tap{ |o| o.serialize }
end
在某些情况下,这种技术可以节省多于一行并使代码更紧凑。
这对于调试一系列链式作用域很有用。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
正如博主所做的那样,使用 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.username = 'foobar'
user
有了tap
你,就可以省去尴尬的回报
User.new.tap do |user|
user.username = 'foobar'
end
在函数中可视化您的示例
def make_user(name)
user = User.new
user.username = name
user.save!
end
这种方法存在很大的维护风险,基本上是隐式返回值。
在该代码中,您确实依赖于save!
返回已保存的用户。但是,如果您使用不同的鸭子(或您当前的鸭子进化),您可能会得到其他东西,例如完成状态报告。因此,对鸭子的更改可能会破坏代码,如果您使用普通user
或使用点击确保返回值,则不会发生这种情况。
我经常看到这样的事故,特别是除了一个黑暗的越野车角落之外通常不使用返回值的函数。
隐式返回值往往是新手倾向于在最后一行之后添加新代码而没有注意到效果的情况之一。他们看不到上面代码的真正含义:
def make_user(name)
user = User.new
user.username = name
return user.save! # notice something different now?
end
由于变量的范围仅限于真正需要的部分,因此它会导致代码不那么混乱。此外,块内的缩进通过将相关代码放在一起使代码更具可读性。
将 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
有助于弄清楚代码的意图(但不一定会使代码更紧凑)。
以下两个函数同样长,但在第一个函数中,您必须通读最后才能弄清楚为什么我在开头初始化了一个空 Hash。
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
它是调用链的助手。它将其对象传递给给定的块,并在块完成后返回该对象:
an_object.tap do |o|
# do stuff with an_object, which is in o #
end ===> an_object
好处是 tap 总是返回它被调用的对象,即使块返回一些其他结果。因此,您可以在现有方法管道的中间插入一个 Tap 块,而不会中断流程。
我会说使用tap
. 正如@sawa 指出的那样,唯一的潜在好处是,我引用:“读者不必阅读块内的内容即可知道创建了实例用户。” 但是,此时可以提出这样的论点,即如果您正在执行非简单的记录创建逻辑,则可以通过将该逻辑提取到其自己的方法中来更好地传达您的意图。
我认为这tap
对代码的可读性造成了不必要的负担,可以不用,或者用更好的技术代替,比如Extract Method。
虽然tap
是一种方便的方法,但它也是个人喜好。试一试tap
。然后在不使用tap的情况下编写一些代码,看看你是否喜欢一种方式而不是另一种方式。
可能有许多用途和我们可以使用的地方tap
。到目前为止,我只发现了tap
.
1) 该方法的主要目的是利用方法链,以便对链中的中间结果执行操作。IE
(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 散列,所以你必须明确地返回它。IE
def update_params(params)
params[:foo] = 'bar'
params
end
为了克服这种情况,tap
方法开始发挥作用。只需在对象上调用它,然后通过 tap 一个包含您要运行的代码的块。对象将屈服于块,然后返回。IE
def update_params(params)
params.tap {|p| p[:foo] = 'bar' }
end
还有几十个其他用例,尝试自己找到它们:)
资料来源:
1) API Dock Object tap
2)五个 ruby-methods-you-should-be-using
有一个名为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!
你是对的:tap
在你的例子中使用 是没有意义的,可能不如你的替代品那么干净。
正如 Rebitzele 所指出的,tap
这只是一种方便的方法,通常用于创建对当前对象的较短引用。
一个很好的用例tap
是用于调试:您可以修改对象,打印当前状态,然后在同一块中继续修改对象。例如,请参见此处:http: //moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions。
我偶尔喜欢使用tap
内部方法有条件地提前返回,否则返回当前对象。
您可以使用 tap 使您的代码更加模块化,并且可以更好地管理局部变量。例如,在下面的代码中,您不需要在方法范围内为新创建的对象分配局部变量。请注意,块变量u的范围在块内。它实际上是 ruby 代码的优点之一。
def a_method
...
name = "foobar"
...
return User.new.tap do |u|
u.username = name
u.save!
end
end
在 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来移除局部变量并移除return:
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
有关更多信息,请参阅以下链接:
除了上述答案之外,我在编写 RSpecs 时还使用了 tap 进行 stubbing 和 mocking。
场景:当我有一个复杂的查询要存根并使用多个不应错过的参数进行模拟时。这里的替代方法是使用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 })