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

ดำดิ่งสู่ #dup และ #clone . ของ Ruby

ในโพสต์ของวันนี้ เราจะมาดู #dup . ของ Ruby และ #clone . เราจะเริ่มต้นด้วยตัวอย่างในชีวิตจริงที่กระตุ้นความสนใจนี้ หลังจากนั้น เราจะเจาะลึกโดยมีเป้าหมายเพื่อเรียนรู้ว่า #dup มีการใช้งานใน Ruby และเปรียบเทียบกับ #clone . จากนั้นเราจะปิดโดยใช้ #dup . ของเราเอง กระบวนการ. ไปกันเถอะ!

ฉันเริ่มใช้ Dup ได้อย่างไร

เมื่อฉันทำงานในบริษัทที่เชี่ยวชาญในการจัดทำแคมเปญสำหรับ NGO เพื่อรวบรวมเงินบริจาค ฉันต้องคัดลอกแคมเปญและสร้างแคมเปญใหม่เป็นประจำ ตัวอย่างเช่น หลังจากสิ้นสุดแคมเปญ 2018 จำเป็นต้องมีแคมเปญใหม่สำหรับปี 2019

แคมเปญมักมีตัวเลือกการกำหนดค่ามากมาย ซึ่งฉันไม่อยากตั้งค่าใหม่เลย จะใช้เวลาค่อนข้างนานและเกิดข้อผิดพลาดได้ง่าย ดังนั้นฉันจึงเริ่มต้นด้วยการคัดลอกบันทึก DB และไปจากที่นั่น

สำหรับแคมเปญแรกๆ นั้น ฉันลอกเลียนมันด้วยมือ หน้าตาประมาณนี้

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 และการประทับเวลาเป็น nil ซึ่งเป็นสิ่งที่เราต้องการทำให้สำเร็จอย่างแท้จริง

นี่เป็นครั้งแรกที่ฉันใช้ #dup . ตอนนี้เรามาดูวิธีการ #dup . กันดีกว่า ใช้งานได้จริง

เกิดอะไรขึ้นภายใต้ประทุน?

การใช้งาน Ruby เริ่มต้นของ #dup method ให้คุณเพิ่ม initializer พิเศษให้กับอ็อบเจ็กต์ของคุณ ซึ่งจะถูกเรียกก็ต่อเมื่ออ็อบเจกต์ถูกเตรียมใช้งานผ่าน #dup กระบวนการ. วิธีการเหล่านี้:

  • initialize_copy
  • initialize_dup

การนำวิธีการเหล่านี้ไปใช้จริงค่อนข้างน่าสนใจ เนื่องจากไม่ได้ดำเนินการใดๆ โดยค่าเริ่มต้น โดยพื้นฐานแล้วจะเป็นตัวยึดตำแหน่งให้คุณแทนที่

สิ่งนี้นำมาโดยตรงจากซอร์สโค้ด Ruby:

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

สำหรับเรา ส่วนที่น่าสนใจอยู่ที่บรรทัดที่ 11 ซึ่ง Ruby เรียกเมธอด initializer #intialize_dup .

rb_funcall เป็นฟังก์ชันที่ใช้ในรหัสทับทิม C เป็นจำนวนมาก มันถูกใช้เพื่อเรียกวิธีการบนวัตถุ ในกรณีนี้จะเรียก id_init_dup บน 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 ให้ปลายทางแก่คุณและมอบเครื่องมือที่จำเป็นในการปรับใช้พฤติกรรมที่น่าสนใจของคุณเอง

การนำ Dup Implementation ของ Rails ไปใช้

นี่คือสิ่งที่ Rails ทำในหลาย ๆ ที่ แต่ตอนนี้เราสนใจแค่ว่า id เป็นอย่างไร และช่องประทับเวลาจะถูกล้าง

ID จะถูกล้างในโมดูลหลักสำหรับ ActiveRecord โดยพิจารณาว่าคีย์หลักของคุณคืออะไร ดังนั้นแม้ว่าคุณจะเปลี่ยนมันก็ยังจะรีเซ็ตอยู่

# 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

