Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm chủ Ruby: Sâu vào #dup vs #clone

Trong bài hôm nay, chúng ta sẽ nhìn vào #dup và #clone của Ruby. Chúng ta sẽ bắt đầu với ví dụ thực tế kích hoạt sự quan tâm này. Sau đó, chúng ta sẽ đi sâu với mục tiêu học cách #dup được triển khai trong Ruby và so sánh với #clone. Chúng ta sẽ kết thúc bằng việc triển khai #dup của chính chúng ta. Cùng đi!

Tôi Bắt đầu Dùng Dup Như Thế Nào

Khi tôi làm ở công ty chuyên setup campaign cho NGO để thu quyên góp, tôi thường phải copy campaign và tạo cái mới. Ví dụ, sau khi campaign 2018 kết thúc, campaign mới cho 2019 cần thiết.

Một campaign thường có rất nhiều option cấu hình, tôi không thực sự muốn setup lại. Nó tốn khá nhiều thời gian và dễ sai. Vì vậy tôi bắt đầu bằng cách copy DB record và đi từ đó.

Với vài campaign đầu, tôi thực sự copy thủ công. Nó trông như thế này:

current_campaign = Campaign.find(1) new_campaign = current_campaign new_campaign.id = nil new_campaign.created_at = nil new_campaign.updated_at = nil new_campaign.title = "Campaign 2019" new_campaign.save!

Điều này hoạt động, nhưng đòi hỏi nhiều typing, không kể dễ sai. Tôi đã quên set created_at về nil vài lần.

Vì cảm thấy hơi đau khổ, tôi không thể tưởng tượng đó là cách tốt nhất. Và ra là có cách tốt hơn!

new_campaign = Campaign.find(1).dup new_campaign.title = "Campaign 2019" new_campaign.save!

Điều này sẽ set ID và timestamps về nil, chính xác điều chúng ta muốn.

Đó là cách tôi lần đầu dùng #dup. Bây giờ, hãy đi sâu xem #dup thực sự hoạt động như thế nào.

Đang Xảy Ra Gì Dưới Mui Xe?

Triển khai mặc định của Ruby cho phương thức #dup cho phép bạn thêm một initializer đặc biệt cho object chỉ được gọi khi object được khởi tạo qua phương thức #dup. Các phương thức này là:

  • initialize_copy
  • initialize_dup

Triển khai của các phương thức này thực sự thú vị, vì chúng không làm gì mặc định. Chúng về cơ bản là placeholder để bạn ghi đè.

Đây là lấy trực tiếp từ Ruby source code:

VALUE rb_obj_dup(VALUE obj) { VALUE dup; if (special_object_p(obj)) { return obj; } dup = rb_obj_alloc(rb_obj_class(obj)); init_copy(dup, obj); rb_funcall(dup, id_init_dup, 1, obj); return dup; }

Đối với chúng ta, phần thú vị là dòng 11 nơi Ruby gọi phương thức initializer #initialize_dup.

rb_funcall là function dùng nhiều trong Ruby C code. Nó dùng để gọi phương thức trên object. Trong trường hợp này nó sẽ gọi id_init_dup trên object dup. Con số 1 cho biết số lượng argument, trong trường hợp này chỉ một: obj

Hãy đi sâu hơn và nhìn triển khai đó:

VALUE rb_obj_init_dup_clone(VALUE obj, VALUE orig) { rb_funcall(obj, id_init_copy, 1, orig); return obj; }

Như bạn thấy, không có gì đặc biệt xảy ra ngoài việc nó gọi id_init_copy. Bây giờ chúng ta đã xuống hố thỏ, hãy nhìn phương thức đó nữa:

VALUE rb_obj_init_copy(VALUE obj, VALUE orig) { if (obj == orig) return obj; rb_check_frozen(obj); rb_check_trusted(obj); if (TYPE(obj) != TYPE(orig) || rb_obj_class(obj) != rb_obj_class(orig)) { rb_raise(rb_eTypeError, "initialize_copy should take same class object"); } return obj; }

Dù có nhiều code hơn, không có gì đặc biệt ngoài một số check cần thiết nội bộ (nhưng có thể là chủ đề tốt cho lần sau).

Vậy điều xảy ra trong triển khai là Ruby cho bạn endpoint và cung cấp tools cần thiết để triển khai hành vi thú vị của riêng bạn.

Triển khai Dup của Rails

Đây chính xác là những gì Rails làm ở một vài nơi, nhưng hiện tại chúng ta chỉ quan tâm đến cách ID và timestamp fields bị clear.

ID bị clear trong core module cho ActiveRecord. Nó tính đến primary key của bạn, nên dù bạn đổi nó, nó vẫn reset.

