ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

で、シリアライズについて(シリアライズとデシリアライズのはなし)。

In computing, serialization is the process of translating a data structure or object state into a format that can be stored or transmitted and reconstructed later.

The opposite operation, extracting a data structure from a series of bytes, is deserialization.

en.wikipedia.org

開発者ブログの主にネタプログラミング担当、e.mattsan です。

最近は。 開発している Rails アプリケーションがほぼ Web API サーバで、HTML をレンダリングするのではなく JSON にシリアライズするコードを書くことが多くなっています。

シリアライズとは言っても JSON への変換で、送る側も受ける側もただデータの集まりとして扱うので、構造が元のオブジェクトとかけ離れていない限り変換は難しいことはありません。

Wikipedia でシリアライズを調べると、逆操作としてデシリアイズが説明されています。 シリアライズには、単にオブジェクトを保存や送信が可能な状態にするというだけでなく、デシリアイズによって元のオブジェクトを復元できるようにする意味も含まれています。

このようにシリアライズとデシリアライズは、時間や(過去に保存したオブジェクトを取り出す)空間を(別のコンピュータが送信したオブジェクトを受け取る)超えてオブジェクトを伝えることができる手段です。

ところで。 デシリアライズって、意識してやったこと、ありますか?

Ruby でオブジェクトをシリアライズする

まずはシリアライズから始めてみましょう。

Rectangle, Circle, Triangle の三種類の図形クラスを定義して、これらをシリアライズしてみます。

それぞれのクラスには、自分のインスタンスを IO オブジェクトに出力する #write_to を定義しておきます。

以下、行を圧縮したいために綺麗でない書き方をしていますがご容赦を。

class Rectangle
  def initialize(x: 0, y: 0, width: 0, height: 0)
    @x, @y, @width, @height = x, y, width, height
  end

  def write_to(io)
    io.puts ['Rectangle', @x, @y, @width, @height].join(' ')
  end
end

class Circle
  def initialize(x: 0, y: 0, r: 0)
    @x, @y, @r = x, y, r
  end

  def write_to(io)
    io.puts ['Circle', @x, @y, @r].join(' ')
  end
end

class Triangle
  def initialize(x1: 0, y1: 0, x2: 0, y2: 0, x3: 0, y3: 0)
    @x1, @y1, @x2, @y2, @x3, @y3 = x1, y1, x2, y2, x3, y3
  end

  def write_to(io)
    io.puts ['Triangle', @x1, @y1, @x2, @y2, @x3, @y3].join(' ')
  end
end

このコードを shape.rb という名前でファイルに保存し、こんな感じでオブジェクトを出力します。

require_relative './shape'

shapes = []

shapes << Rectangle.new(x: 1, y: 2, width: 20, height: 10)
shapes << Circle.new(x: 1, y: 2, r: 10)
shapes << Triangle.new(x1: 1, y1: 1, x2: 5, y2: 1, x3: 3, y3: 3)

shapes.each do |shape|
  shape.write_to($stdout)
end
Rectangle 1 2 20 10
Circle 1 2 10
Triangle 1 1 5 1 3 3

メモリ上のオブジェクトを、再現に必要な情報をすべて含んだテキストに書き出しました。 ですのでこれも一つのシリアライズです。 少なくとも、人の目には元のオブジェクトを復元するために必要な情報は足りていそうです。

で、どのように復元するとよいでしょうか。

Ruby でオブジェクトをデシリアライズする、その1

文字列を空白で分割してトークンの集まりにして、図形を表す文字列が来たら続く数字を数値として読み込む、という単純なアイデアから始めます。

先の IO に出力したテキストが shapes.dat という名前のファイルに格納されているとして話を進めます。

$ cat shapes.dat
Rectangle 1 2 20 10
Circle 1 2 10
Triangle 1 1 5 1 3 3
require_relative './shape'

tokens = File.read('shapes.dat').split
shapes = []

until tokens.empty?
  case tokens.shift
  when 'Rectangle'
    shapes << Rectangle.new(tokens)

  when 'Circle'
    shapes << Circle.new(tokens)

  when 'Triangle'
    shapes << Triangle.new(tokens)
  end