การประทับเวลาจะถูกล้างในโมดูลการประทับเวลา มันบอกให้ Rails ล้างการประทับเวลาทั้งหมดที่ Rails สามารถใช้สำหรับการสร้างและอัปเดต (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 ตั้งใจเลือกที่จะแทนที่ #initialize_dup วิธีการแทน #initialize_copy กระบวนการ. ทำไมมันถึงทำอย่างนั้น? มาสำรวจกัน

วัตถุ#initialize_copy อธิบาย

ในข้อมูลโค้ดข้างต้น เราเห็นว่า Ruby เรียก #initialize_dup อย่างไร เมื่อคุณใช้ .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 . ถ้าเราจะเก็บการโทรไปที่ super จาก #initialize_dup เมธอด เราจะไม่มีวันเรียก initialize_copy ดังนั้นจึงเป็นสิ่งสำคัญที่จะรักษาสิ่งนั้นไว้

มีวิธีอื่นในการคัดลอกบางสิ่งหรือไม่

ตอนนี้เราได้เห็นการใช้งานนี้แล้ว คุณอาจสงสัยว่ากรณีการใช้งานที่มี #initialize_* สองตัวคืออะไร วิธีการ คำตอบคือ มีอีกวิธีในการคัดลอกวัตถุที่เรียกว่า #clone . โดยทั่วไปคุณใช้ #clone หากคุณต้องการคัดลอกวัตถุรวมถึงสถานะภายใน

นี่คือสิ่งที่ Rails ใช้กับ #dup วิธีการใน ActiveRecord มันใช้ #dup เพื่อให้คุณสามารถทำซ้ำระเบียนโดยไม่มีสถานะ "ภายใน" (id และการประทับเวลา) และออกจาก #clone จนถึงทับทิมที่จะนำไปใช้

การมีวิธีการพิเศษนี้ยังขอตัวเริ่มต้นเฉพาะเมื่อใช้ #clone กระบวนการ. สำหรับสิ่งนี้ คุณสามารถแทนที่ #initialize_clone . วิธีนี้ใช้วงจรชีวิตเดียวกับ #initialize_dup และจะโทรไปที่ #initialize_copy .

เมื่อทราบสิ่งนี้ การตั้งชื่อเมธอด initializer ก็สมเหตุสมผลขึ้นเล็กน้อย เราสามารถใช้ #initialize_(dup|clone) สำหรับการใช้งานเฉพาะขึ้นอยู่กับว่าคุณใช้ #dup หรือ #clone . หากเรามีพฤติกรรมครอบคลุมที่ใช้สำหรับทั้งคู่ คุณสามารถวางไว้ใน #initialize_copy .

โคลนสัตว์

(เพียงตัวอย่าง ไม่มีสัตว์ได้รับบาดเจ็บสำหรับโพสต์บล็อกนี้)

มาดูตัวอย่างการใช้งานจริงกัน

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 , และขึ้นอยู่กับว่าเราเลียนแบบสัตว์อย่างไร ควรมีพฤติกรรมที่แตกต่างกัน:

  • เมื่อเราโคลนสัตว์ ดีเอ็นเอจะยังคงเหมือนเดิม และชื่อของมันจะเป็นชื่อเดิมโดยมี 2 ตัวต่อท้าย
  • เมื่อเราทำซ้ำสัตว์ เราก็สร้างสัตว์ใหม่โดยยึดตามสัตว์ดั้งเดิม ได้รับ DNA ของตัวเองและชื่อใหม่
  • ในทุกกรณี สัตว์จะเริ่มตั้งแต่ยังเป็นทารก

เราใช้ตัวเริ่มต้นที่แตกต่างกันสามตัวเพื่อทำให้สิ่งนี้เกิดขึ้น #initialize_(dup|clone) เมธอดจะเรียกถึง #initialize_copy . เสมอ ซึ่งทำให้มั่นใจได้ว่าอายุถูกตั้งค่าเป็น 0

รวบรวมโคลนและสัตว์อื่นๆ

เริ่มต้นด้วยการอธิบายอาการคันที่เราจำเป็นต้องเกาเอง เราจึงมองหาการคัดลอกบันทึกฐานข้อมูล เราเปลี่ยนจากการคัดลอกด้วยมือในตัวอย่างแคมเปญเป็น #dup และ #clone . จากนั้นเราก็นำมันจากภาคปฏิบัติไปสู่ความน่าดึงดูดใจ และดูว่าสิ่งนี้ถูกนำมาใช้ใน Ruby อย่างไร เรายังเล่นด้วย #clone ing และ #dup ไอเอ็นจีสัตว์ เราหวังว่าคุณจะสนุกกับการดำน้ำลึกเท่าที่เราเขียน