我有一个User
带有 40 多个私有变量的“”类,包括复杂的对象,如私钥/公钥(QCA 库)、自定义 QObjects 等。这个想法是该类有一个名为的函数sign()
,它可以加密、签名、序列化自身并返回一个QByteArray
可以然后存储在 SQLite blob 中。
序列化复杂对象的最佳方法是什么?QMetaObject
用?迭代属性 将其转换为 protobuf 对象?
可以将其转换为 char 数组吗?
我有一个User
带有 40 多个私有变量的“”类,包括复杂的对象,如私钥/公钥(QCA 库)、自定义 QObjects 等。这个想法是该类有一个名为的函数sign()
,它可以加密、签名、序列化自身并返回一个QByteArray
可以然后存储在 SQLite blob 中。
序列化复杂对象的最佳方法是什么?QMetaObject
用?迭代属性 将其转换为 protobuf 对象?
可以将其转换为 char 数组吗?
可以将其转换为 char 数组吗?
不,因为你会强制转换QObject
你一无所知的内部结构,第二次运行程序时无效的指针等等。
TL;DR:对于显式数据元素,手动实现它是可以的,并且利用元对象系统QObject
和Q_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
宏使类QVariant
和QMetaType
类型系统知道。因此,例如,可以将 an 分配Address
给 a QVariant
,将这种 a 转换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();
}
一般来说,如果我们要处理来自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
格式的内部版本时,确定整个数据流的版本是非常重要的。该文档是必读的。
人们还必须决定如何处理软件版本之间的兼容性。有几种方法:
(最典型和最不幸的) 不兼容:不存储格式信息。新成员以特别的方式添加到序列化中。旧版本的软件在面对新数据时会表现出未定义的行为。较新的版本将对较旧的数据执行相同的操作。
向后兼容性:格式信息存储在每个自定义类型的序列化中。新版本可以正确处理旧版本的数据。旧版本必须检测未处理的格式、中止反序列化并向用户指示错误。忽略较新的格式会导致未定义的行为。
完全的前后兼容性:每个序列化的自定义类型都存储在一个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;
}
二进制转储序列化是一个坏主意,它会包含很多你不需要的东西,比如对象的 v-table 指针,以及直接包含或来自其他类成员的其他指针,这对序列化没有意义,因为它们不会在应用程序会话之间持续存在。
如果只是一个单独的类,手动实现它,肯定不会杀了你。如果您有一系列类,并且它们是QObject
派生的,则可以使用元系统,但这只会注册属性,而int something
不会与属性绑定的成员将被跳过。如果您有很多不是 Qt 属性的数据成员,那么将它们公开为 Qt 属性将花费您更多的输入,我可能会添加不必要的内容,而不是手动编写序列化方法。