QSerializer ist tot, es lebe der QSerializer

Es ist einige Monate her, seit ich hier über mein Qt-basiertes Bibliotheksprojekt zum Serialisieren von Daten aus einer Objektansicht nach JSON / XML und umgekehrt gesprochen habe.



Und egal wie stolz ich auf die gebaute Architektur bin, ich muss zugeben - die Implementierung war offen gesagt kontrovers.



All dies führte zu einer umfassenden Überarbeitung, deren Ergebnisse in diesem Artikel erörtert werden. Für Details - unter dem Schnitt!







QSerializer ist gestorben



QSerializer hatte Nachteile, deren Lösung oft zu einem noch größeren Nachteil wurde. Hier einige davon:



  • Sehr teuer (Serialisierung, Halten von Immobilienverwaltern auf dem Haufen, Kontrollieren der Lebensdauer der Bewahrer usw.)
  • Arbeiten nur mit QObject-basierten Klassen
  • Verschachtelte "komplexe" Objekte und ihre Sammlungen müssen ebenfalls QObject-basiert sein
  • Unfähigkeit, Sammlungen während der Deserialisierung zu ergänzen
  • Nur theoretisch unendliche Verschachtelung
  • Die Unfähigkeit, mit signifikanten Arten von "komplexen" Objekten zu arbeiten, aufgrund des Verbots des Kopierens aus QObject
  • Die Notwendigkeit einer obligatorischen Registrierung von Typen im Qt-Metaobjektsystem
  • Häufige "Bibliotheks" -Probleme wie Verknüpfungs- und Portabilitätsprobleme zwischen Plattformen


Unter anderem wollte ich in der Lage sein, jedes Objekt "hier und jetzt" zu serialisieren, wenn dies eine große Bindung von Methoden im QSerializer-Namespace verwenden musste.



Es lebe QSerializer!



QSerializer war nicht vollständig. Es war notwendig, eine Lösung zu finden, bei der der Benutzer nicht vom QObject abhängig ist, sondern mit Werttypen und zu geringeren Kosten arbeiten kann.



In einem Kommentar zum vorherigen Artikel der BenutzerMikrolabemerkte, dass Sie über die Verwendung von Q_GADGET nachdenken können .



Vorteile Q_GADGET :



  • Keine Einschränkungen beim Kopieren
  • Verfügt über eine statische Instanz von QMetaObject, um auf Eigenschaften zuzugreifen


Unter Berufung auf Q_GADGET musste ich den Ansatz zum Erstellen von JSON und XML basierend auf den deklarierten Klassenfeldern überdenken. Das Problem der "hohen Kosten" manifestierte sich hauptsächlich aufgrund von:



  • Große Speicherklassengröße (mindestens 40 Byte)
  • Zuweisen eines Heaps für neue Guardian-Entitäten für jede Eigenschaft und Steuern ihrer TTL


Um die Kosten zu senken, habe ich folgende Anforderung formuliert:

Das Vorhandensein von Drahtmethoden zum Serialisieren / Deserialisieren aller Eigenschaften der Klasse in jedem serialisierbaren Objekt und das Vorhandensein von Methoden zum Lesen und Schreiben von Werten für jede Eigenschaft unter Verwendung des für diese Eigenschaft zugewiesenen Formats

Makros



Es ist nicht einfach, die starke Typisierung von C ++ zu umgehen, die die automatische Serialisierung erschwert, und frühere Erfahrungen haben dies gezeigt. Makros hingegen können eine große Hilfe bei der Lösung eines solchen Problems sein (fast das gesamte Qt-Metaobjektsystem basiert auf Makros), da Sie mithilfe von Makros die Codegenerierung von Methoden und Eigenschaften durchführen können.



Ja, Makros sind in ihrer reinsten Form oft böse - es ist fast unmöglich, sie zu debuggen. Ich könnte das Schreiben eines Makros zum Generieren von Code mit dem Anbringen eines Kristallschuhs an der Ferse Ihres Chefs vergleichen, aber schwierig bedeutet nicht unmöglich!



Lyrischer Exkurs über Makros

— , , «» (). .



QSerializer bietet derzeit zwei Möglichkeiten, eine Klasse als serialisierbar zu deklarieren: Erben von der QSerializer-Klasse oder Verwenden des QS_CLASS- Codegenerierungsmakros .



Zunächst müssen Sie das Makro Q_GADGET im Hauptteil der Klasse definieren. Dadurch erhalten Sie Zugriff auf das staticMetaObject. Es speichert die von den Makros generierten Eigenschaften.



Durch das Erben von QSerializer können Sie mehrere serialisierbare Objekte in einen Typ umwandeln und in großen Mengen serialisieren.



