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

Rubyのオブジェクトマーシャリング入門:Marshalモジュールと_dump/_loadメソッドの使い方

本記事では、Rubyにおけるオブジェクトマーシャリング(Object Marshalling)について詳しく解説します。まず、マーシャリングが何であるかを説明し、標準ライブラリのMarshalモジュールの基本的な使い方を紹介します。その後、実際のコード例を通して理解を深め、さらに一歩踏み込んで_dumpメソッドとself._loadメソッドの違いや挙動まで掘り下げていきます。それでは始めましょう!

オブジェクトマーシャリングとは?

プログラムを書いていると、あるオブジェクトを保存しておき、別のプログラムへ受け渡したり、次回のプログラム実行時に再利用したりしたい場面があります。

身近な例としてSidekiqが挙げられます。Ruby on RailsアプリケーションでSidekiqジョブをエンキューするとき、そのジョブ(実態は単なるオブジェクト)はシリアライズされてRedisに格納されます。その後、Sidekiqのワーカープロセスがこのシリアライズ済みデータをデシリアライズし、元のジョブを再構築する仕組みです。

コンピュータープログラミングにおいて、このようなオブジェクトのシリアライズ(直列化)とデシリアライズ(復元)の一連の処理を、一般的に「オブジェクトマーシャリング」と呼びます。それでは、Rubyがネイティブに提供しているマーシャリングの仕組みを見ていきましょう。

Marshalモジュール

Rubyは完全なオブジェクト指向言語であり、標準ライブラリのMarshalモジュールを使うことで、オブジェクトをシリアライズして保存できます。Marshalを使うと、オブジェクトをバイトストリームに変換でき、そのストリームは別のRubyプロセスで保存・復元することが可能です。

それでは、文字列をシリアライズし、シリアライズされたデータの中身を詳しく見てみましょう。

hello_world = 'hello world!'
 
serialized_string = Marshal.dump(hello_world) # => "\x04\bI"\x11hello world!\x06:\x06ET"
serialized_string.class                       # => String
 
deserialized_hello_world = Marshal.load(serialized_string) # => "hello world!"
 
hello_world.object_id              # => 70204420126020
deserialized_hello_world.object_id # => 70204419825700

Marshal.dumpモジュールメソッドを呼び出すことで文字列をシリアライズしています。戻り値にはシリアライズ済みのデータが含まれており、ここではserialized_string変数に格納しています。このデータはファイルに保存でき、そのファイルを別プロセスで読み込めば元のオブジェクトを再構築できます。続いてMarshal.loadメソッドを呼び出すことで、バイトストリームから元のオブジェクトを復元しています。

注目すべき点として、復元されたばかりの文字列は、元のhello_world文字列と異なるobject_idを持っています。つまり別のオブジェクトですが、中身のデータは同一です。

便利ですね。では、Marshalモジュールはどのようにして文字列を再構築できるのでしょうか? また、どの属性をシリアライズ/デシリアライズするかを自分で制御したい場合はどうすればよいのでしょうか?

オブジェクトマーシャリングの具体例

これらの疑問に答えるために、Userというカスタム構造体に対して独自のマーシャリング戦略を実装してみましょう。

User = Struct.new(:fullname, :age, :roles)
 
user = User.new('Mehdi Farsi', 42, [:admin, :operator])

User構造体はfullnameagerolesという3つの属性を持っています。この例では、以下の条件を満たす場合にのみシリアライズを行うというビジネスルールがあるものとします。

  • fullnameが64文字未満であること
  • roles配列に:adminロールが含まれていないこと

これを実現するには、独自のシリアライズ戦略を実装したUser#marshal_dumpメソッドを定義します。このメソッドは、User構造体のインスタンスを引数としてMarshal.dumpを呼び出した際に自動的に呼び出されます。実際に定義してみましょう。

User = Struct.new(:age, :fullname, :roles) do
  def marshal_dump
    {}.tap do |result|
      result[:age]      = age
      result[:fullname] = fullname if fullname.size <= 64
      result[:roles]    = roles unless roles.include? :admin
    end
  end
end
 
user = User.new(42, 'Mehdi Farsi', [:admin, :operator])
 
user_dump = Marshal.dump(user) # 'in User#marshal_dump'
user_dump                      # => "\x04\bU:\tUser{\a:\bageI"\x10Mehdi Farsi\x06:\x06ET:\rfullnamei/"

上記の例では、Marshal.dump(user)を呼び出すとUser#marshal_dumpメソッドが実行されていることがわかります。user_dump変数には、Userインスタンスをシリアライズした結果の文字列が格納されています。

次に、このダンプデータをデシリアライズしてユーザーを復元しましょう。そのために、Userのダンプデータに対するデシリアライズ戦略を担当するUser#marshal_loadメソッドを定義します。

User = Struct.new(:age, :fullname, :roles) do
  def marshal_dump
    {}.tap do |result|
      result[:age]      = age
      result[:fullname] = fullname if fullname.size <= 64
      result[:roles]    = roles unless roles.include? :admin
    end
  end
 
  def marshal_load(serialized_user)
    self.age      = serialized_user[:age]
    self.fullname = serialized_user[:fullname]
    self.roles    = serialized_user[:roles] || []
  end
end
 
user = User.new(42, 'Mehdi Farsi', [:admin, :operator])
 
user_dump = Marshal.dump(user) # 'in User#marshal_dump'
user_dump                      # => "\x04\bU:\tUser{\a:\bagei/:\rfullnameI"\x10Mehdi Farsi\x06:\x06ET"
 
original_user = Marshal.load(user_dump)  # 'in User#marshal_load'
original_user                            # => #<struct User age=42, fullname="Mehdi Farsi", roles=[]>

