lợi thế của phương pháp chạm trong ruby


116

Tôi chỉ đang đọc một bài báo trên blog và nhận thấy rằng tác giả đã sử dụng tapmột đoạn mã như:

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

Câu hỏi của tôi là chính xác thì lợi ích hoặc lợi thế của việc sử dụng là tapgì? Tôi không thể chỉ làm:

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

hoặc tốt hơn:

user = User.create! username: "foobar"

Câu trả lời:


103

Khi độc giả gặp phải:

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

họ sẽ phải làm theo tất cả ba dòng và sau đó nhận ra rằng nó chỉ đang tạo một thể hiện có tên user.

Nếu đó là:

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

thì điều đó sẽ rõ ràng ngay lập tức. Người đọc sẽ không phải đọc những gì bên trong khối để biết rằng một thể userhiện đã được tạo.


3
@Matt: Ngoài ra, hãy loại bỏ bất kỳ định nghĩa biến nào được thực hiện trong quy trình sau khi khối đã hoàn thành công việc của nó. Và nên có thể chỉ là một phương pháp được gọi trên đối tượng, bạn có thể viếtUser.new.tap &:foobar
Boris Stitnicky

28
Tôi thấy cách sử dụng này không hấp dẫn lắm - có thể nói là không thể đọc được nữa, đó là lý do tại sao lại xuất hiện trên trang này. Không có lập luận về khả năng đọc mạnh mẽ, tôi đã so sánh tốc độ. Các thử nghiệm của tôi cho thấy thời gian chạy bổ sung 45% cho các triển khai đơn giản ở trên, giảm dần khi số bộ thiết lập trên đối tượng tăng lên - khoảng 10 bộ trở lên trong số chúng và chênh lệch thời gian chạy là không đáng kể (YMMV). 'khai thác' vào một chuỗi các phương pháp trong khi gỡ lỗi có vẻ như là một chiến thắng, nếu không thì tôi cần nhiều hơn để thuyết phục tôi.
dinman2022

7
Tôi nghĩ điều gì đó giống như user = User.create!(username: 'foobar')sẽ rõ ràng và ngắn gọn nhất trong trường hợp này :) - ví dụ cuối cùng từ câu hỏi.
Lee

4
Câu trả lời này mâu thuẫn với chính nó, và do đó không có ý nghĩa. Nhiều điều đang xảy ra hơn là "chỉ tạo một cá thể được đặt tên user." Ngoài ra, lập luận rằng "Người đọc sẽ không phải đọc những gì bên trong khối để biết rằng một thể userhiện được tạo ra." không có trọng lượng, bởi vì trong khối mã đầu tiên, người đọc cũng chỉ cần đọc dòng đầu tiên "để biết rằng một cá thể userđược tạo."
Jackson

5
Tại sao tôi lại ở đây sau đó? Tại sao tất cả chúng ta ở đây tìm kiếm vòi là gì.
Eddie

37

Một trường hợp khác để sử dụng vòi là thực hiện thao tác trên đối tượng trước khi trả lại.

Vì vậy, thay vì điều này:

def some_method
  ...
  some_object.serialize
  some_object
end

chúng ta có thể tiết kiệm thêm dòng:

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

Trong một số tình huống, kỹ thuật này có thể tiết kiệm nhiều hơn một dòng và làm cho mã gọn gàng hơn.


24
Tôi sẽ còn nghiêm trọng hơn:some_object.tap(&:serialize)
amencarini

28

Sử dụng vòi, như blogger đã làm, chỉ đơn giản là một phương pháp tiện lợi. Nó có thể đã quá mức cần thiết trong ví dụ của bạn, nhưng trong trường hợp bạn muốn thực hiện một loạt việc với người dùng, nhấn có thể được cho là cung cấp một giao diện trông gọn gàng hơn. Vì vậy, có lẽ nó có thể tốt hơn trong một ví dụ như sau:

user = User.new.tap do |u|
  u.build_profile
  u.process_credit_card
  u.ship_out_item
  u.send_email_confirmation
  u.blahblahyougetmypoint
end

Sử dụng phần trên giúp bạn dễ dàng nhanh chóng thấy rằng tất cả các phương thức đó được nhóm lại với nhau và tất cả đều tham chiếu đến cùng một đối tượng (người dùng trong ví dụ này). Sự thay thế sẽ là:

user = User.new
user.build_profile
user.process_credit_card
user.ship_out_item
user.send_email_confirmation
user.blahblahyougetmypoint

Một lần nữa, điều này còn gây tranh cãi - nhưng có thể xảy ra trường hợp phiên bản thứ hai trông lộn xộn hơn một chút và cần nhiều phân tích cú pháp của con người hơn một chút để thấy rằng tất cả các phương thức đang được gọi trên cùng một đối tượng.


