Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Ruby: Deep Dive into #dup vs #clone

ในโพสต์วันนี้ เราจะ看 vào Ruby's #dup และ #clone เราจะเริ่มด้วย real-life example ที่กระตุ้นความสนใจนี้ หลังจากนั้น เราจะดำดิ่งลึกขึ้นเพื่อเรียนรู้ว่า #dup ถูก implement ใน Ruby อย่างไร และมันเปรียบเทียบกับ #clone อย่างไร เราจะปิดท้ายด้วยการ implement #dup method ของตัวเอง มาเริ่มกัน!

How I Started Using Dup

เมื่อผมทำงานที่บริษัทที่專門ตั้งแคมเปญสำหรับ NGOs เพื่อเก็บเงินบริจาค ผมต้อง copy campaigns และสร้างใหม่บ่อยครั้ง ตัวอย่างเช่น หลังจากแคมเปญ 2018 จบ ต้องการแคมเปญใหม่สำหรับ 2019

แคมเป็นปกติมี loads of configuration options ซึ่งผมไม่ค่อยอยาก setup ใหม่ มันจะใช้เวลานานและ error-prone ดังนั้นผมเริ่มด้วยการ copy DB record และดำเนินต่อจากนั้น

สำหรับแคมเป็นแรกๆ ผมจริงๆ copy ด้วยมือ มัน看起來像這樣:

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!

這有效但需要大量打字 更不用說它容易出錯 我過去幾次忘記將 created_at 設為 nil

既然這感覺有點痛苦 我無法想像這是最好的方法 原來 有更好的方法!

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

這將設定 ID 和 timestamps 為 nil ซึ่ง正是我們想達成的

這就是我第一次開始使用 #dup 的方式 現在 讓我們去深入了解 #dup 實際上如何運作

What Is Going on Under the Hood?

Default Ruby implementation ของเมธอด #dup 允許你添加一個特殊的 initializer 到你的對象 只有當對象通過 #dup 方法初始化時才會被調用 這些方法是:

  • initialize_copy
  • initialize_dup

這些方法的實現其實很有趣 因為它們預設不做任何事 它們基本上是 placeholders 供你 override

這直接取自 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;
}

對我們來說 有趣的部分在 line 11 Ruby 呼叫 initializer method #initialize_dup

rb_funcall 是一個在 ruby C code 中大量使用的函數 它用於在對象上調用方法 在這種情況下 它將在 dup 對象上調用 id_init_dup 1 告訴有多少參數 在這種情況下只有一個: obj

讓我們深入一點看那個實現:

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

正如你在這個例子中看到的 除了調用 id_init_copy 之外 沒有發生任何實際的事情 現在我們進入了兔子洞 讓我們也看看那個方法:

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;
}

即使有更多代碼 除了內部所需的一些檢查 (但那可能是另一個時候的好主題) 沒有發生特別的事情

所以在實現中發生的是 Ruby 給你一個 endpoint 並提供所需的工具來實現你自己的有趣行為

Rails' Dup Implementation

這正是 Rails 在一堆地方所做的 但現在我們只關心 id 和 timestamp fields 如何被清除

ID 在 ActiveRecord 的 core module 中被清除 它考慮了你的 primary key 是什麼 所以即使你改變了它 它仍然會 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 在 Timestamps module 中被清除 它告訴 Rails 清除所有 Rails 可以用於 creating 和 updating 的 timestamps (created_at, created_on, updated_at 和 updated_on)

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

這裡一個有趣的事實是 Rails 故意選擇 override #initialize_dup 方法而不是 #initialize_copy 方法 為什麼會這樣? 讓我們調查一下

Object#initialize_copy Explained

在上面的代碼片段中 我們看到 Ruby 如何在你對方法使用 .dup 時調用 #initialize_dup 但也有 #initialize_copy 方法 為了更好地解釋這用在哪裡 讓我們看個例子:

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

我們現在可以看到調用順序 Ruby 首先調用 #initialize_dup 然後調用 #initialize_copy 如果我們在 #initialize_dup 方法中保留對 super 的調用 我們永遠不會調用 initialize_copy 所以保留它很重要

Are There Other Methods to Copy Something?

現在我們看到了這個實現 你可能會想知道有兩個 #initialize_* 方法的用例是什麼 答案是: 還有另一種複製對象的方法 叫做 #clone 你通常使用 #clone 如果你想複製一個對象包括其 internal state

這就是 Rails 用在 ActiveRecord 上的 #dup 方法 它使用 #dup 允許你複製記錄而不帶其 "internal" state (id 和 timestamps) 並將 #clone 留給 Ruby 實現

擁有這個額外的方法也要求一個特定的 initializer 當使用 #clone 方法 對於這個 你可以 override #initialize_clone 此方法使用與 #initialize_dup 相同的 lifecycle 並將調用向上到 #initialize_copy

知道這一點 初始化方法的命名更有意義了 我們可以使用 #initialize_(dup|clone) 為特定實現 取決於你使用 #dup 或 #clone 如果我們有用於兩者的 overarching behavior 你可以將其放在 #initialize_copy 內部

Cloning an Animal

(只是例子 沒有動物受傷為了這篇部落格)

現在讓我們看看實際上如何運作的例子

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

讓我們分解實際上發生了什麼 我們有一個叫 Animal 的類 根據我們如何複製動物 它應該有不同的行為:

  • 當我們 clone 動物 DNA 保持不變 它的名字將是原名加上 2
  • 當我們 duplicate 動物 我們基於原始動物創建一個新動物 它得到自己的 DNA 和新名字
  • 在所有情況下 動物都從嬰兒開始

我們實現了三個不同的 initializers 來實現這一點 #initialize_(dup|clone) 方法將總是調用向上到 #initialize_copy 從而確保 age 被設為 0

Rounding up the CLONES and Other Animals

從解釋我們需要抓癢的地方開始 我們看了複製資料庫記錄 我們從 Campaign 例子中的手動複製 到 #dup 和 #clone 然後我們從實際帶到迷人並看這在 Ruby 中如何實現 我們也玩了 #cloneing 和 #duping 動物 我們希望你享受我們的 deep dive 就像我們寫它時享受一樣