1 引言
Visual C++ 6.0语言开发环境在MFC中以CArchive类为中心提供输入输出的串行化和数据版本控制功能。一般来说,随着软件的升级,对应的输入输出也会发生改变,如何保证多个版本的软件与多种输入输出数据之间的亲和性成为问题。比如说,当用户使用1.0版的软件,而提供的输入却是2.0版软件的输出,结果会如何呢?同样2.0版的软件如何处理1.0版生成的数据呢?
的确,用户可以定义一套自己的文件格式为上述因版本不同造成的造成的麻烦提供解决方案,许多软件都是这么干,但如果你所编制的软件规模很小,功能单一,对数据格式并不在意(当然要保证输入输出正确),那么花力气指定自己的数据文件格式似乎过于繁琐。使用CArchive可以简化这类软件的编制。
2 CArchive的版本控制方式
当你使用DECLARE_SERIAL和IMPLEMENT_SERAIL宏时,你就在你的类中声明并定义了一个CRuntimeClass类的静态结构成员,该结构中的m_wSchema可以记录你指定的版本号。注意虽然按照命名约定m_wSchema似乎是个WORD,但实际它是个UINT。当串行化输出时,类信息(即类名和版本号的低字,高字是标志)被写入数据文件,而串行化输入时CArchive::ReadClass()和CArchive::ReadObject()根据这些信息以及后面的对象数据重建对象,返回对象指针。这一切均在类似pObject<<ar;的语句中发生。如果CRuntimeClass检测到数据文件中纪录的版本号与类的版本号(即类的CRuntimeClass成员中记录的版本号)不一致,根据你设置版本号时是否指定VERSIONABLE_SCHEMA标志有不同处理:如果你没有设置该标志,CArchive::ReadClass()将抛出一个CArchiveException类型的异常;如果你设置了,那么CArchive::ReadClass()将成功返回对应的CRuntimeClass类对象,而CArchive::ReadObject()将创建串行类对象并返回该对象和版本号(在 CArchive::m_nObjectShema成员中,该成员是public型,可以直接访问或调用CArchive::GetObjectSchema()返回。如果调用CArchive::GetObjectSchema(),要注意该函数只能调用一次!)。
虽然这一切工作的很好,但问题并非象看上那么简单,因为你的软件在不同版本中会有改动。由于CArchive::ReadClass()是根据类名来获取CRuntimeClass类成员的,而CArchive::ReadObject()又是根据CArchive::ReadClass()返回的CRuntimeClass类成员对象来创建串行类对象的(CRuntimeClass成员是串行类的静态成员)。因此,如果新的软件中根本没有对应的串行类,那么必然失败!
3 类的改动对版本控制的影响
版本升级需要扩展或改变类的接口,从串行类的定义来看,有多种方式类中扩展新的功能:
(1). 修改原先的类定义,同时改变类名
这时实际上原先的类已经不存在,那么CArchive无法根据数据文件创建对应的对象,因此新版本与旧版本之间完全无法兼容,如同两个完全无关的软件。不仅旧软件无法识别并处理新软件的数据文件,新软件同样无法识别并处理旧软件的数据文件。你需要自己识别处理数据文件的转换,MFC无法识别处理。
(2). 修改原先的类定义,但类名保留
此时,CArchive可以根据旧的数据文件创建对象,但在新版本串行类的CArchive::Serailize()需要根据版本好做不同的输入输出处理。由于类定义已经改变,对旧数据的处理可能同旧版本处理方法上有差异,需要小心从事。而旧版本可以识别出新版本,但一般无法处理。
(3). 从原先的类派生新类支持扩展功能
这种方式充分利用了C++语言的特性,新版本可以根据版本号对不同版本数据文件进行处理,并且依然可以生成旧版本的数据文件,兼容性非常好。但旧版本无法识别出新版的数据文件,更谈不上处理了。
比较看来,方式(1)比较粗暴,完全不考虑软件升级的兼容性问题,方式(2)和(3)均考虑到了兼容问题,但方式(2)的编程复杂,串行类要针对版本号编制处理代码,而方式(3)利用C++语言的特性比较好处理多版本数据问题,只要注意虚拟函数就可以了。但如果软件升级次数很多,从1.0不停地升到8.0,那么显然有点不舒服。另外一个重要的不足是旧版本无法识别新版本的数据文件,只能提示数据文件格式不对。
4 例子
下面的例子是升级后的代码,采用方式(3)处理不同版本问题:
// 注意:虽然使用某些MFC的代码,但为了简单没有使用
// 向导生成的初始化代码,因此对MFC的支持是不全的!
// 声明对象的头文件
#ifndef __LCG_OBJECT_H__
#define __LCG_OBJECT_H__
#include<AfxWin.h>
// 版本1.0采用的对象
class CObject1 : public CObject
{
DECLARE_SERIAL( CObject1 )
public:
CObject1();
virtual void Serialize( CArchive& ar );
virtual BOOL IsOkVer( UINT nVer );
CString m_str;
};
// 版本(2)扩展了版本1.0的类
class CObject2 : public CObject1
{
DECLARE_SERIAL( CObject2 )
public:
CObject2();
virtual void Serialize( CArchive& ar );
virtual BOOL IsOkVer( UINT nVer );
CString m_str2; // 版本2.0才有的数据
};
#endif
// 对象的.cpp文件
#include "Object.h"
IMPLEMENT_SERIAL( CObject1, CObject, (VERSIONABLE_SCHEMA|1) )
CObject1::CObject1()
{
m_str = "CObject1 Msg\n";
}
// 1.0版的版本检查函数,版本检查函数要声明为虚拟的,
// 这样实际的对象才可能进行正确的检查,否则,即使对象正确,
// 但由于调用指针是基类的指针,检查结果不正确!
BOOL CObject1::IsOkVer( UINT nVer )
{
return nVer==1;
}
void CObject1::Serialize( CArchive& ar )
{
if( ar.IsStoring() )
{
ar<<(int)m_str.GetLength();
ar.WriteString( m_str );
}
else
{
if( !IsOkVer( ar.m_nObjectSchema ) )
{
AfxMessageBox( "CObject1得到不正确的版本!" );
return;
}
int nLen; ar>>nLen;
ar.Read( m_str.GetBuffer(nLen), nLen );
}
}
// 2.0版的对象
IMPLEMENT_SERIAL(CObject2,CObject1, (VERSIONABLE_SCHEMA|2))
CObject2::CObject2()
{
m_str2 = "CObject2 Msg\n";
}
void CObject2::Serialize( CArchive& ar )
{
CObject1::Serialize( ar );
if( ar.IsStoring() )
{
ar<<(int)m_str2.GetLength();
ar.WriteString( m_str2 );
}
else
{
if( !IsOkVer( ar.m_nObjectSchema ) )
{
AfxMessageBox( "CObject2得到不正确的版本!" );
return;
}
int nLen; ar>>nLen;
ar.Read( m_str2.GetBuffer(nLen), nLen );
}
}
BOOL CObject2::IsOkVer( UINT nVer )
{
return nVer==2;
}
// 主程序,对不同版本使用不同的对象
#include<AfxWin.h>
#include"Object.h"
#pragma comment( lib, "msvcrt.lib" )
int WINAPI WinMain( HINSTANCE hInst, HINSTANCE hNULL,
LPSTR lpCmdLine, int nCmdShow )
{
AfxWinInit( hInst, hNULL, lpCmdLine, nCmdShow ); // MFC初始化
CFile file( "Try1.txt", CFile::modeRead );
CArchive ar( &file, CArchive::load );
CObject *pb = NULL;
CObject1 *pb1 = NULL;
CObject2 *pb2 = NULL;
char szMsg[128];
BOOL bError = FALSE;
// 如果数据文件格式不正确会触发异常!
TRY{
ar>>pb;
}
CATCH( CArchiveException, pe )
{
bError = TRUE; // 出错!
pe->GetErrorMessage( szMsg, sizeof(szMsg) ); // 获取提示信息
DELETE_EXCEPTION( pe );
AfxMessageBox( szMsg );
}
END_CATCH
if( bError )
return 0;
// 根据不同的版本调用不同的类
switch((pb->GetRuntimeClass()->m_wSchema)&
~(VERSIONABLE_SCHEMA) )
{
case 1:
pb1 =
(CObject1*)AfxDynamicDownCast( RUNTIME_CLASS(CObject1), pb );
AfxMessageBox( pb1->m_str );
break;
case 2:
pb2=
(CObject2*)AfxDynamicDownCast( RUNTIME_CLASS(CObject2), pb );
AfxMessageBox( pb2->m_str2 );
break;
default:
AfxMessageBox( "Unkown Object!" );
break;
}
delete pb;
return 0;
}
5. 结束语
上述思路和例子是一般软件版本控制的一个简单的模拟,实际中要复杂的多,甚至需要为版本控制生成专用数据,但对于那些需要快速开发的小规模的软件,上述方法可以作为一种选择。
参考文献
1 Visual C++ 6.0 MSDN
2 Visual C++ 6.0 MFC源码