是否可以转换为 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 类型系统知道这些类。因此,例如,可以将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;
}
属性系统也适用于任何其他类,只要您声明其属性并添加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);
}
}
TODOloadProperties 实现中未解决的问题是清除对象中存在但序列化中不存在的属性。
当涉及到QDataStream 格式的内部版本时,确定整个数据流的版本是非常重要的。 documentation 是必读。
还必须决定如何处理软件版本之间的兼容性。有几种方法:
(最典型和不幸的)不兼容:不存储格式信息。新成员以特别的方式添加到序列化中。旧版本的软件在面对新数据时会表现出未定义的行为。较新的版本将对较旧的数据执行相同的操作。
向后兼容性:格式信息存储在每个自定义类型的序列化中。新版本可以正确处理旧版本的数据。旧版本必须检测未处理的格式、中止反序列化并向用户指示错误。 忽略较新的格式会导致未定义的行为。
完全的前后兼容性:每个序列化的自定义类型都存储在QByteArray 或类似的容器中。通过这样做,您可以获得有关整个类型的数据记录多长时间的信息。 QDataStream 版本必须是固定的。要读取自定义类型,首先读取其字节数组,然后设置 QBuffer,您可以使用 QDataStream 进行读取。您阅读可以以您知道的格式解析的元素,并忽略其余数据。这迫使对格式采用增量方法,其中较新的格式只能在现有格式上附加元素。但是,如果新格式放弃了旧格式中的某些数据元素,它仍然必须转储它,但使用 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 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;
}