2

我有一个User带有 40 多个私有变量的“”类,包括复杂的对象,如私钥/公钥(QCA 库)、自定义 QObjects 等。这个想法是该类有一个名为的函数sign(),它可以加密、签名、序列化自身并返回一个QByteArray可以然后存储在 SQLite blob 中。

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

可以将其转换为 char 数组吗?

4

2 回答 2

3

可以将其转换为 char 数组吗?

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

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

最简单的解决方案可能是为QDataStream您使用的对象和类型实现运算符。确保遵循良好的实践:可以想象到的每个类都可能改变它所持有的数据格式,必须发出一个格式标识符。

例如,让我们学习以下课程:

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宏使类QVariantQMetaType类型系统知道。因此,例如,可以将 an 分配Address给 a QVariant,将这种 a 转换QVariantAddress,将变体直接流式传输到数据流等。

首先,让我们解决如何转储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();
}

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

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;
}

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

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

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格式的内部版本时,确定整个数据流的版本是非常重要的。该文档是必读的。

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

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

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

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

如果您认为格式字节可能会用完,您可以采用可变长度编码方案,称为扩展或扩展八位字节,在各种 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 整数文字中有效。

它将按如下方式使用:

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;
}
于 2015-08-11T19:12:36.023 回答
0

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

如果只是一个单独的类,手动实现它,肯定不会杀了你。如果您有一系列类,并且它们是QObject派生的,则可以使用元系统,但这只会注册属性,而int something不会与属性绑定的成员将被跳过。如果您有很多不是 Qt 属性的数据成员,那么将它们公开为 Qt 属性将花费您更多的输入,我可能会添加不必要的内容,而不是手动编写序列化方法。

于 2015-08-11T17:09:27.713 回答