上記の例でも、Marshal.load(user_dump)を呼び出すとUser#marshal_loadメソッドが実行されていることが確認できます。original_user変数には、ユーザーインスタンスを復元した結果の構造体が格納されています。

注目したいのは、original_user.rolesが元のuser.roles配列と一致していない点です。これは、シリアライズ時点でuser.roles:adminロールが含まれていたため、ビジネスルールによりuser.rolesuser_dumpにシリアライズされなかったからです。

_dumpメソッドとself._loadメソッド

Marshal.dumpMarshal.loadが呼び出されると、引数として渡されたオブジェクト上のmarshal_dumpおよびmarshal_loadメソッドが呼び出されることは、すでに見たとおりです。

しかし実は、Marshal.dumpMarshal.loadは、引数のオブジェクトに対して_dumpおよびself._loadという別の2つのメソッドも呼び出そうとします。ここからは、これらのメソッドについて詳しく見ていきましょう。

_dumpメソッド

marshal_dumpメソッドと_dumpメソッドの主な違いは次のとおりです。

  • _dumpメソッドを使う場合は、より低レベルでシリアライズ戦略を扱う必要があります。具体的には、シリアライズ対象のデータを表す文字列を自ら返却しなければなりません。
  • 両方が定義されている場合、marshal_dumpメソッドが_dumpメソッドより優先されます。

次の例を見てみましょう。

User = Struct.new(:age, :fullname, :roles) do
  def _dump level
    [age, fullname].join(':')
  end
end
 
user = User.new(42, 'Mehdi Farsi', [:admin, :operator])
 
Marshal.dump(user) # => "\x04\bIu:\tUser\x1342:Mehdi Farsi\x06:\x06EF"

User#_dumpメソッド内では、シリアライズ結果となるオブジェクト(ここではシリアライズ内容を表す文字列)を自分で生成して返す必要があります。

次の例では、User#marshal_dumpUser#_dumpの両方を定義し、それぞれ識別用の文字列を返すことで、どちらのメソッドが呼び出されるかを確認します。

User = Struct.new(:age, :fullname, :roles) do
  def marshal_dump
    'in User#marshal_dump'
  end
 
  def _dump level
    'in User#_dump'
  end
end
 
user = User.new(42, 'Mehdi Farsi', [:admin, :operator])
 
user_dump = Marshal.dump(user) # "in User#marshal_dump"

両方定義されているにもかかわらず、実際に呼び出されたのはUser#marshal_dumpだけです。優先順位の違いが確認できましたね。

self._loadメソッド

続いて、marshal_loadメソッドと_loadメソッドを比較してみましょう。両者の主な違いは次のとおりです。

  • _loadメソッドを使う場合は、より低レベルでデシリアライズ戦略を扱う必要があり、元のオブジェクトのインスタンス化まで自分で行う責任を負います。
  • marshal_loadメソッドはデシリアライズ済みオブジェクトを引数に取るのに対し、self._loadメソッドはシリアライズ済み文字列を引数に取ります。
  • marshal_loadはインスタンスメソッドである一方、self._loadはクラスメソッドです。

実際の例を見てみましょう。

User = Struct.new(:age, :fullname, :roles) do
  def _dump level
    [age, fullname].join(':')
  end
 
  def self._load serialized_user
    user_info = serialized_user.split(':')
    new(*user_info, Array.new)
  end
end
 
user = User.new(42, 'Mehdi Farsi', [:admin, :operator])
 
user_dump = Marshal.dump(user)
user_dump # => "\x04\bIu:\tUser\x1342:Mehdi Farsi\x06:\x06EF"
 
original_user = Marshal.load(user_dump)
original_user # => #<struct User age="Mehdi Farsi", fullname=42, roles=[]>

User._loadメソッド内では、次の処理を行っています。

  • User#_dumpメソッドが返した文字列をデシリアライズする
  • デシリアライズした情報を渡して新しいUserインスタンスを生成する

このように、_loadを使う場合は、元のユーザーを復元するためのオブジェクトの割り当てとインスタンス化を自分で担うことになります。

一方、marshal_loadと組み合わせたMarshal.loadの場合は、復元対象オブジェクトのインスタンス化はRuby側が自動的に行ってくれます。その後、新しくインスタンス化されたオブジェクトに対して、シリアライズ済みオブジェクトを引数としてmarshal_loadメソッドが呼び出されます。

逆に、_loadと組み合わせたMarshal.loadの呼び出しでは、self._loadクラスメソッドが以下の2つの役割を担います。

  • _dumpメソッドが返したデータのデシリアライズ
  • 復元対象オブジェクトのインスタンス化

まとめ

要件に応じて、より高レベルまたは低レベルのシリアライズ/デシリアライズ戦略を選択して実装することができます。その際は、Marshalモジュールを適切なフックメソッド(marshal_dump/marshal_load、または_dump/self._load)と組み合わせて活用しましょう。

以上です。Voilà!

  1. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端

  2. Rubyのfreezeメソッド完全解説 – オブジェクトの可変性と不変性を理解しよう

    オブジェクトが「変更可能(ミュータブル)」であるとは、どういう意味なのでしょうか? 難しい言葉に構える必要はありません。「可変性(ミュータビリティ)」とは、単純に「オブジェクトの内部状態を後から変更できる」という意味です。これはすべてのオブジェクトのデフォルトの挙動であり、freeze(凍結)されたオブジェクトや、言語側で特別扱いされている一部のオブジェクトだけが例外となります。 つまり、Rubyのすべてのオブジェクトが変更可能というわけではないのです。 なぜ数値やシンボルは変更できないのか? たとえば、整数・シンボル、さらにはtrueやfalse(これらもすべてオブジェクトです)が変化するの