2
Đây chỉ là một ví dụ dài hơn về những gì OP đã đặt trong câu hỏi của mình, bạn vẫn có thể làm tất cả những điều trên với user = User.new, user.do_something, user.do_another_thing... bạn có thể vui lòng mở rộng thêm tại sao một người có thể làm điều này không?
Matt,

Mặc dù ví dụ về cơ bản là giống nhau, nhưng khi nó hiển thị ở dạng dài hơn, người ta có thể thấy cách sử dụng vòi có thể hấp dẫn hơn về mặt thẩm mỹ đối với trường hợp này. Tôi sẽ thêm một chỉnh sửa để giúp chứng minh.
Rebitzele

Tôi cũng không thấy. Sử dụng tapchưa bao giờ thêm bất kỳ lợi ích nào trong kinh nghiệm của tôi. Theo tôi, việc tạo và làm việc với một userbiến cục bộ sạch hơn nhiều và có thể đọc được.
gylaz

Hai cái đó không tương đương. Nếu bạn đã làm u = user = User.newvà sau đó sử dụng ucho các cuộc gọi thiết lập thì nó sẽ phù hợp hơn với ví dụ đầu tiên.
Gerry

26

Điều này có thể hữu ích với việc gỡ lỗi một loạt các ActiveRecordphạm vi được xâu chuỗi.

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')

Điều này giúp bạn dễ dàng gỡ lỗi tại bất kỳ điểm nào trong chuỗi mà không cần phải lưu trữ bất kỳ thứ gì trong một biến cục bộ cũng như không yêu cầu thay đổi nhiều mã gốc.

Và cuối cùng, hãy sử dụng nó như một cách nhanh chóng và không phô trương để gỡ lỗi mà không làm gián đoạn quá trình thực thi mã bình thường :

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

Hình dung ví dụ của bạn trong một hàm

def make_user(name)
  user = User.new
  user.username = name
  user.save!
end

Có một rủi ro bảo trì lớn với cách tiếp cận đó, về cơ bản là giá trị hoàn trả ngầm định .

Trong mã đó, bạn phụ thuộc vào việc save!trả lại người dùng đã lưu. Nhưng nếu bạn sử dụng một con vịt khác (hoặc con hiện tại của bạn tiến hóa), bạn có thể nhận được những thứ khác như báo cáo trạng thái hoàn thành. Do đó, các thay đổi đối với con vịt có thể phá vỡ mã, điều này sẽ không xảy ra nếu bạn đảm bảo giá trị trả về bằng một lần nhấn đơn giản userhoặc sử dụng.

Tôi đã thấy những tai nạn như thế này khá thường xuyên, đặc biệt là với các chức năng mà giá trị trả về thường không được sử dụng ngoại trừ một góc lỗi tối.

Giá trị trả về tiềm ẩn có xu hướng là một trong những thứ mà người mới có xu hướng phá vỡ mọi thứ khi thêm mã mới sau dòng cuối cùng mà không nhận thấy hiệu ứng. Họ không thấy đoạn mã trên thực sự có nghĩa là gì:

def make_user(name)
  user = User.new
  user.username = name
  return user.save!       # notice something different now?
end

1
Hoàn toàn không có sự khác biệt giữa hai ví dụ của bạn. Bạn có ý định trở về user?
Bryan Ash

1
Đó là quan điểm của anh ấy: các ví dụ hoàn toàn giống nhau, một ví dụ chỉ rõ ràng về lợi nhuận. Quan điểm của anh ấy là điều này có thể tránh được bằng cách sử dụng vòi:User.new.tap{ |u| u.username = name; u.save! }
Obversity

14

Nếu bạn muốn trả lại người dùng sau khi đặt tên người dùng, bạn cần thực hiện

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

Với tapbạn có thể cứu vãn sự trở lại khó xử đó

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

1
Đây là trường hợp sử dụng phổ biến nhất đối Object#tapvới tôi.
Lyndsy Simon

1
Chà, bạn đã không lưu dòng mã nào và bây giờ, khi nhìn vào phần cuối của phương thức để biết nó trả về, tôi phải quét lại để thấy rằng khối đó là khối #tap. Không chắc chắn đây là bất kỳ loại chiến thắng.
Irongaze.com

có lẽ nhưng điều này có thể dễ dàng trở thành một 1 liner user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder

11

Nó dẫn đến mã ít lộn xộn hơn vì phạm vi của biến chỉ bị giới hạn ở phần thực sự cần thiết. Ngoài ra, thụt lề trong khối giúp mã dễ đọc hơn bằng cách giữ các mã có liên quan lại với nhau.

Mô tả của tapnói :

Nhường bản thân cho khối, rồi tự trả về. Mục đích chính của phương pháp này là “khai thác” một chuỗi phương pháp, để thực hiện các thao tác trên các kết quả trung gian trong chuỗi.

