C++自動 序/反序列化在實際軟件開發(fā)中的應(yīng)用
“閱讀本文大概需要 3 分鐘
背景
最近有人私信詢問關(guān)于序列化的問題,我花時間整理了筆記中的內(nèi)容并撰寫了這篇文章,希望它能為大家提供幫助。
在軟件開發(fā)過程中(C++),為了實現(xiàn)對象的持久化和快速加載,我們通常需要將對象的信息進(jìn)行保存和讀取。當(dāng)對象的結(jié)構(gòu)較簡單且字段較少時,我們可以手動進(jìn)行序列化和反序列化操作。然而,在處理大量具有相似結(jié)構(gòu)的對象時,純手工操作會變得繁瑣且效率低下。
為了解決這個問題,需要使用一種能夠自動進(jìn)行序列化/反序列話的方法,提高開發(fā)效率。
方案
不想其他語音那樣有很多庫可以用,使用C++
可用的方案并不多,其中像Boost.Serialization
,Google Protocol Buffers
都是屬于比較重的,小項目完全沒有必要用。下面介紹幾種輕便好用的
cereal
cereal
是一個head only
開源的C ++11 序列化庫,支持自定義數(shù)據(jù)類型序列化成 JSON,XML,二進(jìn)制壓縮,反之也可以反序列化。幾乎不依賴于其他第三方庫(RapidJson,RadidXml),方便快速.
之前專門寫過兩篇使用筆記:
cereal 使用筆記一
cereal 使用筆記二
Qt QObject
借助 Qt
的元對象系統(tǒng),可以非常方便實現(xiàn)。
實現(xiàn)
我們知道 Qt
的元對象系統(tǒng)非常強大,基于此屬性我們可以實現(xiàn)對象的序列化和反序列化操作。
比如有一個學(xué)生類,包含以下幾個字段:學(xué)號、姓名、性別、出生日期等,定義如下類結(jié)構(gòu):
class?DStudent?:?public?QObject
{
????Q_OBJECT
????Q_PROPERTY(QString?name?READ?name?WRITE?setName)
????Q_PROPERTY(QString?number?READ?number?WRITE?setNumber)
????Q_PROPERTY(QString?sex?READ?sex?WRITE?setSex)
????Q_PROPERTY(QDateTime?birthday?READ?birthday?WRITE?setBirthda)
public:
????explicit?DStudent(QObject?*parent?=?nullptr);
?
?QString?name()?const;
????void?setName(const?QString?&newName);
?
????QString?number()?const;
????void?setNumber(const?QString?&newNumber);
????QString?sex()?const;
????void?setSex(const?QString?&newSex);
?
?QDateTime?birthday()?const;
????void?setBirthda(const?QDateTime?&newBirthday);
?
?//...
需要增加那些字段,只需要在上述屬性位置繼續(xù)追加即可,通過把需要反射的字段定義成屬性,我們就可以遍歷該類的元對象,進(jìn)而獲取其中的屬性信息。
序列化 Json
QJsonObject?DStudent::toJson()
{
????QJsonObject?jsObj?=?KJsonHelp::object2Json(this);
????return?jsObj;
}
bool?DStudent::fromJson(const?QJsonObject?&jsObj)
{
????return?KJsonHelp::json2Object(jsObj,?this);
}
核心代碼封裝到工具類中,方便其它地方調(diào)用,詳細(xì)實現(xiàn)如下:
QJsonObject?KJsonHelp::object2Json(QObject?*object)
{
????QJsonObject?jsObj;
????if(nullptr?==?object)
????{
????????return?jsObj;
????}
????const?QMetaObject?*pMetaObj?=?object->metaObject();
????for(int?i?=?0;?i?<?pMetaObj->propertyCount();?i++)
????{
????????auto?proName?=?pMetaObj->property(i).name();
????????jsObj.insert(proName,?QJsonValue::fromVariant(object->property(proName)));
????}
????if(jsObj.contains("objectName"))
????{
????????jsObj.remove("objectName");
????}
????return?jsObj;
}
bool?KJsonHelp::json2Object(const?QJsonObject?&jsObj,?QObject?*object)
{
????if?(jsObj.isEmpty()?||?nullptr?==?object)
????{
????????return?false;
????}
????QStringList?list;
????const?QMetaObject?*pMetaObj?=?object->metaObject();
????for(int?i?=?0;?i?<?pMetaObj->propertyCount();?i++)
????{
????????list?<<?pMetaObj->property(i).name();
????}
????QStringList?jsonKeys?=?jsObj.keys();
????foreach(const?QString?&proName?,list)
????{
????????if(!jsonKeys.contains(proName)?||?jsObj.value(proName).isNull())
????????{
????????????continue;
????????}
????????object->setProperty(proName.toLocal8Bit().data(),?jsObj.value(proName));
????}
????return?true;
}
任意一個繼承 QObject
的對象都可以獲取到它的元對象,接著可以獲取到屬性個數(shù),然后挨個進(jìn)行遍歷即可。
如果想序列化到其他格式的,比如XML,在上述方法中根據(jù) XML
規(guī)則生成即可,這個不是本文的重點。
一些坑和注意點
當(dāng)然了并不是所有的類型都支持這種方式自動生成字段的,一些特殊類型或者自定義的類需要自己特殊去實現(xiàn)。
我們可以在上述學(xué)生類中,繼續(xù)添加新的測試屬性字段,來看看輸出的結(jié)果:
????//?custome?type
????Q_PROPERTY(DScore?sScore?READ?sScore?WRITE?setSScore)
????//?test?other?type
????Q_PROPERTY(int?testInt?READ?testInt?WRITE?setTestInt)
????Q_PROPERTY(bool?testBool?READ?testBool?WRITE?setTestBool)
????Q_PROPERTY(double?testDouble?READ?testDouble?WRITE?setTestDouble)
????Q_PROPERTY(char?testChar?READ?testChar?WRITE?setTestChar)
????Q_PROPERTY(QUrl?testUrl?READ?testUrl?WRITE?setTestUrl)
????Q_PROPERTY(QVariant?testV?READ?testV?WRITE?setTestV)
????Q_PROPERTY(QStringList?testStringList?READ?testStringList?WRITE?setTestStringList)
????Q_PROPERTY(QRect?testRect?READ?testRect?WRITE?setTestRect)
????Q_PROPERTY(QSize?testSize?READ?testSize?WRITE?setTestSize)
????Q_PROPERTY(QPoint?testPoint?READ?testPoint?WRITE?setTestPoint)
????Q_PROPERTY(QList<int>?testIntList?READ?testIntList?WRITE?setTestIntList)
????Q_PROPERTY(QList<QString>?testListString?READ?testListString?WRITE?setTestListString)
打印輸出:
????qRegisterMetaType<DScore>("DScore");
????DStudent?st;
????st.setName(QStringLiteral("法外狂徒張三"));
????st.setNumber("123456789");
????st.setSex(QStringLiteral("男"));
????st.setBirthda(QDateTime::currentDateTime());
????//?test?other?type
????st.setTestInt(10);
????st.setTestBool(true);
????st.setTestV(12);
????st.setTestDouble(12.121212);
????st.setTestChar('k');????????//->string
????st.setTestUrl(QUrl("http://kevinlq.com/"));?//?->?string
????st.setTestStringList(QStringList()?<<?"stringList1"?<<?"stringList2");
????st.setTestRect(QRect(10,10,10,10));?????????????//?null
????st.setTestSize(QSize(10,10));???????????????????//?null
????st.setTestPoint(QPoint(10,?10));????????????????//?null
????st.setTestIntList({11,?12});????????????????????//?null
????st.setTestListString({"kevinlq",?"devstone"});??//?null
????DScore?score;
????score.setName("computer");
????score.setNumber("001");
????st.setSScore(score);????????????????????????????//?null
????qDebug()?<<?"st:"?<<?st.toJson();st:?QJsonObject({"birthday":"2023-08-05T19:33:14.815","name":"法外狂徒張三","number":"123456789","sScore":null,"sex":"男","testBool":true,"testChar":"k","testDouble":12.121212,"testInt":10,"testIntList":null,"testListString":null,"testPoint":null,"testRect":null,"testSize":null,"testStringList":["stringList1","stringList2"],"testUrl":"http://kevinlq.com/","testV":12})
可以看到很多字段的值是 null
,出現(xiàn)這種問題表示這個類型目前無法直接自動生成,如果你缺失需要這種結(jié)構(gòu),那么需要自行在序列化函數(shù)中進(jìn)行特殊處理,比如自定義的類型處理:
QJsonObject?DStudent::toJson()
{
????QJsonObject?jsObj?=?KJsonHelp::object2Json(this);
????jsObj.insert("sScore",?m_sScore.toJson());
????return?jsObj;
}
上述對定義的課程類,進(jìn)行了特殊處理,再次編譯后,輸出的結(jié)果如下:
{
?"birthday":?"2023-08-05T23:27:00.757",
?"name":?"法外狂徒張三",
?"number":?"123456789",
?"sScore":?{
??"name":?"computer",
??"number":?"001"
?}
}
可以看到正常輸出了我們需要的類型,其他類型可以照葫蘆畫瓢。
進(jìn)階
隨著類屬性字段越來越多,手動編寫這些重復(fù)的代碼會顯得非常繁瑣。幸運的是,我們可以使用工具自動生成這些代碼。參考前面的文章,你可以輕松地編寫出更加緊湊的代碼,詳細(xì)教程請見這里:http://kevinlq.com/2023/01/16/generateProperty/
總結(jié)
序列化其實有很應(yīng)用場景,以下是工作中經(jīng)常使用的小 case:
持久化保存類對象數(shù)據(jù)到本地磁盤,重啟軟件后把數(shù)據(jù)反序列化到對象;
socket 傳輸數(shù)據(jù),需要把 json/xml/other 類型轉(zhuǎn)為對象,用對象進(jìn)行業(yè)務(wù)處理;
數(shù)據(jù)庫操作:從 db 中讀取出來的數(shù)據(jù)表字段序列化成對象,方便業(yè)務(wù)進(jìn)行處理(使用 ORM 框架例外)
與界面進(jìn)行交互,比如界面使用 QML 編寫,那么大部分超場景會使用到 JSON進(jìn)行傳參
其他:待補充……
參考文檔
c++自動生成get/set方法
C++ 序列化庫 Cereal使用(一)
C++ 序列化庫 Cereal使用(二)
cereal 官網(wǎng)