Trong bài viết hôm nay, chúng ta sẽ xem xét #dup
của Ruby và #clone
. Chúng tôi sẽ bắt đầu với một ví dụ thực tế đã kích hoạt sự quan tâm này. Sau đó, chúng ta sẽ đi sâu hơn với mục tiêu tìm hiểu cách thực hiện #dup
được triển khai trong Ruby và cách nó so sánh với #clone
. Sau đó, chúng tôi sẽ kết thúc bằng cách triển khai #dup
của riêng mình phương pháp. Đi thôi!
Cách tôi bắt đầu sử dụng bản sao
Khi tôi làm việc tại một công ty chuyên thiết lập các chiến dịch cho các tổ chức phi chính phủ để quyên góp, tôi thường xuyên phải sao chép các chiến dịch và tạo các chiến dịch mới. Ví dụ:sau khi chiến dịch năm 2018 kết thúc, chiến dịch mới cho năm 2019 là cần thiết.
Một chiến dịch thường có vô số tùy chọn cấu hình, mà tôi không thực sự cảm thấy muốn thiết lập lại. Nó sẽ mất khá nhiều thời gian và dễ xảy ra lỗi. Vì vậy, tôi bắt đầu bằng cách sao chép bản ghi DB và bắt đầu từ đó.
Đối với một vài chiến dịch đầu tiên, tôi thực sự đã sao chép nó bằng tay. Nó trông giố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 phải gõ rất nhiều, chưa kể nó là dễ xảy ra lỗi. Tôi đã quên đặt created_at
thành nil
một vài lần trước đây.
Vì điều này có vẻ hơi đau, tôi không thể tưởng tượng rằng đó là cách tốt nhất để tiếp tục. Và hóa ra, có một cách tốt hơn!
new_campaign = Campaign.find(1).dup
new_campaign.title = "Campaign 2019"
new_campaign.save!
Thao tác này sẽ đặt ID và dấu thời gian thành nil
, đó chính xác là những gì chúng tôi muốn đạt được.
Đây là cách lần đầu tiên tôi sử dụng #dup
. Bây giờ, chúng ta hãy đi tìm hiểu sâu hơn về cách #dup
thực sự hoạt động.
Chuyện gì đang xảy ra dưới mái che?
Việc triển khai Ruby mặc định của #dup
phương thức này cho phép bạn thêm một trình khởi tạo đặc biệt vào đối tượng của mình, đối tượng này chỉ được gọi khi một đối tượng được khởi tạo thông qua #dup
phương pháp. Các phương pháp này là:
-
initialize_copy
-
initialize_dup
Việc thực hiện các phương pháp này thực sự khá thú vị, vì chúng không làm bất cứ điều gì theo mặc định. Về cơ bản, chúng là trình giữ chỗ để bạn ghi đè.
Điều này được lấy trực tiếp từ mã nguồn 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;
}
Đối với chúng tôi, phần thú vị nằm ở dòng 11 nơi Ruby gọi phương thức khởi tạo #intialize_dup
.
rb_funcall
là một hàm được sử dụng rất nhiều trong mã ruby C. Nó được sử dụng để gọi các phương thức trên một đối tượng. Trong trường hợp này, nó sẽ gọiid_init_dup
trêndup
sự vật.1
cho biết có bao nhiêu đối số, trong trường hợp này chỉ có một đối số:obj
Hãy đi sâu hơn một chút và xem xét cách 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 có thể thấy trong ví dụ này, không có gì thực sự xảy ra ngoài việc nó gọi id_init_copy
. Bây giờ chúng ta đã xuống lỗ thỏ, hãy cũng xem xét phương pháp đó:
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;
}
Mặc dù có nhiều mã hơn nhưng không có gì đặc biệt xảy ra ngoại trừ một số kiểm tra được yêu cầu nội bộ (nhưng đó có thể là một chủ đề tốt cho thời gian khác).
Vì vậy, những gì xảy ra trong quá trình triển khai là Ruby cung cấp cho bạn một điểm cuối và cung cấp cho bạn các công cụ cần thiết để thực hiện hành vi thú vị của riêng bạn.
Triển khai trùng lặp của Rails
Đây chính xác là những gì Rails đã làm ở nhiều nơi, nhưng hiện tại, chúng tôi chỉ quan tâm đến cách id
và các trường dấu thời gian bị xóa.
ID được xóa trong mô-đun cốt lõi cho ActiveRecord. Nó tính đến khóa chính của bạn là gì, vì vậy ngay cả khi bạn đã thay đổi khóa đó, nó vẫn sẽ đặt lại khóa đó.
# 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
Dấu thời gian được xóa trong mô-đun Dấu thời gian. Nó yêu cầu Rails xóa tất cả các dấu thời gian mà Rails có thể sử dụng để tạo và cập nhật (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 sự thật thú vị ở đây là Rails đã cố tình chọn ghi đè #initialize_dup
thay vì #initialize_copy
phương pháp. Tại sao bạn đã làm được điều đó? Hãy điều tra.
Đối tượng # khởi tạo_copy được giải thích
Trong các đoạn mã trên, chúng ta đã thấy cách Ruby gọi #initialize_dup
khi bạn sử dụng .dup
trên một phương pháp. Nhưng cũng có #initialize_copy
phương pháp. Để giải thích rõ hơn điều này được sử dụng ở đâu, hãy xem một 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
Bây giờ chúng ta có thể thấy thứ tự gọi là gì. Đầu tiên Ruby gọi đến #initialize_dup
và sau đó gọi đến #initialize_copy
. Nếu chúng tôi giữ cuộc gọi đến super
ra khỏi #initialize_dup
, chúng tôi sẽ không bao giờ gọi initialize_copy
, vì vậy điều quan trọng là phải giữ được điều đó.
Có các phương pháp khác để sao chép thứ gì đó không?
Bây giờ chúng ta đã thấy triển khai này, bạn có thể tự hỏi trường hợp sử dụng là gì khi có hai #initialize_*
các phương pháp. Câu trả lời là:có một cách khác để sao chép các đối tượng, được gọi là #clone
. Bạn thường sử dụng #clone
nếu bạn muốn sao chép một đối tượng bao gồm cả trạng thái bên trong của nó.
Đây là những gì Rails đang sử dụng với #dup
của nó trên ActiveRecord. Nó sử dụng #dup
để cho phép bạn sao chép bản ghi mà không có trạng thái "nội bộ" (id và dấu thời gian) và để lại #clone
lên đến Ruby để triển khai.
Có thêm phương thức này cũng yêu cầu một trình khởi tạo cụ thể khi sử dụng #clone
phương pháp. Đối với điều này, bạn có thể ghi đè #initialize_clone
. Phương pháp này sử dụng cùng một vòng đời với #initialize_dup
và sẽ gọi tới #initialize_copy
.
Biết được điều này, việc đặt tên cho các phương thức khởi tạo có ý nghĩa hơn một chút. Chúng tôi có thể sử dụng #initialize_(dup|clone)
để triển khai cụ thể tùy thuộc vào việc bạn sử dụng #dup
hoặc #clone
. Nếu chúng tôi có hành vi bao quát được sử dụng cho cả hai, bạn có thể đặt nó bên trong #initialize_copy
.
Nhân bản một con vật
(chỉ là một ví dụ, không có động vật nào bị thương cho bài đăng trên blog này)
Bây giờ chúng ta hãy xem xét một ví dụ về cách nó hoạt động trong 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 chia nhỏ những gì đang thực sự xảy ra ở đây. Chúng ta có một lớp gọi là Animal
và tùy thuộc vào cách chúng tôi sao chép con vật, nó sẽ có các hành vi khác nhau:
- Khi chúng tôi nhân bản con vật, DNA vẫn giữ nguyên và tên của nó sẽ là tên ban đầu với 2 tên được thêm vào.
- Khi chúng tôi nhân bản con vật, chúng tôi tạo một con vật mới dựa trên con vật gốc. Nó có DNA riêng và một cái tên mới.
- Trong mọi trường hợp, con vật bắt đầu từ khi còn nhỏ.
Chúng tôi đã triển khai ba trình khởi tạo khác nhau để làm cho điều này xảy ra. #initialize_(dup|clone)
phương thức sẽ luôn gọi đến #initialize_copy
, do đó đảm bảo rằng tuổi được đặt thành 0.
Làm tròn các CLONES và các động vật khác
Bắt đầu bằng cách giải thích vết ngứa mà chúng tôi cần tự gãi, chúng tôi đã xem xét việc sao chép một bản ghi cơ sở dữ liệu. Chúng tôi đã chuyển từ việc sao chép thủ công trong ví dụ Chiến dịch sang #dup
và #clone
. Sau đó, chúng tôi đưa nó từ thực tế đến hấp dẫn và xem xét cách điều này được thực hiện trong Ruby. Chúng tôi cũng đã chơi với #clone
ing và #dup
động vật ing. Chúng tôi hy vọng bạn sẽ thích chuyến đi sâu của chúng tôi nhiều như chúng tôi đã viết.