qt序列化 (三)

In the previous post I already wrote about backward and forward compatibility when serializing and deserializing data into a binary stream. Let's summarize:

  • backward compatibility - the ability to deserialize data serialized by and older version of the application
  • forward compatibility - the ability to deserialize data serialized by a newer version of the application

I said that backward compatibility can be achieve by storing a version tag in the data stream and conditionally changing the deserialization routine based on the version of data. However forward compatibility cannot be achieved this way because we can't predict what changes will be made in the future. This is fine for configuration data, but in case of documents it's not always acceptable.

The best solution would be to allow the application to skip and ignore information it doesn't understand, and extract as much information as it can. Note that it's not always possible. In case of a text document, the content can be preserved even if some fancy formatting is lost. However, let's recall the example in which we added child bookmarks to the Bookmark class. Even if we could skip loading the child bookmarks, we would still lose a lot of information, as only the top level bookmarks would be available in the old version. So before we start thinking about a fancy solution, we should first ask ourserlves if it's really worth the effort.

There is also a relatively simple workaround available. The new version of the application can be forced to save data in format compatible with an older version. This simply means that we have to add similar conditional code in serialization routines. Many applications work in this way, including MS Office applications. For example, the application could save all bookmarks in a linear fashion, losing the parent-child relationship, but still preserving all bookmarks.

But for true compatibility we need to design the data format in such way, that when the application encounters data that it doesn't understand, it can at least skip it and continue processing. But without additional metadata the application doesn't even know how many bytes it should skip.

A simple solution is to wrap all data in a QVariant before serializing, because QVariant writes a tag which identifies the type of the data before the actual data. Let's start with the following code:

template<typename T>
void operator <<( QVariant& data, const T& target )
{
    data = QVariant::fromValue<T>( target );
}

template<typename T>
void operator >>( const QVariant& data, T& target )
{
    target = data.value<T>();
}

These are generic function templates that convert any data to and from a variant. Now let's specialize these functions for our Bookmark type from the previous post. We will use a map to convert a bookmark to a variant and vice versa:

void operator <<( QVariant& data, const Bookmark& target )
{
    QVariantMap map;
    map[ "Name" ] << target.m_name;
    map[ "URL" ] << target.m_url;
    map[ "Children" ] << target.m_children;
    data << map;
}

void operator >>( const QVariant& data, Bookmark& target )
{
    QVariantMap map;
    data >> map;
    map[ "Name" ] >> target.m_name;
    map[ "URL" ] >> target.m_url;
    map[ "Children" ] >> target.m_children;
}

Note that because the bookmark object is converted to a QVariantMap before serializing, it can be successfully deserialized even if the application that reads the data doesn't know anything about the Bookmark type. What's more, we can add more elements to the map in the future without affecting either backward or forward compatibility. When reading a newer version of the file, the elements which are not understood are simply ignored. When reading an older version, missing elements are automatically replaced with default values for the given type.

When we try to compile the above code, we will receive a cryptic error similar to 'qt_metatype_id' : is not a member of 'QMetaTypeId<T>'. That's because a QList<Bookmark> cannot be converted into a QVariant. Since we know how to convert a Bookmark into a QVariant, we can easily convert a QList<Bookmark> into a QVariantList. This can even be done in a generic way:

template<typename T>
void operator <<( QVariant& data, const QList<T>& target )
{
    QVariantList list;
    list.reserve( target.count() );
    for ( int i = 0; i < target.count(); i++ ) {
        QVariant item;
        item << target[ i ];
        list.append( item );
    }
    data = list;
}

template<typename T>
void operator >>( const QVariant& data, QList<T>& target )
{
    QVariantList list = data.toList();
    target.reserve( list.count() );
    for ( int i = 0; i < list.count(); i++ ) {
        T item;
        list[ i ] >> item;
        target.append( item );
    }
}

This way any QList<T> can be converted from/to a QVariant as long as T can be converted from/to a QVariant. Note that we may also want to create additional specializations for QStringList and QVariantList, so that they are not unnecessarily converted, and to add similar conversion functions for maps and other containers.

To summarize, the following conversions are used before data is serialized:

  • Primitive types (numbers, strings and many other built-in types in Qt) are stored as QVariant
  • Objects are stored as QVariantMap that maps properties to values
  • List of various types are stored as QVariantList

The actual serialization consists of two steps: converting the serialized object into a QVariant and serializing the converted data into the stream. Deserialization is analogous and works in the opposite way.

You can notice that the converted data is somewhat similar to the DOM tree of XML document. A variant of a primitive type is analogous to a leaf XML node, and a map of variants is similar to an XML element with child nodes. However, this approach is more compact, faster and easier to use than XML.

Note that simple custom types don't necessarily have to be stored as a QVariantMap. For example, in a financial application, there may be a Money class, which is really a wrapper over some simple numeric value. We can directly place this numeric value in a variant (e.g. as a qlonglong) without wrapping it in a map.

We can also combine the method of serialization based on QVariantMap with the traditional approach, as described in the previous article, for certain types that highly unlikely to change, and can be treated as primitive types. For example, in a graphic application we might define a Circle class which consists of a central QPoint and a radius. We can use the Q_DECLARE_METATYPE macro and the qRegisterMetaTypeStreamOperators function, so that the Circle object can be directly wrapped into a variant and serialized without any conversions.

Just remember that when the Circle type is introduced in a later version of the application, previous versions will not be able to load a file that contains it, so we must remeber about the version tag, as described in the previous post. Also the version of the data format used by built-in Qt types is important to maintain compatibility across different environments.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Qt序列化Serialization)功能允许你在程序之间或持久化存储中保存和恢复对象的状态。这是一项强大的工具,特别是在跨版本迁移或不同模块间传递数据时,能够保证数据的一致性和可读性。 **版本兼容性**在Qt序列化中是非常关键的。当你升级或改变应用程序的代码后,旧版本的程序可能无法读取新版本生成的数据,反之亦然。Qt提供了几种机制来处理版本兼容性: 1. **Qt Meta Object System (QMetaObject)**:使用QMetaObject可以创建具有不同版本的序列化数据的类。你可以设置元对象系统中的`Q_OBJECT`宏来启用序列化,并定义`Q_PROPERTY`来指定哪些属性需要被序列化。通过指定`Q_INVOKABLE`标志,函数也可以被序列化Qt会自动处理不同版本之间的属性更改。 2. **qRegisterMetaType()**: 这个函数用于注册特定类型的数据,以便于序列化。如果你在新版本中添加了新的数据类型,需要确保在旧版本中也注册,或者为旧版本提供一种方法来忽略新数据。 3. **QDataStream**和`QTextStream`:在更新过程中,如果数据结构发生变化,你可以定义一个策略来处理旧版本的数据,比如跳过未知字段,或者为新字段提供默认值。 4. **Qt Serializers**: Qt提供了一些预编译的序列化库,如QSettings、QByteArray等,它们有自己的方式处理版本差异,例如提供不同的文件格式或版本号标记。 **相关问题--:** 1. 如何确保Qt对象在不同版本间的序列化一致性? 2. 在更新Qt应用程序时,如何处理已有的QDataStream或QTextStream数据格式变化? 3. Qt的哪些类或工具可以帮助处理版本不兼容的序列化问题?

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值