end

shapes.each do |shape|
  shape.write_to($stdout)
end

各クラスもトークンの集まり tokens から値を読み込めるように拡張します。

class Rectangle
  def initialize(tokens = nil, x: 0, y: 0, width: 0, height: 0)
    @x, @y, @width, @height =
      if tokens.nil?
        [x, y, width, height]
      else
        [tokens.shift.to_i, tokens.shift.to_i, tokens.shift.to_i, tokens.shift.to_i]
      end
  end

  def write_to(io)
    io.puts ['Rectangle', @x, @y, @width, @height].join(' ')
  end
end

class Circle
  def initialize(tokens = nil, x: 0, y: 0, r: 0)
    @x, @y, @r =
      if tokens.nil?
        [x, y, r]
      else
        [tokens.shift.to_i, tokens.shift.to_i, tokens.shift.to_i]
      end
  end

  def write_to(io)
    io.puts ['Circle', @x, @y, @r].join(' ')
  end
end

class Triangle
  def initialize(tokens = nil, x1: 0, y1: 0, x2: 0, y2: 0, x3: 0, y3: 0)
    @x1, @y1, @x2, @y2, @x3, @y3 =
      if tokens.nil?
        [x1, y1, x2, y2, x3, y3]
      else
        [tokens.shift.to_i, tokens.shift.to_i, tokens.shift.to_i, tokens.shift.to_i, tokens.shift.to_i, tokens.shift.to_i]
      end
  end

  def write_to(io)
    io.puts ['Triangle', @x1, @y1, @x2, @y2, @x3, @y3].join(' ')
  end
end

オブジェクトの種類の識別をハードコードした、種類が増えるたびにコードを編集しなければならないよくある例です。

このハードコードした部分をどう解消するかが肝になりそうです。

Ruby でオブジェクトをデシリアライズする、その2

Ruby は、定義されているクラスを実行時に知ることができる言語です。 ですので、どのクラスが必要になるのかをプログラムでいちいち判定しなくても、実行している環境に問い合わせることができます。

Object.const_get('Rectangle')
#=> Rectangle

これでハードコードした部分を置き換えます。

require_relative './shape'

tokens = File.read('shapes.dat').split
shapes = []

until tokens.empty?
  class_name = tokens.shift
  shapes << Object.const_get(class_name).new(tokens)
end

shapes.each do |shape|
  shape.write_to($stdout)
end

便利。

自分が実行されている環境を、実行時に参照できる言語の利点です。

余談 - Ruby のマーシャリングとアンマーシャリング

このようなシリアライズとデシリアライズの機能は、実のところ Ruby にはマーシャリングという名前で標準装備されています。

docs.ruby-lang.org

rectangle = Rectangle.new(x: 1, y: 2, width: 20, height: 10)
#=> #<Rectangle:0x0000000136af1180 @height=10, @width=20, @x=1, @y=2>
data = Marshal.dump(rectangle)
#=> "\x04\bo:\x0ERectangle\t:\a@xi\x06:\a@yi\a:\v@widthi\x19:\f@heighti\x0F"
Marshal.load(data)
#=> #<Rectangle:0x00000001366b3708 @height=10, @width=20, @x=1, @y=2>

マーシャリングされたデータの形式もドキュメントにまとめられています。

docs.ruby-lang.org

dRuby は、マーシャリングの時空を超える能力を利用して実現されています。

docs.ruby-lang.org

C++ でオブジェクトをシリアライズする

かつてわたしが慣れ親しんでいた C++ ではどうでしょうか。

かつては慣れ親しんでいたもののしばらく触れていないため、書いたコードはいささか古臭いコードになっているかもしれません。 ご容赦を。

#ifndef SHAPE_HPP
#define SHAPE_HPP

#include <iosfwd>

class Shape {
public:
    virtual void write_to(std::ostream&) const = 0;
};

class Rectangle : public Shape {
public:
    Rectangle(int x, int y, int width, int height) : x(x), y(y), width(width), height(height) {}

    void write_to(std::ostream& out) const {
        out << "Rectangle " << x << " " << y << " " << width << " " << height << std::endl;
    }

private:
    int x;
    int y;
    int width;
    int height;
};