# activerecord/lib/active_record/core.rb def initialize_dup(other) # :nodoc: @attributes = @attributes.deep_dup @attributes.reset(self.class.primary_key) _run_initialize_callbacks @new_record = true @destroyed = false @_start_transaction_state = {} @transaction_state = nil super end

Timestamps bị clear trong module Timestamps. Nó nói với Rails clear tất cả timestamp mà Rails dùng cho tạo và update (created_at, created_on, updated_at và updated_on).

# activerecord/lib/active_record/timestamp.rb def initialize_dup(other) # :nodoc: super clear_timestamp_attributes end

Một fact thú vị ở đây là Rails cố ý ghi đè #initialize_dup thay vì #initialize_copy. Tại sao? Hãy điều tra.

Object#initialize_copy Giải thích

Trong các code snippet trên, chúng ta thấy Ruby gọi #initialize_dup khi bạn dùng .dup trên method. Nhưng cũng có phương thức #initialize_copy. Để giải thích tốt hơn nơi nó dùng, hãy xem ví dụ:

class Animal attr_accessor :name def initialize_copy(*args) puts "#initialize_copy is called" super end def initialize_dup(*args) puts "#initialize_dup is called" super end end animal = Animal.new animal.dup # => #initialize_dup is called # => #initialize_copy is called

Chúng ta bây giờ thấy thứ tự gọi. Ruby gọi trước #initialize_dup rồi gọi #initialize_copy. Nếu chúng ta bỏ gọi super khỏi #initialize_dup, chúng ta sẽ không bao giờ gọi initialize_copy, nên quan trọng phải giữ nó.

Có Phương thức Khác để Copy Không?

Bây giờ chúng ta đã thấy triển khai này, bạn có thể thắc mắc use case của hai phương thức #initialize_* là gì. Câu trả lời: có cách khác copy object, gọi là #clone. Bạn thường dùng #clone nếu muốn copy object bao gồm internal state của nó.

Đây là những gì Rails dùng với phương thức #dup trên ActiveRecord. Nó dùng #dup để cho phép duplicate record không có "internal" state (id và timestamps), và để #clone cho Ruby triển khai.

Có thêm phương thức này cũng yêu cầu initializer cụ thể khi dùng #clone. Cho việc này, bạn có thể ghi đè #initialize_clone. Phương thức này dùng vòng đời giống #initialize_dup và sẽ gọi lên #initialize_copy.

Biết điều này, việc đặt tên các initializer method có ý nghĩa hơn. Chúng ta có thể dùng #initialize_(dup|clone) cho triển khai cụ thể tùy bạn dùng #dup hay #clone. Nếu có hành vi chung cho cả hai, bạn có thể đặt trong #initialize_copy.

Clone một Con Vật

(chỉ ví dụ, không con vật nào bị đau cho bài blog này)

Bây giờ hãy xem ví dụ cách nó hoạt động thực tế.

class Animal attr_accessor :name, :dna, :age def initialize self.dna = generate_dna end def initialize_copy(original_animal) self.age = 0 super end def initialize_dup(original_animal) self.dna = generate_dna self.name = "A new name" super end def initialize_clone(original_animal) self.name = "#{original_animal.name} 2" super end def generate_dna SecureRandom.hex end end bello = Animal.new bello.name = "Bello" bello.age = 10 bello_clone = bello.clone bello_dup = bello.dup bello_clone.name # => "Bello 2" bello_clone.age # => 0 bello_dup.name # => "A new name" bello_dup.age # => 0

Hãy phân tích điều thực sự xảy ra. Chúng ta có class Animal, và tùy cách copy animal, nó nên có hành vi khác:

  • Khi clone animal, DNA giữ nguyên, tên sẽ là tên gốc với "2" appended.
  • Khi duplicate animal, chúng ta tạo animal mới dựa trên gốc. Nó có DNA riêng và tên mới.
  • Trong mọi trường hợp animal bắt đầu như baby.

Chúng ta triển khai ba initializer khác nhau để làm điều này. #initialize_(dup|clone) sẽ luôn gọi lên #initialize_copy, đảm bảo age được set về 0.

Tổng hợp CLONES và Các Con Vật Khác

Bắt đầu bằng giải thích "ngứa" chúng ta cần xoa, chúng ta nhìn vào copy database record. Chúng ta đi từ copy thủ công trong ví dụ Campaign, đến #dup và #clone. Chúng ta sau đó đưa từ thực tế đến thú vị và nhìn vào cách triển khai trong Ruby. Chúng ta cũng chơi với #cloneing và #duping animals. Hy vọng bạn enjoy deep dive này như chúng ta viết nó.