Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby의 #dup 및 #clone 자세히 알아보기

오늘 포스팅에서는 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_atupdated_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 잉 동물. 우리가 이 글을 작성하는 것만큼 당신이 우리의 심층 분석을 즐겼기를 바랍니다.