Die QSerializer-Klasse enthält 4 Explorer-Methoden, mit denen Sie die Eigenschaften eines Objekts analysieren können, und eine virtuelle Methode, um eine Instanz eines QMetaObject abzurufen:



QJsonValue toJson() const
void fromJson(const QJsonValue &)
QDomNode toXml() const
void fromXml(const QDomNode &)
virtual const QMetaObject * metaObject() const


Q_GADGET verfügt nicht über die gesamte von Q_OBJECT bereitgestellte Metaobjektbindung .



Innerhalb des QSerializers repräsentiert die staticMetaObject-Instanz die QSerializer-Klasse, leitet sie jedoch in keiner Weise ab. Wenn Sie also die QSerializer-basierte Klasse erstellen, müssen Sie die metaObject-Methode überschreiben. Sie können dem Klassenkörper das Makro QS_SERIALIZER hinzufügen, das die metaObject-Methode für Sie überschreibt.



Wenn Sie staticMetaObject verwenden, anstatt eine QMetaObject-Instanz in jedem Objekt zu speichern, werden 40 Bytes von der Klassengröße eingespart, im Allgemeinen also Schönheit!



Wenn Sie aus irgendeinem Grund nicht erben möchten, können Sie das Makro QS_CLASS im Hauptteil der serialisierten Klasse definierenEs werden alle erforderlichen Methoden generiert, anstatt von QSerializer zu erben.



Deklaration der Felder



Unabhängig davon gibt es in JSON und XML vier Arten von serialisierbaren Daten, ohne die die Serialisierung in diese Formate nicht vollständig ist. Die Tabelle zeigt die Datentypen und die entsprechenden Makros zur Beschreibung:

Datentyp Beschreibung Makro
Feld gewöhnliches Feld vom primitiven Typ (verschiedene Zahlen, Zeichenfolgen, Flags) QS_FIELD
Sammlung Wertesatz primitiver Datentypen QS_COLLECTION
ein Objekt komplexe Struktur von Feldern oder anderen komplexen Strukturen QS_OBJECT
Sammlung von Objekten eine Reihe komplexer Datenstrukturen des gleichen Typs QS_COLLECTION_OBJECTS


Wir gehen davon aus, dass der Code, der diese Makros generiert, als Beschreibung bezeichnet wird und die Makros, die sie generieren, als beschreibend bezeichnet werden.



Es gibt nur ein Prinzip zum Generieren einer Beschreibung: Generieren Sie für ein bestimmtes Feld JSON- und XML-Eigenschaften und definieren Sie Methoden zum Schreiben / Lesen von Werten.



Lassen Sie uns die Generierung einer JSON-Beschreibung am Beispiel eines primitiven Datentypfelds analysieren:



/* Create JSON property and methods for primitive type field*/
#define QS_JSON_FIELD(type, name)                                                           
    Q_PROPERTY(QJsonValue name READ get_json_##name WRITE set_json_##name)                  
    private:                                                                                
        QJsonValue get_json_##name() const {                                                
            QJsonValue val = QJsonValue::fromVariant(QVariant(name));                       
            return val;                                                                     
        }                                                                                   
        void set_json_##name(const QJsonValue & varname){                                   
            name = varname.toVariant().value<type>();                                       
        }   
...
int digit;
QS_JSON_FIELD(int, digit)  


Für das int-Ziffernfeld wird eine Eigenschaftsziffer mit dem Typ QJsonValue generiert und private Schreib- und Lesemethoden - get_json_digit und set_json_digit werden definiert. Diese werden dann zu Leitern für die Serialisierung / Deserialisierung des Ziffernfelds mit JSON.



Wie kommt es dazu?
name digit, ('##') digit — .



type int. , type int . QVariant int .



Und hier ist die Generierung einer JSON-Beschreibung für eine komplexe Struktur:



/* Generate JSON-property and methods for some custom class */
/* Custom type must be provide methods fromJson and toJson */
#define QS_JSON_OBJECT(type, name)
    Q_PROPERTY(QJsonValue name READ get_json_##name WRITE set_json_##name)
    private:
    QJsonValue get_json_##name() const {
        QJsonObject val = name.toJson();
        return QJsonValue(val);
    }
    void set_json_##name(const QJsonValue & varname) {
        if(!varname.isObject())
        return;
        name.fromJson(varname);
    } 
...
SomeClass object;
QS_JSON_OBJECT(SomeClass, object)


Komplexe Objekte sind eine Reihe verschachtelter Eigenschaften, die als eine "große" Eigenschaft für eine externe Klasse fungieren, da solche Objekte auch über Drahtmethoden verfügen. Dazu müssen Sie lediglich die entsprechende Führungsmethode in den Lese- und Schreibmethoden komplexer Strukturen aufrufen.



