如何序列化一个大而复杂的对象?

How to go about serializing a large, complex object?

我有一个“User”class,其中包含 40 多个私有变量,包括 private/public 键(QCA 库)、自定义 QObject 等复杂对象。这个想法是 class 有一个名为 sign() 的函数,它可以加密、签名、序列化自身和 returns 一个 QByteArray 然后可以将其存储在 SQLite blob 中。

序列化复杂对象的最佳方法是什么?使用 QMetaObject 遍历属性?将其转换为 protobuf 对象?

可以转换为字符数组吗?

二进制转储序列化是个坏主意,它会包含很多你不需要的东西,比如对象的 v-table 指针,以及其他指针,直接包含或来自其他 class 成员,序列化没有意义,因为它们不会在应用程序会话之间持续存在。

如果只是一个class,直接手动实现,肯定不会死的。如果你有一个 classes 家族,并且它们是 QObject 派生的,你可以使用元系统,但这只会注册属性,而 int something 成员不依赖于属性 将被跳过。如果您有很多不是 Qt 属性的数据成员,与手动编写序列化方法相比,将它们公开为 Qt 属性需要更多的输入,我可能会不必要地补充。

Could it be casted to a char array?

不,因为您将转换 QObject 您一无所知的内部结构,第二次您 运行 您的程序时无效的指针,等等。

TL;DR:手动实现它对于显式数据元素是可以的,并且利用 QObjectQ_GADGET classes 的元对象系统将有助于一些苦差事。

最简单的解决方案可能是为您使用的对象和类型实施 QDataStream 运算符。确保遵循良好做法:每个 class 可能会改变其保存的数据格式的每个人都必须发出格式标识符。

例如,让我们采用以下 classes:

class User {
  QString m_name;
  QList<CryptoKey> m_keys;
  QList<Address> m_addresses;
  QObject m_props;
  ...
  friend QDataStream & operator<<(QDataStream &, const User &);
  friend QDataStream & operator>>(QDataStream &, User &);
public:
  ...
};
Q_DECLARE_METATYPE(User) // no semi-colon

class Address {
  QString m_line1;
  QString m_line2;
  QString m_postCode;
  ...
  friend QDataStream & operator<<(QDataStream &, const Address &);
  friend QDataStream & operator>>(QDataStream &, Address &);
public:
  ...
};
Q_DECLARE_METATYPE(Address) // no semi-colon!

Q_DECLARE_METATYPE 宏使 classes 为 QVariantQMetaType 类型系统所知。因此,例如,可以将 Address 分配给 QVariant,将这样的 QVariant 转换为 Address,将变体直接流式传输到数据流等。

首先,我们来解决如何转储 QObject 属性:

QList<QByteArray> publicNames(QList<QByteArray> names) {
  names.erase(std::remove_if(names.begin(), names.end(),
              [](const QByteArray & v){ return v.startsWith("_q_"); }), names.end());
  return names;
}

bool isDumpable(const QMetaProperty & prop) {
  return prop.isStored() && !prop.isConstant() && prop.isReadable() && prop.isWritable();
}

