翻译:
The serialization primer(本文源自http://www.codeproject.com, 查看原文:http://www.codeproject.com/KB/cpp/serialization_primer1.aspx )
教程分为三部分,我就不再分直接放在一起。
l 第一部分 介绍序列化的基础。
l 第二部分 解释怎样处理读取无效数据和版本支持。
l 第三部分 介绍怎样对复杂的对象进行序列化。
第一部分
序列化是指将一个对象读取或者写入一个持久存储介质,比如硬盘文件。序列化一个对象需要三个成分:
l 一个代表数据文件的CFile对象
l 一个提供序列化上下文的CArchive对象
l 需要序列化的对象。
第一步:打开数据文件
将一个对象序列化到文件“foo.dat”,需要以合适的访问方式打开一个文件。在这个例子中,文件以单独读写的方式打开。
// Open file "foo.dat"
CFile* pFile = new CFile();
ASSERT (pFile != NULL);
if (!pFile->Open ("foo.dat", CFile::modeReadWrite | CFile::shareExclusive)) {
// Handle error
return;
}
第二步:关联CArchive
接下来,将一个CArchive对象关联到文件。CArchive提供了一个有效的渠道来进行持久存储。你可以通过这个对象来序列化数据而不是直接读写文件,但是它需要知道你是用它来写入数据还是读取数据。在这个例子里,我们假定要写数据。
// Create archive ...
bool bReading = false; // ... for writing
CArchive* pArchive = NULL;
try
{
pFile->SeekToBegin();
UINT uMode = (bReading ? CArchive::load : CArchive::store);
pArchive = new CArchive (pFile, uMode);
ASSERT (pArchive != NULL);
}
catch (CException* pException)
{
// Handle error
return;
}
第三步:序列化对象
最终我们将调用对象的serialize()函数来序列化一个对象。serialize()函数只是我们自己写的一个函数,与MFC中的CObject::Serialize()没有关系。同时,你也不需要继承自CObject。这个serialize()函数使用CArchive的一个指针并返回一个整型类型的状态值。
int CFoo::serialize
(CArchive* pArchive)
{
int nStatus = SUCCESS;
// Serialize the object ...
...
return (nStatus);
}
我们可以在一分钟之内得到一个实际的序列化过程。同时,让我们注意以下两个重点:
l CFoo::serialize()函数用来将一个对象写入到持久存储介质或者从持久存储介质上读取数据
l CFoo::serialize()函数不知道将要访问的数据文件的任何信息。
假设CFoo代表一个包含两个成员变量职工记录。
class CFoo
{
// Construction/destruction
public:
CFoo::CFoo();
virtual CFoo::~CFoo();
// Methods
public:
int serialize (CArchive* pArchive);
// Data members
public:
CString m_strName; // employee name
int m_nId; // employee id
};
我们使用CArchive的流操作符<<和>>向存储介质上读写数据。CArchive知道怎样去序列化像int, float或者DWORD的数据类型,或者像CString的对象。同时它也知道它是在读还是在写。你可以用CArchive::IsStoring()函数来查询它的读写状态。CFoo的序列化函数可以写成下面的样子:
int CFoo::serialize
(CArchive* pArchive)
{
int nStatus = SUCCESS;
// Serialize the object ...
ASSERT (pArchive != NULL);
try
{
if (pArchive->IsStoring()) {
// Write employee name and id
(*pArchive) << m_strName;
(*pArchive) << m_nId;
}
else {
// Read employee name and id
(*pArchive) >> m_strName;
(*pArchive) >> m_nId;
}
}
catch (CException* pException)
{
nStatus = ERROR;
}
return (nStatus);
}
第四步:清除资源
当结束序列化时,你需要关闭CArchive的对象和数据文件对象,并清理资源。
pArchive->Close();
delete pArchive;
pFile->Close();
delete pFile();
总结
到现在,我们有了一个非常小的序列化雏形。在第二部分,我们将看看怎样处理读取非法数据和支持对象的不同版本。在第三部分我们将序列化一个复杂的对象。
第二部分
在第一部分我们知道了怎样通过CArchive使用serilize()函数来序列化一个简单的对象:
int CFoo::serialize
(CArchive* pArchive)
{
int nStatus = SUCCESS;
// Serialize the object ...
ASSERT (pArchive != NULL);
TRY
{
if (pArchive->IsStoring()) {
// Write employee name and id
(*pArchive) << m_strName;
(*pArchive) << m_nId;
}
else {
// Read employee name and id
(*pArchive) >> m_strName;
(*pArchive) >> m_nId;
}
}
CATCH_ALL (pException)
{
nStatus = ERROR;
}
END_CATCH_ALL
return (nStatus);
}
这个代码存在一个问题。当我们读取的数据文件不存在我们期望的数据时怎么办?如果数据文件在int数据后没有一个CString对象,我们的serialize()函数将会返回ERROR。这样处理非常好,但是如果我们可以提前知晓这个状况并且返回一个更加详细的状态码INVALID_DATAFILE会更好。我们可以使用一个签名来检查是否正在读写一个合法的数据文件(例如,包含一个CFoo类)。
对象签名
一个对象签名就是一个表示一个对象的字符串(比如:“FooObject”)。我们修改类定义来给CFoo添加一个签名:
class CFoo
{
...
// Methods
public:
...
CString getSignature();
// Data members
...
protected:
static const CString Signature; // object signature
};
这个签名在Foo.cpp文件中声明。
// Static constants
const CString CFoo::Signature = "FooObject";
接下来,我们修改serialize()函数,使它在存储两个成员变量前先存储签名。如果遇到一个非法的签名或者丢失了签名,那么就有可是是我们存取的数据存储介质不包含CFoo对象。这是读写逻辑就变成了下面的样子:
代码如下:
int CFoo::serialize
(CArchive* pArchive)
{
int nStatus = SUCCESS;
bool bSignatureRead = false;
// Serialize the object ...
ASSERT (pArchive != NULL);
TRY
{
if (pArchive->IsStoring()) {
// Write signature
(*pArchive) << getSignature();
// Write employee name and id
(*pArchive) << m_strName;
(*pArchive) << m_nId;
}
else {
// Read signature - complain if invalid
CString strSignature;
(*pArchive) >> strSignature;
bSignatureRead = true;
if (strSignature.Compare (getSignature()) != 0) {
return (INVALID_DATAFILE);
}
// Read employee name and id
(*pArchive) >> m_strName;
(*pArchive) >> m_nId;
}
}
CATCH_ALL (pException)
{
nStatus = bSignatureRead ? ERROR : INVALID_DATAFILE;
}
END_CATCH_ALL
return (nStatus);
}
你必须保证你的对象签名是唯一的。它的重要性仅次于签名本身。如果你开发了一个产品套间,这对于你注册公司产品签名非常有用。这样的话,开发人员不会在不同的对象中使用相同的签名。如果你想扭转组织数据文件的工程,你需要使用不关联对象名称的签名。
版本信息
当你在你的产品生命周期中升级时,你可以发现方便的添加删除CFoo结构的成员函数非常有必要。如果你发布一个新版本的CFoo,尝试独缺老版本存储的数据文件会失败。这样是不可以接受的。任何版本的CFoo都应该能够从老版本的序列化中恢复。换句话说,CFoo序列化的函数要能够向后兼容。在对象中使用版本信息很容易实现。就像我们添加一个签名一样,我们添加一个整型数来表示版本号。
class CFoo
{
...
// Methods
public:
...
CString getSignature();
int getVersion();
// Data members
...
protected:
static const CString Signature; // object signature
static const int Version; // object version
};
在CFoo的版本1中有两个成员函数:一个CString类型的m_strName和一个int类型的m_nId。如果在版本2添加第三个成员(比如:int m_nDept),那么我们在读取老版本的对象时就需要决定什么时候初始化m_nDept。如果老版本的对象我们就将这个变量初始化为-1。
class CFoo
{
...
// Data members
public:
CString m_strName; // employee name
int m_nId; // employee id
int m_nDept; // department code (-1 = unknown)
};
同样,我们需要在新版本中加入:
const int CFoo::Version = 2;
最后,我们修改serialize()函数,当我们读到旧版本的数据文件时就将m_nDept赋值为-1。需要注意的是文件存储总是保存为最新的版本。
int CFoo::serialize
(CArchive* pArchive)
{
...
// Serialize the object ...
ASSERT (pArchive != NULL);
TRY
{
if (pArchive->IsStoring()) {
...
// Write employee name, id and department code
(*pArchive) << m_strName;
(*pArchive) << m_nId;
(*pArchive) << m_nDept;
}
else {
...
// Read employee name and id
(*pArchive) >> m_strName;
(*pArchive) >> m_nId;
// Read department code (new in version 2)
if (nVersion >= 2) {
(*pArchive) >> m_nDept;
}
else {
m_nDept = -1; // unknown
}
}
}
CATCH_ALL (pException)
{
nStatus = bSignatureRead && bVersionRead ? ERROR : INVALID_DATAFILE;
}
END_CATCH_ALL
return (nStatus);
}
总结
到此为止,第二部分完成,待第三部分…
第三部分
在前两个部分,我们学习了怎样提供简单的序列化,在这里,我们要学习序列化任何类型对象的规则。下面要考虑四种情况,每一种都建立在前一种之上。
l 序列化一个简单的类
l 序列化一个继承的类
l 序列化一个同质集合类
l 序列化一个异质集合类
我们的serialize()函数将返回以下几个状态码之一:
l Success
l InvalidFormat
l UnsupportedVersion
l ReadError
l WriteError
序列化一个简单的类
一个简单的类是指一个对象没有父类并且不是一个集合类。序列化这样一个类,需要这样做:
1. 序列化类的签名和版本信息
2. 序列化类的成员变量
在下面的例子里,Point类包含两个int类型的表示一个坐标点成员变量。这个对象的签名和版本信息定义static成员变量(m_strSignature和m_nVersion),并用它们来表示Point的实例
int Point::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
// Step 1: Serialize signature and version
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << Point::m_strSignature;
(*pArchive) << Point::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != Point::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > Point::m_nVersion;)
return (Status::UnsupportedVersion);
}
// Step 2: Serialize members
if (pArchive->IsStoring()) {
(*pArchive) << m_nX;
(*pArchive) << m_nY;
} else {
(*pArchive) >> m_nX;
(*pArchive) >> m_nY;
}
}
catch (CException* pException) {
// A read/write error occured
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
// Object was successfully serialized
return (Status::Success);
}
序列化一个继承的类
在本文中,一个继承的类是指一个继承自简单类的类并且不是集合类。序列化这样的类,需要这样做:
1. 序列化类的签名和版本信息
2. 序列化对象基类<<额外步骤
3. 序列化类的成员变量
下面的例子中,ColoredPoint类继承自Point并且包含一个额外的int成员函数m_nColor来制定坐标点的颜色。和所有的序列化类一样,它也包含版本和签名信息。
int ColoredPoint::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
// Step 1: Serialize signature and version
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ColoredPoint::m_strSignature;
(*pArchive) << ColoredPoint::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ColoredPoint::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ColoredPoint::m_nVersion;)
return (Status::UnsupportedVersion);
}
// Step 2: Serialize the base class
int nStatus = Point::serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
// Step 3: Serialize members
if (pArchive->IsStoring())
(*pArchive) << m_nColor;
else
(*pArchive) >> m_nColor;
}
catch (CException* pException) {
// A read/write error occured
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
// Object was successfully serialized
return (Status::Success);
}
序列化一个同质集合类
同质集合类用来存储同一个类型的动态大小的对象集合。序列化这样的类,需要这样做:
1. 序列化类的签名和版本信息
2. 序列化基类
3. 序列化集合内条目的数据<<额外步骤
4. 序列化集合内的每个对象<<额外步骤
5. 序列化类的成员变量
在这个例子中,ColoredPointList是ColoredPoint对象的集合。为了保持简单,ColoredPointList使用CPtrArray来存储对象。ColoredPointList同样含有版本信息和签名,下面是ColoredPointList的定义:
class ColoredPointList
{
// Construction/destruction
public:
ColoredPointList::ColoredPointList();
virtual ColoredPointList::~ColoredPointList();
// Attributes
public:
static const CString m_strSignature;
static const int m_nVersion;
// Operations
public:
int serialize (CArchive* pArchive);
// Members
protected:
CPtrArray m_coloredPoints;
}
下面是我们序列化这个类的代码:
int ColoredPointList::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
int nStatus = Status::Success;
// Step 1: Serialize signature and version
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ColoredPointList::m_strSignature;
(*pArchive) << ColoredPointList::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ColoredPointList::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ColoredPointList::m_nVersion;)
return (Status::UnsupportedVersion);
}
// Step 2: Serialize base class (if any)
//
// Nothing to do since ColoredPointList isn't derived from anything.
// But if it was derived from BaseColoredPointList, we'd do:
//
// nStatus = BaseColoredPointList::serialize (pArchive);
// if (nStatus != Status::Success)
// return (nStatus);
// Step 3: Serialize number of items in collection
int nItems = 0;
if (pArchive->IsStoring()) {
nItems = m_coloredPoints.GetSize();
(*pArchive) << nItems;
} else
(*pArchive) >> nItems;
// Step 4: Serialize each object in collection
for (int nObject=0; (nObject < nItems); nObject++) {
// 4a: Point to object being serialized
ColoredPoint* pColoredPoint = NULL;
if (pArchive->IsStoring())
pColoredPoint = (ColoredPoint *) m_coloredPoints.GetAt (nObject);
else
pColoredPoint = new ColoredPoint();
ASSERT (pColoredPoint != NULL);
// 4b: Serialize it
nStatus = pColoredPoint->serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
if (!pArchive->IsStoring())
m_coloredPoints.Add (pColoredPoint);
}
// Step 5: Serialize object's other members (if any)
//
// Nothing to do since ColoredPointList doesn't have any other
// members. But if it contained an int (m_nSomeInt) and a Foo
// object (m_foo), we'd do:
//
// if (pArchive->IsStoring())
// (*pArchive) << m_nSomeInt;
// else
// (*pArchive) >> m_nColor;
//
// nStatus = m_foo::serialize (pArchive);
// if (nStatus != Status::Success)
// return (nStatus);
}
catch (CException* pException) {
// A read/write error occured
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
// Object was successfully serialized
return (Status::Success);
}
序列化一个异质集合类
移植集合类用来存储动态大小的不同类型的集合对象。序列化内容为:
1. 序列化类的签名和版本信息
2. 序列化基类
3. 序列化集合内条目的数据<<额外步骤
4. 序列化集合内的每个对象<<额外步骤
a) 序列化这个对象的签名
b) 序列化这个对象
5. 序列化类的成员变量
你可以注意到只有在第四步中增加了一个处理步骤。这里我们再序列化每个对象本身之前先序列化他的签名。这样做是为了方便的读取数据。当我们序列化一个同质集合类,我们只需要按照相同的类型解析。在读取ColoredPoint时,我们在堆中创建一个对象并调用它的serialize()函数就可以了。
ColoredPoint* pColoredPoint = new ColoredPoint();
nStatus = pColoredPoint->serialize (pArchive);
当我们读取一个异质集合类,我们需要知道要读取对象的类型,这是对象的签名起到了作用。
// Read object signature
CString strSignature;
pArchive >> strSignature;
// Construct object of appropriate type
ISerializable* pObject = NULL;
if (strSignature == ColoredPoint::m_strSignature)
pObject = new ColoredPoint();
else
if (strSignature == Line::m_strSignature)
pObject = new Line();
else
if (strSignature == Rectangle::m_strSignature)
pObject = new Rectangle();
else
return (Status::InvalidFormat);
ASSERT (pObject != NULL);
// Read it back in
nStatus = pObject->serialize (pArchive);
上面的片断中,ColoredPoint,Line和Rectangle同时继承于相同的基类ISerializable,ISerializable是一个只包含虚函数的接口类,并定义成员函数getSignature(),getVersion()和serialize()。
class ISerializable
{
// Construction/destruction
public:
ISerializable::ISerializable()
{ }
virtual ISerializable::~ISerializable()
{ }
// Operations
public:
// Get the object's signature
virtual CString getSignature() = 0;
// Get the object's version
virtual int getVersion() = 0;
// Serialize the object
virtual int serialize (CArchive* pArchive) = 0;
}
OK, 让我们序列化这个类,下面的例子,ShapeList类是不同类型的成员对象(ColoredPoint
, Line
和Rectangle
)的集合。所有的对象类都继承自接口类ISerializable
。
int ShapeList::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
int nStatus = Status::Success;
// Step 1: Serialize signature and version
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ShapeList::m_strSignature;
(*pArchive) << ShapeList::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ShapeList::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ShapeList::m_nVersion;)
return (Status::UnsupportedVersion);
}
// Step 2: Serialize base class (if any)
//
// Nothing to do since ShapeList isn't derived from anything.
// But if it was derived from BaseShapeList, we'd do:
//
// nStatus = BaseShapeList::serialize (pArchive);
// if (nStatus != Status::Success)
// return (nStatus);
// Step 3: Serialize number of items in collection
int nItems = 0;
if (pArchive->IsStoring()) {
nItems = m_shapes.GetSize();
(*pArchive) << nItems;
} else
(*pArchive) >> nItems;
// Step 4: Serialize each object in collection
for (int nObject=0; (nObject < nItems); nObject++) {
// 4a: First serialize object's signature
CString strSignature;
if (pArchive->IsStoring())
(*pArchive) << pObject->getSignature();
else
(*pArchive) >> strSignature;
//
// 4b: Then serialize object
//
// 4b (1): Point to object being serialized
ISerializable* pObject = NULL;
if (pArchive->IsStoring())
pObject = (ISerializable *) m_shapes.GetAt (nObject);
else {
if (strSignature == ColoredPoint::m_strSignature)
pObject = new ColoredPoint();
else
if (strSignature == Line::m_strSignature)
pObject = new Line();
else
if (strSignature == Rectangle::m_strSignature)
pObject = new Rectangle();
else
return (Status::InvalidFormat);
}
ASSERT (pObject != NULL);
// 4b (2): Serialize it
nStatus = pObject->serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
if (!pArchive->IsStoring())
m_shapes.Add (pObject);
}
// Step 5: Serialize object's other members (if any)
//
// Nothing to do since ShapeList doesn't have any other
// members. But if it contained an int (m_nSomeInt) and
// a Foo object (m_foo), we'd do:
//
// if (pArchive->IsStoring())
// (*pArchive) << m_nSomeInt;
// else
// (*pArchive) >> m_nColor;
//
// nStatus = m_foo::serialize (pArchive);
// if (nStatus != Status::Success)
// return (nStatus);
}
catch (CException* pException) {
// A read/write error occured
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
// Object was successfully serialized
return (Status::Success);
}
类工厂
你可以将代码中讨厌的if块用类工厂来替换,你可以参考以下文章:
- Generic Class Factory by Robert A. T. Káldy
- The Factory Method (Creational) Design Pattern by Gopalan Suresh Raj
- Abstract Factory Pattern by Mark Grand
尽管这些文章看起来比较复杂,但是都蕴含一个简单的道理。类工厂只比一个类多了一个静态成员函数Create()来创建一个制定类型的对象。你可以使用类工厂的Create()函数来隐藏丑陋的if语句块,使代码更加简洁。
...
// Construct object of appropriate type
ISerializable* pObject = MyClassFactory::create (strSignature);
ASSERT (pObject != NULL);
...
总结
尽管这个入门手册使用了MFC的CString和CPtrArray对象,但是序列化过程不是专门针对MFC的。它可以操作存储任何软件中的任何信息到持久存储介质。