class Circle : public Shape {
public:
    Circle(int x, int y, int r) : x(x), y(y), r(r) {}

    void write_to(std::ostream& out) const {
        out << "Circle " << x << " " << y << " " << r << std::endl;
    }

private:
    int x;
    int y;
    int r;
};

class Triangle : public Shape {
public:
    Triangle(int x1, int y1, int x2, int y2, int x3, int y3) : x1(x1), y1(y1), x2(x2), y2(y2), x3(x3), y3(y3) {}

    void write_to(std::ostream& out) const {
        out << "Triangle " << x1 << " " << y1 << " " << x2 << " " << y2 << " " << x3 << " " << y3 << std::endl;
    }

private:
    int x1;
    int y1;
    int x2;
    int y2;
    int x3;
    int y3;
};

#endif//SHAPE_HPP

オブジェクトを生成してシリアライズするコードです。

#include <iostream>
#include <memory>
#include "shape.hpp"

typedef std::shared_ptr<Shape> shape_ptr;

int main(int, char**) {
    shape_ptr shapes[] = {
        shape_ptr(new Rectangle(1, 2, 20, 10)),
        shape_ptr(new Circle(1, 2, 10)),
        shape_ptr(new Triangle(1, 1, 5, 1, 3, 3))
    };

    for(shape_ptr* i = std::begin(shapes); i != std::end(shapes); ++i) {
        (*i)->write_to(std::cout);
    }
}

g++ コマンドなどでコンパイルすると実行ファイルが生成されます。 実行すると Ruby で書いたプログラムと同じ結果を出力します。

C++ でオブジェクトをデシリアライズする、その1

Ruby で書いたときと同じように、ハードコードするところから始めてみます。

#include <fstream>
#include <iostream>
#include <memory>
#include <vector>
#include "shape.hpp"

typedef std::shared_ptr<Shape> shape_ptr;
typedef std::vector<shape_ptr> Shapes;

int main(int, char**) {
    Shapes shapes;

    std::ifstream ifs("shapes.dat");

    for (;;) {
        std::string name;
        ifs >> name;
        if (ifs.eof()) {
            break;
        }

        if (name == "Rectangle") {
            shapes.push_back(shape_ptr(new Rectangle(ifs)));
        } else if (name == "Circle") {
            shapes.push_back(shape_ptr(new Circle(ifs)));
        } else if (name == "Triangle") {
            shapes.push_back(shape_ptr(new Triangle(ifs)));
        }
    }

    for (Shapes::const_iterator i = shapes.begin(); i != shapes.end(); ++i) {
        (*i)->write_to(std::cout);
    }
}

図形クラスにも IO からデータを読み込めるようにします。

コードが長くなるので、追加分だけ示します。

// 略
class Rectangle : public Shape {
public:
    // 略
    Rectangle(std::istream& in) {
        in >> x >> y >> width >> height;
    }
    // 略
};

class Circle : public Shape {
public:
    // 略
    Circle(std::istream& in) {
        in >> x >> y >> r;
    }
    // 略
};

class Triangle : public Shape {
public:
    // 略
    Triangle(std::istream& in) {
        in >> x1 >> y1 >> x2 >> y2 >> x3 >> y3;
    }
    // 略
};
// 略

ところで。 データを IO に出力する挿入演算子 ( << ) や IO からデータを入力する抽出演算子 (>>) は、シフト演算子をオーバーロードしたもので、C++ プログラミングで最初に遭遇する C++ の奇妙なところだと思います。 なかなか慣れないですし、人によってはずっと慣れることのない代物かもしれませんが、使い所を選べばデータを流れとしてやりとりできるのでとても重宝します。

閑話休題。

C++ でオブジェクトをデシリアライズする、その2

C++ の構文でハードコードせずに、文字列に対応するクラスのインスタンスを生成するにはどうしたらよいでしょう。

C++ には Ruby のように文字列からクラスを見つける仕組みは用意されていません。 自分で書く必要があります。

ここでは Deserializer というクラスと、文字列とデシリアライザの対応付けを事前に格納した deserializers を用意します*1

#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <vector>
#include "shape.hpp"

