ในโพสต์ของวันนี้ เราจะมาดู #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
ไอเอ็นจีสัตว์ เราหวังว่าคุณจะสนุกกับการดำน้ำลึกเท่าที่เราเขียน