Klassenerstellung



Somit haben wir eine ziemlich einfache Infrastruktur zum Erstellen einer serialisierbaren Klasse.



So können Sie beispielsweise eine Klasse serialisierbar machen, indem Sie von QSerializer erben:



class SerializableClass : public QSerializer {
Q_GADGET
QS_SERIALIZER
QS_FIELD(int, digit)
QS_COLLECTION(QList, QString, strings)
};


Oder wie folgt mit dem Makro QS_CLASS :



class SerializableClass {
Q_GADGET
QS_CLASS
QS_FIELD(int, digit)
QS_COLLECTION(QList, QString, strings)
};


Beispiel für eine JSON-Serialisierung
:



class CustomType : public QSerializer {
Q_GADGET
QS_SERIALIZER
QS_FIELD(int, someInteger)
QS_FIELD(QString, someString)
};

class SerializableClass : public QSerializer {
Q_GADGET
QS_SERIALIZER
QS_FIELD(int, digit)
QS_COLLECTION(QList, QString, strings)
QS_OBJECT(CustomType, someObject)
QS_COLLECTION_OBJECTS(QVector, CustomType, objects)
};


, :



SerializableClass serializable;
serializable.someObject.someString = "ObjectString";
serializable.someObject.someInteger = 99999;
for(int i = 0; i < 3; i++) {
    serializable.digit = i;
    serializable.strings.append(QString("list of strings with index %1").arg(i));
    serializable.objects.append(serializable.someObject);
}
QJsonObject json = serializable.toJson();


JSON:



{
    "digit": 2,
    "objects": [
        {
            "someInteger": 99999,
            "someString": "ObjectString"
        },
        {
            "someInteger": 99999,
            "someString": "ObjectString"
        },
        {
            "someInteger": 99999,
            "someString": "ObjectString"
        }
    ],
    "someObject": {
        "someInteger": 99999,
        "someString": "ObjectString"
    },
    "strings": [
        "list of strings with index 0",
        "list of strings with index 1",
        "list of strings with index 2"
    ]
}


— , XML , toJson toXml.



example.



Einschränkungen



Einzelfelder



Benutzerdefinierte oder primitive Typen müssen einen Standardkonstruktor bereitstellen.



Sammlungen



Die Sammlungsklasse muss mit Vorlagen versehen sein und die Methoden clear, at, size und append bereitstellen. Sie können Ihre eigenen Sammlungen verwenden, abhängig von den Bedingungen. Qt-Sammlungen, die diese Bedingungen erfüllen: QVector, QStack, QList, QQueue.



Qt-Versionen



Minimale Version Qt 5.5.0

Minimale getestete Version Qt 5.9.0

Maximale getestete Version Qt 5.15.0 HINWEIS

: Sie können QSerializer auf früheren Versionen von Qt testen und testen



Ergebnis



Bei der Überarbeitung von QSerializer habe ich mir absolut nicht die Aufgabe gestellt, es signifikant zu reduzieren. Die Größe ging jedoch von 9 Dateien auf 1 zurück, was auch die Komplexität verringerte. Jetzt ist QSerializer keine Bibliothek mehr in unserer üblichen Form, sondern nur noch eine Header-Datei, die ausreicht, um in das Projekt aufgenommen zu werden und alle Funktionen für eine komfortable Serialisierung / Deserialisierung zu erhalten. Die Entwicklung begann im März, eine knifflige Architektur wurde erfunden und das Projekt wurde mit Abhängigkeiten, Krücken überwachsen, die mehrmals von 0 umgeschrieben wurden. Und das alles, um sich letztendlich in eine kleine Datei zu verwandeln.



Wenn ich mich frage: "War er die Mühe wert?", Werde ich antworten: "Ja, das war er." Ich habe es bereits bei meinen Kampfprojekten versucht und das Ergebnis hat mir gefallen.



Links

GitHub: Link

Neueste Version: v1.1

Vorheriger Artikel: QSerializer: Lösung für einfache JSON / XML-Serialisierung



Zukünftige Liste



  • Erhebliche Kostenreduzierung (noch günstiger)
  • Kompaktheit
  • Arbeiten mit signifikanten Typen
  • Grundlegende Beschreibung serialisierbarer Daten
  • Unterstützung für alle Vorlagenvorlagen, die eindeutige Methoden für Größe, Größe und Anhängen bereitstellen. Sogar ihre eigenen
  • Vollständig veränderbare Sammlungen zur Deserialisierung
  • Unterstützung für alle gängigen primitiven Typen
  • Unterstützung für alle mit QSerializer beschriebenen benutzerdefinierten Typen
  • Benutzerdefinierte Typen müssen nicht registriert werden



All Articles