void dumpProperties(QDataStream & s, const QObject & obj)
{
  s << quint8(0); // format
  QList<QByteArray> names = publicNames(obj.dynamicPropertyNames());
  s << names;
  for (name : names) s << obj.property(name);
  auto mObj = obj.metaObject();
  for (int i = 0; i < mObj->propertyCount(), ++i) {
    auto prop = mObj->property(i);
    if (! isDumpable(prop)) continue;
    auto name = QByteArray::fromRawData(prop.name(), strlen(prop.name());
    if (! name.isEmpty()) s << name << prop.read(&obj);
  }
  s << QByteArray();
}

一般来说,如果我们要处理来自没有 m_props 成员的 User 的数据,我们需要能够清除属性。每次扩展存储对象和升级序列化格式时都会出现这个成语。

void clearProperties(QObject & obj)
{
  auto names = publicNames(obj.dynamicPropertyNames());
  const QVariant null;
  for (name : names) obj.setProperty(name, null);
  auto const mObj = obj.metaObject();
  for (int i = 0; i < mObj->propertyCount(), ++i) {
    auto prop = mObj->property(i);
    if (! isDumpable(prop)) continue;
    if (prop.isResettable()) {
      prop.reset(&obj);
      continue;
    }
    prop.write(&obj, null);
  }
}

现在我们知道如何从流中恢复属性了:

void loadProperties(QDataStream & s, QObject & obj)
{
  quint8 format;
  s >> format;
  // We only support one format at the moment.
  QList<QByteArray> names;
  s >> names;
  for (name : names) {
    QVariant val;
    s >> val;
    obj.setProperty(name, val);
  }
  auto const mObj = obj.metaObject();
  forever {
    QByteArray name;
    s >> name;
    if (name.isEmpty()) break;
    QVariant value;    
    s >> value;
    int idx = mObj->indexOfProperty(name);
    if (idx < 0) continue;
    auto prop = mObj->property(idx);
    if (! isDumpable(prop)) continue;
    prop.write(&obj, value);
  }
}

因此我们可以实现流操作符来序列化我们的对象:

#define fallthrough

QDataStream & operator<<(QDataStream & s, const User & user) {
  s << quint8(1) // format
    << user.m_name << user.m_keys << user.m_addresses;
  dumpProperties(s, &m_props);
  return s;
}

QDataStream & operator>>(QDataStream & s, User & user) {
  quint8 format;
  s >> format;
  switch (format) {
  case 0:
    s >> user.m_name >> user.m_keys;
    user.m_addresses.clear();
    clearProperties(&user.m_props);
    fallthrough;
  case 1:
    s >> user.m_addresses;
    loadProperties(&user.m_props);
    break;
  }
  return s;
}

QDataStream & operator<<(QDataStream & s, const Address & address) {
  s << quint8(0) // format
    << address.m_line1 << address.m_line2 << address.m_postCode;
  return s;
}

QDataStream & operator>>(QDataStream & s, Address & address) {
  quint8 format;
  s >> format;
  switch (format) {
  case 0:
    s >> address.m_line1 >> address.m_line2 >> address.m_postCode;
    break;
  }
  return s;
}

属性 系统也适用于任何其他 class,只要您声明其属性并添加 Q_GADGET 宏(而不是 Q_OBJECT)。这是从 Qt 5.5 开始支持的。

假设我们声明我们的 Address class 如下:

class Address {
  Q_GADGET
  Q_PROPERTY(QString line1 MEMBER m_line1)
  Q_PROPERTY(QString line2 MEMBER m_line2)
  Q_PROPERTY(QString postCode MEMBER m_postCode)

  QString m_line1;
  QString m_line2;
  QString m_postCode;
  ...
  friend QDataStream & operator<<(QDataStream &, const Address &);
  friend QDataStream & operator>>(QDataStream &, Address &);
public:
  ...
};

然后让我们根据 [dump|clear|load]Properties 为处理小工具而修改的数据流运算符声明:

QDataStream & operator<<(QDataStream & s, const Address & address) {
  s << quint8(0); // format
  dumpProperties(s, &address);
  return s;
}

QDataStream & operator>>(QDataStream & s, Address & address) {
  quint8 format;
  s >> format;
  loadProperties(s, &address);
  return s;
}

即使 属性 集已更改,我们也不需要更改格式指示符。我们应该保留格式指示符,以防我们有其他更改无法再表示为简单的 属性 转储。在大多数情况下这不太可能,但必须记住,不使用格式说明符的决定会立即将流式数据的格式固定下来。之后无法更改它!

最后,属性 处理程序是用于 QObject 属性的处理程序的略微缩减和修改变体:

template <typename T> void dumpProperties(QDataStream & s, const T * gadget) {
  dumpProperties(s, T::staticMetaObject, gadget);
}

void dumpProperties(QDataStream & s, const QMetaObject & mObj, const void * gadget)
{
  s << quint8(0); // format
  for (int i = 0; i < mObj.propertyCount(), ++i) {
    auto prop = mObj.property(i);
    if (! isDumpable(prop)) continue;
    auto name = QByteArray::fromRawData(prop.name(), strlen(prop.name());
    if (! name.isEmpty()) s << name << prop.readOnGadget(gadget);
  }
  s << QByteArray();
}

template <typename T> void clearProperties(T * gadget) {
  clearProperties(T::staticMetaObject, gadget);
}

void clearProperties(const QMetaObject & mObj, void * gadget)
{
  const QVariant null;
  for (int i = 0; i < mObj.propertyCount(), ++i) {
    auto prop = mObj.property(i);
    if (! isDumpable(prop)) continue;
    if (prop.isResettable()) {
      prop.resetOnGadget(gadget);
      continue;
    }
    prop.writeOnGadget(gadget, null);
  }
}

template <typename T> void loadProperties(QDataStream & s, T * gadget) {
  loadProperties(s, T::staticMetaObject, gadget);
}

void loadProperties(QDataStream & s, const QMetaObject & mObj, void * gadget)
{
  quint8 format;
  s >> format;
  forever {
    QByteArray name;
    s >> name;
    if (name.isEmpty()) break;
    QVariant value;    
    s >> value;
    auto index = mObj.indexOfProperty(name);
    if (index < 0) continue;
    auto prop = mObj.property(index);
    if (! isDumpable(prop)) continue;
    prop.writeOnGadget(gadget, value);
  }
}

TODO loadProperties 实现中未解决的一个问题是清除对象中存在但序列化中不存在的属性。

当涉及到 QDataStream 格式的内部版本时,确定整个数据流的版本控制非常重要。 documentation 是必读内容。

还必须决定如何处理软件版本之间的兼容性。有几种方法:

  1. (最典型和不幸)没有兼容性:没有存储格式信息。新成员以临时方式添加到序列化中。旧版本的软件在面对新数据时会表现出未定义的行为。较新的版本将对较旧的数据执行相同的操作。

  2. 向后兼容性:格式信息存储在每个自定义类型的序列化中。新版本可以正确处理旧版本的数据。旧版本必须检测未处理的格式、中止反序列化并向用户指示错误。 忽略较新的格式会导致未定义的行为

  3. 完全向后和向前兼容:每个序列化的自定义类型都存储在 QByteArray 或类似的容器中。通过这样做,您可以获得有关整个类型的数据记录有多长的信息。 QDataStream 版本必须修复。要读取自定义类型,首先读取其字节数组,然后设置一个 QBuffer,您可以使用 QDataStream 从中读取。您读取可以以您知道的格式解析的元素,并忽略其余数据。这迫使对格式采用增量方法,其中较新的格式只能在现有格式上附加元素。但是,如果新格式放弃了旧格式中的某些数据元素,它仍然必须转储它,但使用 null 或其他安全的默认值来保留旧版本的代码 "happy".

如果您认为格式字节可能 运行,您可以使用可变长度编码方案,称为扩展或扩展八位字节,在各种 ITU 标准中都很熟悉(例如 Q.931 4.5.5 承载能力信息元素)。思路如下:一个八位位组(字节)的最高位用来表示这个值是否需要更多的八位位组来表示。这使得字节有 7 位来表示值,1 位来标记扩展。如果该位已设置,您将读取后续的八位字节并以小端方式将它们连接到现有值。以下是您可以执行此操作的方法:

class VarLengthInt {
public:
  quint64 val;
  VarLengthInt(quint64 v) : val(v) { Q_ASSERT(v < (1ULL<<(7*8))); }
  operator quint64() const { return val; }
};

QDataStream & operator<<(QDataStream & s, VarLengthInt v) {
  while (v.val > 127) {
    s << (quint8)((v & 0x7F) | 0x80);
    v.val = v.val >> 7;
  }
  Q_ASSERT(v.val <= 127);
  s << (quint8)v.val;
  return s;
}

QDataStream & operator>>(QDataStream & s, VarLengthInt & v) {
  v.val = 0;
  forever {
    quint8 octet;
    s >> octet;
    v.val = (v.val << 7) | (octet & 0x7F);
    if (! (octet & 0x80)) break;
  }
  return s;
}

VarLengthInt 的序列化具有可变长度,并且始终使用给定值可能的最小字节数:1 个字节到 0x7F,2 个字节到 0x3FFF,3 个字节到 0x1F'FFFF, 4 个字节,直到 0x0FFF'FFFF 等。撇号在 C++14 integer literals.

中有效

它将按如下方式使用:

QDataStream & operator<<(QDataStream & s, const User & user) {
  s << VarLengthInt(1) // format
    << user.m_name << user.m_keys << user.m_addresses;
  dumpProperties(s, &m_props);
  return s;
}

QDataStream & operator>>(QDataStream & s, User & user) {
  VarLengthInt format;
  s >> format;
  ...
  return s;
}