typedef std::shared_ptr<Shape> shape_ptr;
typedef std::vector<shape_ptr> Shapes;

class Deserializer {
public:
    virtual shape_ptr create(std::istream&) const = 0;
};

template<class T> class DeserializerImpl : public Deserializer {
    shape_ptr create(std::istream& in) const {
        return shape_ptr(new T(in));
    }
};

const std::map<std::string, std::shared_ptr<const Deserializer> > deserializers = {
    {"Rectangle", std::shared_ptr<Deserializer>(new DeserializerImpl<Rectangle>)},
    {"Circle", std::shared_ptr<Deserializer>(new DeserializerImpl<Circle>)},
    {"Triangle", std::shared_ptr<Deserializer>(new DeserializerImpl<Triangle>)}
};

int main(int, char**) {
    Shapes shapes;

    std::cout << "Deserializer 3" << std::endl;
    std::ifstream ifs("shapes.dat");
    for (;;) {
        std::string name;
        ifs >> name;
        if (ifs.eof()) {
            break;
        }

        shapes.push_back(deserializers.at(name)->create(ifs));
    }

    for (Shapes::const_iterator i = shapes.begin(); i != shapes.end(); ++i) {
        (*i)->write_to(std::cout);
    }
}

C++ ではクラスの世界とインスタンスの世界は別世界であるため、実行時に手に入るデータから new 演算子を使ってインスタンスを生成することはできません。

そのためインスタンスを生成する操作をインスタンスの世界に持ち込む仕組みが必要になります。

まず、クラステンプレートでインスタンスを生成するメソッドを持つクラスを定義します。

これによって例えば次のようなクラスが定義されます。

// テンプレートによって生成されるクラスの定義を便宜的に書き下したもの
class DeserializerImplRectangle : public Deserializer {
    shape_ptr create(std::istream& in) const {
        return shape_ptr(new Rectangle(in));
    }
};

このクラスをインスタンス化することで Rectangle の生成操作を値として扱えるようになります。

Deserializer* deserializer = new DeserializerImplRectangle;
deserializer->create(ifs);

Rectangle, Circle, Triangle のそれぞれの生成操作をオブジェクトにして、辞書 ( std::map ) に文字列とオブジェクトの対応付けを格納します。

const std::map<std::string, std::shared_ptr<const Deserializer> > deserializers = {
    {"Rectangle", std::shared_ptr<Deserializer>(new DeserializerImpl<Rectangle>)},
    {"Circle", std::shared_ptr<Deserializer>(new DeserializerImpl<Circle>)},
    {"Triangle", std::shared_ptr<Deserializer>(new DeserializerImpl<Triangle>)}
};

これでようやく文字列からインスタンスの生成操作を取得することができるようになり、取得した生成操作を使ってインスタンスを生成することができるようになりました。

Ruby では、実行環境がこのような辞書を持っていて、プログラムからその辞書を実行時に利用でき、クラスもオブジェクトであるため辞書から値としてクラスを取り出すことができ、そのクラスのメソッド .new を当たり前に利用できるようになっています。

逆にこの C++ のコードは、Ruby のそのような挙動を再現しようとしたものと言えるかもしれません。

まとめ

  • シリアライズは、オブジェクト自身が自分が何者なのかを知っているので、データの形式と内容が決まればそれほど難しくはありません。
  • デシリアライズは、入力した内容が何者なのかを調べなければならず、それを誰かに問い合わせなければなりません。何者かがわかってもそれを復元する仕組みが用意されていなければなりません。
  • インスタンスの生成は、プログラミング言語によってはプログラムの実行環境とは別の階層にあり、それを超えるための仕組みを実現することが強いられます。メタプログラミングの入り口です。
  • ネタプログラミングにしては、今回はネタのひねりが今ひとつという自己評価。
  • 事業部では「メタプログラミング Ruby 読書会」も開催しています。 気になる方はこちらからお問い合わせを。

agile.esm.co.jp

*1:Mac の g++ でコンパイルすると "cannot be initialized with an initializer list" のようなエラーが出るかもしれません。その場合は -std=gnu++11 オプションをつけてコンパイルしてみてください。