오늘 포스팅에서는 Ruby의 #dup
에 대해 알아보겠습니다. 및 #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
설정을 잊어버렸습니다. 무
까지 과거에 몇 번.
이것이 약간의 고통처럼 느껴졌기 때문에 그것이 그것에 대해 가는 최선의 방법이라고 상상할 수 없었습니다. 더 나은 방법이 있다는 것이 밝혀졌습니다!
new_campaign = Campaign.find(1).dup
new_campaign.title = "Campaign 2019"
new_campaign.save!
그러면 ID와 타임스탬프가 nil
로 설정됩니다. , 이것이 바로 우리가 달성하고자 하는 것입니다.
이것이 내가 #dup
을(를) 처음 사용하게 된 방법입니다. . 이제 #dup
실제로 작동합니다.
무슨 일입니까?
#dup
의 기본 Ruby 구현 메소드를 사용하면 #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;
}
우리에게 흥미로운 부분은 Ruby가 초기화 메서드 #intialize_dup
를 호출하는 11번째 줄에 있습니다. .
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가 끝점을 제공하고 사용자 고유의 흥미로운 동작을 구현하는 데 필요한 도구를 제공한다는 것입니다.
Rails의 중복 구현
이것이 바로 Rails가 여러 곳에서 했던 일이지만, 지금은 id
가 어떻게 id
및 타임스탬프 필드가 지워집니다.
ActiveRecord용 코어 모듈에서 ID가 지워집니다. 기본 키가 무엇인지 고려하므로 변경하더라도 재설정됩니다.
# 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가 생성 및 업데이트에 사용할 수 있는 모든 타임스탬프를 지우도록 지시합니다(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
대신 메소드 방법. 왜 그럴까요? 조사합시다.
Object#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
을 남깁니다. 구현하려면 Ruby까지.
이 추가 메서드를 사용하면 #clone
을 사용할 때 특정 이니셜라이저도 요청합니다. 방법. 이를 위해 #initialize_clone
을 재정의할 수 있습니다. . 이 메서드는 #initialize_dup
과 동일한 수명 주기를 사용합니다. #initialize_copy
를 호출합니다. .
이것을 알면 이니셜라이저 메서드의 이름을 지정하는 것이 좀 더 이해가 됩니다. #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
이라는 클래스가 있습니다. , 그리고 동물을 복사하는 방법에 따라 다른 행동을 해야 합니다.
- 동물을 복제할 때 DNA는 그대로 유지되며 이름은 원래 이름에 2가 추가됩니다.
- 동물을 복제할 때 원본을 기반으로 새로운 동물을 만듭니다. 고유한 DNA와 새로운 이름을 갖게 됩니다.
- 모든 경우에 동물은 아기로 시작합니다.
이를 위해 세 가지 다른 이니셜라이저를 구현했습니다. #initialize_(dup|clone)
메서드는 항상 #initialize_copy
를 호출합니다. , 따라서 나이가 0으로 설정되도록 합니다.
CLONES 및 기타 동물 반올림
긁어야 할 가려움을 설명하는 것으로 시작하여 데이터베이스 레코드 복사에 대해 살펴보았습니다. 캠페인 예에서 손으로 복사하는 것에서 #dup
으로 이동했습니다. 및 #clone
. 그런 다음 실용적인 것에서 매혹적인 것으로 가져와 이것이 Ruby에서 어떻게 구현되는지 살펴보았습니다. #clone
도 가지고 놀았습니다. ing 및 #dup
잉 동물. 우리가 이 글을 작성하는 것만큼 당신이 우리의 심층 분석을 즐겼기를 바랍니다.