Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyの#dupと#cloneを徹底解説!内部実装から独自実装まで完全ガイド

本記事では、Rubyの#dup#cloneについて詳しく掘り下げていきます。まず、このテーマに興味を持つきっかけとなった実際の開発事例を紹介し、その後、Rubyにおける#dupの内部実装や#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_atnilに設定し忘れたことが何度かありました。

この方法は少々面倒に感じられたため、これがベストなやり方だとは思えませんでした。そして実際、より良い方法が存在したのです!

new_campaign = Campaign.find(1).dup
new_campaign.title = "Campaign 2019"
new_campaign.save!

このコードを実行すると、IDとタイムスタンプが自動的にnilに設定されます。まさに私たちがやりたかったことそのものです。

これが私が#dupを使い始めたきっかけです。次に、#dupが実際にどのように機能しているのか、さらに深く見ていきましょう。

内部では何が起きているのか?

Rubyの#dupメソッドのデフォルト実装では、オブジェクトに特殊なイニシャライザを追加できます。これは#dupメソッド経由でオブジェクトが初期化されたときにのみ呼び出されるものです。対象となるメソッドは以下の2つです:

  • 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がイニシャライザメソッド#initialize_dupを呼び出しています。

rb_funcallはRubyのCコードで頻繁に使われる関数で、オブジェクトに対してメソッドを呼び出すために使用されます。この場合、dupオブジェクトに対してid_init_dupを呼び出しています。引数の1は引数の数を示しており、この場合はobjの1つだけです。

もう少し深く潜って、その実装を見てみましょう:

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におけるdupの実装

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

タイムスタンプはTimestampsモジュールでクリアされます。これにより、Railsがレコードの作成・更新時に使用するすべてのタイムスタンプ(created_atcreated_onupdated_atupdated_on)がクリアされます。

# activerecord/lib/active_record/timestamp.rb
def initialize_dup(other) # :nodoc:
  super
  clear_timestamp_attributes
end

ここで興味深いのは、Railsが#initialize_copyではなく#initialize_dupをあえてオーバーライドしている点です。なぜでしょうか?調べてみましょう。

Object#initialize_copyの解説

上記のコードスニペットでは、.dupを使用するとRubyが#initialize_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を呼び出します。もし#initialize_dup内でsuperを呼び出さなければ、initialize_copyは決して呼ばれないため、superを残しておくことが重要です。

他にもオブジェクトをコピーする方法はある?

この実装を見て、「なぜ#initialize_*系メソッドが2種類もあるのか?」と疑問に思った方もいるでしょう。答えは、#cloneという別のコピー方法が存在するからです。一般に、オブジェクトの内部状態ごと複製したい場合は#cloneを使用します。

ActiveRecordにおけるRailsの#dupメソッドはまさにこれを利用しています。Railsは#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というクラスがあり、コピー方法によって異なる振る舞いをするように設計されています:

  • クローン(clone)した場合:DNAは元のまま維持され、名前は元の名前に「2」を付け加えたものになります。
  • 複製(dup)した場合:元の動物をベースに新しい動物が作られ、固有のDNAと新しい名前が与えられます。
  • 共通の挙動:いずれの場合も、動物は赤ちゃん(age = 0)の状態から始まります。

この挙動を実現するために、3つの異なるイニシャライザを実装しました。#initialize_(dup|clone)メソッドは必ず#initialize_copyを呼び出すため、ageが0に設定されることが保証されます。

まとめ:クローンたちとその他の動物たち

まず解決したい課題の説明から始まり、データベースレコードのコピーについて考察しました。Campaignの例での手動コピーから#dup#cloneへの移行を確認し、その後は実践的な話題からRubyの内部実装という興味深い世界へと踏み込みました。さらに、動物の#clone#dupでも遊んでみました。この深掘り記事を書いた私たちと同じくらい、読者の皆様にも楽しんでいただけたなら幸いです。

  1. RuboCopで実現するRubyコードのリントと自動フォーマット徹底ガイド

    リンティング(Lint)とは、ソースコードに含まれるプログラム上の誤りやスタイル違反を自動的に検出する仕組みです。このチェックは「リンター」と呼ばれる静的コード解析ツールによって行われます。一方、コードフォーマッターは、あらかじめ設定されたルールセットにソースコードを厳密に準拠させることに特化したツールです。一般的にリンターは違反を報告するだけで、実際の修正はプログラマーの手に委ねられます。それに対してコードフォーマッターは、ルールを直接ソースコードに適用し、フォーマットのミスを自動的に修正してくれます。 プロジェクトで一貫性のあるコードスタイルを実現するには、通常リンティング用とフォーマッ

  2. Rubyでのログ出力をマスターする:LoggerとLogrageの使い方徹底解説

    Rubyでのログ出力入門:LoggerとLogrageの使い方 ロギングは、アプリケーション開発において最も重要なタスクの一つです。ログは以下のような場面で活用されます。 アプリ内部で何が起きているかを把握したいとき アプリケーションを監視したいとき 特定のデータに関するメトリクスを収集したいとき 新しいプログラミング言語を学ぶ際、最初に選ばれるのはその言語がネイティブに備えているロギング機構でしょう。標準機能は通常、扱いやすく、ドキュメントも充実しており、コミュニティでも広く使われています。 ただし、ログデータの内容や扱い方は、企業の方針、ビジネスの性質、アプリケーションの種類に