Nếu chúng ta tìm kiếm mã nguồn rails để tapsử dụng , chúng ta có thể tìm thấy một số cách sử dụng thú vị. Dưới đây là một số mục (không phải danh sách đầy đủ) sẽ cung cấp cho chúng tôi một số ý tưởng về cách sử dụng chúng:

  1. Nối một phần tử vào một mảng dựa trên các điều kiện nhất định

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
  2. Khởi tạo một mảng và trả về nó

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. Như một đường cú pháp để làm cho mã dễ đọc hơn - Người ta có thể nói, trong ví dụ dưới đây, sử dụng các biến hashserverlàm cho mục đích của mã rõ ràng hơn.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. Khởi tạo / gọi các phương thức trên các đối tượng mới được tạo.

    Rails::Server.new.tap do |server|
       require APP_PATH
       Dir.chdir(Rails.application.root)
       server.start
    end

    Dưới đây là một ví dụ từ tệp thử nghiệm

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
  5. Để thực hiện theo kết quả của một yieldcuộc gọi mà không cần phải sử dụng một biến tạm thời.

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

9

Một biến thể trong câu trả lời của @ sawa:

Như đã lưu ý, việc sử dụng tapgiúp tìm ra mục đích của mã của bạn (trong khi không nhất thiết phải làm cho nó nhỏ gọn hơn).

Hai hàm sau đây dài như nhau, nhưng ở hàm đầu tiên, bạn phải đọc hết phần cuối để tìm ra lý do tại sao tôi khởi tạo một Hash trống ở đầu.

def tapping1
  # setting up a hash
  h = {}
  # working on it
  h[:one] = 1
  h[:two] = 2
  # returning the hash
  h
end

Mặt khác, ở đây, bạn biết ngay từ đầu rằng hàm băm được khởi tạo sẽ là đầu ra của khối (và trong trường hợp này là giá trị trả về của hàm).

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

ứng dụng này taptạo ra một lập luận thuyết phục hơn. Tôi đồng ý với những người khác rằng khi bạn nhìn thấy user = User.new, ý định đã rõ ràng. Tuy nhiên, một cấu trúc dữ liệu ẩn danh có thể được sử dụng cho mọi thứ và tapphương pháp này ít nhất cũng làm rõ rằng cấu trúc dữ liệu là trọng tâm của phương pháp.
volx757

Không chắc ví dụ này là bất kỳ tốt hơn và benchmark vs def tapping1; {one: 1, two: 2}; endchương trình sử dụng .taplà khoảng 50% chậm hơn trong trường hợp này
lacostenycoder

9

Nó là một người trợ giúp cho chuỗi cuộc gọi. Nó chuyển đối tượng của nó vào khối đã cho và sau khi khối kết thúc, trả về đối tượng:

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

Lợi ích là việc nhấn luôn trả về đối tượng mà nó được gọi, ngay cả khi khối trả về một số kết quả khác. Vì vậy, bạn có thể chèn một khối vòi vào giữa đường ống phương pháp hiện có mà không làm đứt dòng chảy.


8

Tôi sẽ nói rằng không có lợi thế khi sử dụng tap. Lợi ích tiềm năng duy nhất, như @sawa chỉ ra là, và tôi trích dẫn: "Người đọc sẽ không phải đọc những gì bên trong khối để biết rằng một người dùng cá thể được tạo." Tuy nhiên, tại thời điểm đó, lập luận có thể được đưa ra rằng nếu bạn đang thực hiện logic tạo bản ghi không đơn giản, ý định của bạn sẽ được truyền đạt tốt hơn bằng cách trích xuất logic đó thành phương pháp của riêng nó.

Tôi giữ quan điểm rằng đó taplà gánh nặng không cần thiết đối với khả năng đọc của mã và có thể được thực hiện mà không cần hoặc thay thế bằng một kỹ thuật tốt hơn, như Phương pháp trích xuất .

Mặc dù taplà một phương pháp tiện lợi, nó cũng là sở thích cá nhân. Hãy tapthử. Sau đó viết một số mã mà không cần sử dụng chạm, xem bạn có thích cách này hơn cách khác không.


4

Có thể có số lần sử dụng và những nơi mà chúng tôi có thể sử dụng tap. Cho đến nay tôi chỉ tìm thấy sau 2 công dụng của tap.

1) Mục đích chính của phương pháp này là khai thác vào một chuỗi phương pháp, để thực hiện các hoạt động trên các kết quả trung gian trong chuỗi. I E

(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) Bạn có bao giờ thấy mình đang gọi một phương thức trên một số đối tượng và giá trị trả về không như bạn muốn không? Có thể bạn muốn thêm một giá trị tùy ý vào một tập hợp các tham số được lưu trữ trong một hàm băm. Bạn cập nhật nó bằng Hash. [] , Nhưng bạn nhận được back bar thay vì params hash, vì vậy bạn phải trả lại nó một cách rõ ràng. I E

