红宝石拍打法的优势


116

我刚刚读了一篇博客文章,发现作者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:


103

当读者遇到:

user = User.new
user.username = "foobar"
user.save!

他们必须遵循这三行代码,然后认识到它只是在创建名为的实例user

如果是:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

那将立即清楚。读者不必阅读块中的内容即可知道user已创建实例。


3
@Matt:而且,一旦该块完成其工作,就丢弃该过程中所做的任何变量定义。而且应该有刚才打电话的对象上的一个方法,你可以写User.new.tap &:foobar
鲍里斯Stitnicky

28
我觉得这种用法不是很引人注目-可以说不再可读,这就是为什么在此页面上。没有明显的可读性,我比较了速度。我的测试表明,上述简单实现的附加运行时间增加了45%,随着对象上设置器数量的增加而减少-其中大约10个或更多设置器,并且运行时差异可以忽略不计(YMMV)。在调试过程中“攻入”方法链似乎是一个胜利,否则我需要更多的说服我。
dinman2022

7
我认为user = User.create!(username: 'foobar')在这种情况下,类似的东西将是最清晰最短的:)-问题的最后一个例子。
李李

4
这个答案本身矛盾,因此没有任何意义。除了“仅创建名为的实例”之外,发生的事情还更多user。还有一个论点,即“读者不必知道要user创建的实例,也不必阅读块中的内容”。它没有权重,因为在第一个代码块中,读者也只需要阅读第一行“就知道实例user已创建”。
杰克逊

5
那我为什么在这里 为什么我们所有人都在这里搜索水龙头。
艾迪(Eddie)

37

使用tap的另一种情况是在返回对象之前对其进行操作。

所以代替这个:

def some_method
  ...
  some_object.serialize
  some_object
end

我们可以节省额外的行:

def some_method
  ...
  some_object.tap{ |o| o.serialize }
end

在某些情况下,此技术可以节省多于一行的内容,并使代码更紧凑。


24
我会更加激烈:some_object.tap(&:serialize)
amencarini 2015年

28

就像博客作者一样,使用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

同样,这值得商--但可以证明第二个版本看起来有些混乱,需要更多的人工分析才能看到所有方法都在同一个对象上被调用。


2
这只是OP已在他的问题中提出的一个较长的例子,您仍然可以使用上述方法进行所有user = User.new, user.do_something, user.do_another_thing... ...您能否解释为什么有人可以这样做?
马特2013年

尽管该示例实质上是相同的,但是当以更长的形式显示该示例时,可以看到使用水龙头在这种情况下可能更具美学吸引力。我将添加一个修改以帮助演示。
Rebitzele

我也看不到。使用tap从未给我的经验带来任何好处。user在我看来,创建和使用局部变量更加简洁,而且可读性强。
gylaz

这两个不是等效的。如果您这样做了u = user = User.new,然后将其u用于设置调用,则它将与第一个示例更加一致。
格里

26

这对于调试一系列链接的作用域很有用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

14

在函数中可视化您的示例

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

1
您的两个示例之间绝对没有区别。你是要回来user吗?
布莱恩·阿什

1
那就是他的观点:示例完全相同,只是关于收益的明确说明。他的观点是,使用水龙头可以避免这种情况:User.new.tap{ |u| u.username = name; u.save! }
Obversity,2016年

14

如果要在设置用户名后返回用户,则需要执行此操作

user = User.new
user.username = 'foobar'
user

有了tap你,你可以节省那尴尬的回报

User.new.tap do |user|
  user.username = 'foobar'
end

1
Object#tap对我来说,这是最常见的用例。
Lyndsy Simon

1
好了,您已经保存了零行代码,现在,当在方法末尾查看返回的内容时,我必须向上扫描以查看该块是#tap块。不确定这是什么胜利。
Irongaze.com

也许,但这很容易成为1班轮 user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder,

11

由于变量的范围仅限于实际需要的部分,因此它使代码更整洁。同样,通过将相关代码保持在一起,该块内的缩进使代码更具可读性。

说明tap

将self屈服于块,然后返回self。此方法的主要目的是“进入”方法链,以便对链中的中间结果执行操作。

如果我们在rails源代码中搜索tap用法,我们会发现一些有趣的用法。以下是一些项目(并非详尽列表),这些内容将使我们对如何使用它们有一些想法:

  1. 根据某些条件将元素追加到数组

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
  2. 初始化数组并返回它

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. 至于语法糖使代码更易读-可以说,在下面的例子,使用的变量hash,并server使得意图的代码更清晰。

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. 在新创建的对象上初始化/调用方法。

    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
  5. 根据yield调用结果采取行动,而不必使用临时变量。

    yield.tap do |rendered_partial|
      collection_cache.write(key, rendered_partial, cache_options)
    end

9

@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方法至少表明数据结构是该方法的重点。
volx757

不确定此示例是否更好,在这种情况下基准测试与def tapping1; {one: 1, two: 2}; end显示使用.tap速度相比降低了约50%
lacostenycoder

9

它是呼叫链接的助手。它将其对象传递到给定的块中,并在该块完成后返回该对象:

an_object.tap do |o|
  # do stuff with an_object, which is in o #
end  ===> an_object

好处是,tap总是返回调用的对象,即使该块返回了其他结果。因此,您可以在不中断流程的情况下将水龙头块插入现有方法管道的中间。


8

我会说使用并没有优势tap。正如@sawa指出的那样,唯一的潜在好处是,我引用:“读者不必知道要创建实例用户的情况,就不必阅读块中的内容。” 但是,此时可以说,如果您正在执行非简单记录创建逻辑,则可以通过将逻辑提取到自己的方法中来更好地传达您的意图。

我认为这tap对代码的可读性是不必要的负担,可以不使用或使用更好的技术(例如提取方法)来完成

虽然这tap是一种方便的方法,但它也是个人喜好。给tap一试。然后编写一些代码而不用点击,看看是否喜欢一种方式。


4

可能有许多用途和地方,我们可以使用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)您应该使用的五种红宝石方法


3

您说对了:使用 tap在您的示例中,使用毫无意义,而且可能不如其他方法那么干净。

正如瑞比泽勒所说, tap这只是一种便捷的方法,通常用于创建对当前对象的较短引用。

一个很好的用例tap是进行调试:您可以修改对象,打印当前状态,然后继续在同一块中修改对象。例如,请参见此处:http : //moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions

我偶尔喜欢使用tap内部方法有条件地早返回,否则返回当前对象。



3

有一个名为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!

1

您可以使用tap使代码更具模块化,并可以更好地管理局部变量。例如,在以下代码中,您无需在方法范围内为新创建的对象分配局部变量。请注意,块变量u的作用域在该块内。它实际上是红宝石代码的优点之一。

def a_method
  ...
  name = "foobar"
  ...
  return User.new.tap do |u|
    u.username = name
    u.save!
  end
end


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


0

有什么区别?

代码可读性方面的差异纯粹是风格上的。

代码演练:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

关键点:

  • 请注意 u变量现在用作块参数?
  • 块完成后, user变量现在应指向用户(用户名:'foobar',并且还将保存谁)。
  • 这只是令人愉快且易于阅读。

API文档

这是一个易于阅读的源代码版本:

class Object
  def tap
    yield self
    self
  end
end

有关更多信息,请参见以下链接:

https://apidock.com/ruby/Object/tap

http://ruby-doc.org/core-2.2.3/Object.html#method-i-tap

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.