def update_params(params)
  params[:foo] = 'bar'
  params
end

Để khắc phục tình trạng này ở đây, tapphương pháp ra đời. Chỉ cần gọi nó trên đối tượng, sau đó chuyển chạm vào một khối có mã mà bạn muốn chạy. Đối tượng sẽ được nhường cho khối, sau đó được trả lại. I E

def update_params(params)
  params.tap {|p| p[:foo] = 'bar' }
end

Có hàng tá các trường hợp sử dụng khác, hãy thử tự tìm kiếm chúng :)

Nguồn:
1) API Dock Object tap
2) five-ruby-method-you-should-be-using


3

Bạn nói đúng: việc sử dụng taptrong ví dụ của bạn là vô nghĩa và có thể kém sạch sẽ hơn các lựa chọn thay thế của bạn.

Như Rebitzele lưu ý, tapchỉ là một phương pháp tiện lợi, thường được sử dụng để tạo một tham chiếu ngắn hơn đến đối tượng hiện tại.

Một trường hợp sử dụng tốt taplà để gỡ lỗi: bạn có thể sửa đổi đối tượng, in trạng thái hiện tại, sau đó tiếp tục sửa đổi đối tượng trong cùng một khối. Xem ví dụ ở đây: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions .

Tôi thỉnh thoảng thích sử dụng tapcác phương thức bên trong để trả về sớm có điều kiện trong khi trả về đối tượng hiện tại theo cách khác.


Đây cũng là ứng dụng được đề cập trong tài liệu: ruby-doc.org/core-2.1.3/Object.html#method-i-tap
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

3

Có một công cụ gọi là flog đo lường mức độ khó đọc của một phương pháp. "Điểm càng cao, mã càng đau."

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

và theo kết quả của flog, phương pháp với taplà khó đọc nhất (và tôi đồng ý với nó)

 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

Bạn có thể làm cho mã của mình trở nên mô-đun hơn bằng cách sử dụng chạm và có thể quản lý tốt hơn các biến cục bộ. Ví dụ, trong đoạn mã sau, bạn không cần phải gán một biến cục bộ cho đối tượng mới được tạo, trong phạm vi của phương thức. Lưu ý rằng biến khối, u , nằm trong phạm vi khối. Nó thực sự là một trong những vẻ đẹp của mã ruby.

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

1

Trong đường ray, chúng ta có thể sử dụng tapđể đưa các tham số vào danh sách trắng một cách rõ ràng:

def client_params
    params.require(:client).permit(:name).tap do |whitelist|
        whitelist[:name] = params[:client][:name]
    end
end

1

Tôi sẽ đưa ra một ví dụ khác mà tôi đã sử dụng. Tôi có một phương thức user_params trả về các tham số cần thiết để lưu cho người dùng (đây là một dự án Rails)

def user_params
  params.require(:user).permit(
    :first_name,
    :last_name,
    :email,
    :address_attributes
  )
end

Bạn có thể thấy tôi không trả lại bất cứ thứ gì nhưng ruby ​​trả về đầu ra của dòng cuối cùng.

Sau đó, đôi khi, tôi cần thêm một thuộc tính mới có điều kiện. Vì vậy, tôi đã thay đổi nó thành một cái gì đó như thế này:

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

Ở đây, chúng ta có thể sử dụng chạm để xóa biến cục bộ và xóa trả về:

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

1

Trong thế giới mà mô hình lập trình chức năng đang trở thành một thực hành tốt nhất ( https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming ), bạn có thể nhìn thấy tap, như một maptrên một giá trị duy nhất, thực sự , để sửa đổi dữ liệu của bạn trên một chuỗi chuyển đổi.

transformed_array = array.map(&:first_transformation).map(&:second_transformation)

transformed_value = item.tap(&:first_transformation).tap(&:second_transformation)

Không cần khai báo itemnhiều lần ở đây.


0

Sự khác biệt là gì?

Sự khác biệt về khả năng đọc mã hoàn toàn là phong cách.

Code Dạo qua:

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

Những điểm chính:

  • Lưu ý rằng ubiến bây giờ được sử dụng như tham số khối như thế nào?
  • Sau khi chặn xong, userbiến bây giờ sẽ trỏ đến Người dùng (với tên người dùng: 'foobar' và người cũng được lưu).
  • Nó chỉ dễ chịu và dễ đọc hơn.

Tài liệu API

Đây là một phiên bản mã nguồn dễ đọc:

class Object
  def tap
    yield self
    self
  end
end

Để biết thêm thông tin, hãy xem các liên kết sau:

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

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.