E não importa o quão orgulhoso eu esteja da arquitetura construída, devo admitir - a implementação acabou sendo, francamente, controversa.
Tudo isso resultou em uma revisão em larga escala, cujos resultados serão discutidos neste artigo. Para obter detalhes - por baixo do corte!

QSerializer morreu
QSerializer tinha desvantagens, a solução das quais muitas vezes se tornou uma desvantagem ainda maior, aqui estão algumas delas:
- Muito caro (serialização, manutenção de detentores de propriedade na pilha, controle da vida útil dos detentores, etc.)
- Trabalhando apenas com classes baseadas em QObject
- Objetos "complexos" aninhados e suas coleções também devem ser baseados em QObject
- Incapacidade de suplementar coleções durante a desserialização
- Apenas aninhamento teoricamente infinito
- A incapacidade de trabalhar com tipos significativos de objetos "complexos", devido à proibição de cópia do QObject
- A necessidade de registro obrigatório de tipos no sistema de meta-objetos Qt
- Problemas comuns de "biblioteca", como problemas de vinculação e portabilidade entre plataformas
Entre outras coisas, eu queria ser capaz de serializar qualquer objeto "aqui e agora" quando tivesse que usar uma enorme associação de métodos no namespace QSerializer para isso.
Viva o QSerializer!
QSerializer não foi concluído. Era necessário chegar a uma solução em que o usuário não dependesse do QObject, fosse possível trabalhar com tipos de valor e com um custo menor.
Em um comentário ao artigo anterior , o usuáriomicrolanotei que você pode pensar em usar Q_GADGET .
Vantagens Q_GADGET :
- Sem restrições de cópia
- Tem uma instância estática de QMetaObject para acessar propriedades
Contando com Q_GADGET , tive que reconsiderar a abordagem de como criar JSON e XML com base nos campos de classe declarados. O problema de "alto custo" se manifestou principalmente devido a:
- Grande tamanho de classe de armazenamento (pelo menos 40 bytes)
- Alocar um heap para novas entidades guardiãs para cada propriedade e controlar seu TTL
Para reduzir o custo, formulei o seguinte requisito:
A presença em cada objeto serializável de métodos de fio para serialização / desserialização de todas as propriedades da classe e a presença de métodos para ler e escrever valores para cada propriedade usando o formato atribuído a esta propriedade
Macros
Contornar a forte tipagem de C ++, que complica a serialização automática, não é fácil, e a experiência anterior mostrou isso. As macros, por outro lado, podem ser uma grande ajuda para resolver tal problema (quase todo o sistema de meta-objetos Qt é construído em macros), porque usando macros, você pode fazer a geração de código de métodos e propriedades.
Sim, as macros geralmente são más em sua forma mais pura - são quase impossíveis de depurar. Eu poderia comparar a escrita de uma macro para gerar código a colocar um sapato de cristal no calcanhar do seu chefe, mas difícil não significa impossível!
Digressão lírica sobre macros
— , , «» (). .
O QSerializer atualmente fornece 2 maneiras de declarar uma classe como serializável: herdar da classe QSerializer ou usar a macro de geração de código QS_CLASS .
Em primeiro lugar, é necessário definir a macro Q_GADGET no corpo da classe, isso dá acesso ao staticMetaObject, ele armazenará as propriedades geradas pelas macros.
Herdar de QSerializer permitirá que você converta vários objetos serializáveis em um tipo e os serialize em massa.
A classe QSerializer contém 4 métodos explorer que permitem analisar as propriedades de um objeto e um método virtual para obter uma instância de um QMetaObject:
QJsonValue toJson() const
void fromJson(const QJsonValue &)
QDomNode toXml() const
void fromXml(const QDomNode &)
virtual const QMetaObject * metaObject() const
Q_GADGET não tem todas as ligações de metaobjetos que Q_OBJECT fornece .
Dentro do QSerializer, a instância staticMetaObject representará a classe QSerializer, mas não derivará dela de forma alguma, portanto, ao criar a classe baseada em QSerializer, você deve substituir o método metaObject. Você pode adicionar a macro QS_SERIALIZER ao corpo da classe e ela substituirá o método metaObject para você.
Além disso, usar staticMetaObject em vez de armazenar uma instância de QMetaObject em cada objeto economiza 40 bytes do tamanho da classe, bem, em geral, beleza!
Se você não quiser herdar por algum motivo, você pode definir a macro QS_CLASS no corpo da classe serializada, ele irá gerar todos os métodos necessários em vez de herdar de QSerializer.
Declaração de campos
Separadamente, existem 4 tipos de dados serializáveis em JSON e XML, sem os quais a serialização para esses formatos não será completa. A tabela mostra os tipos de dados e as macros correspondentes como forma de descrever:
Tipo de dados | Descrição | Macro |
---|---|---|
campo | campo comum de tipo primitivo (vários números, strings, bandeiras) | QS_FIELD |
coleção | conjunto de valores de tipos de dados primitivos | QS_COLLECTION |
um objeto | estrutura complexa de campos ou outras estruturas complexas | QS_OBJECT |
coleção de objetos | um conjunto de estruturas de dados complexas do mesmo tipo | QS_COLLECTION_OBJECTS |
Vamos supor que o código que gera essas macros é chamado de descrição e as macros que o geram são chamadas de descritivas.
Existe apenas um princípio para gerar uma descrição - para um campo específico, gere propriedade JSON e XML e defina métodos para escrever / ler valores.
Vamos analisar a geração de uma descrição JSON usando o exemplo de um campo de tipo de dados primitivo:
/* 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)
Para o campo de dígito interno, um dígito de propriedade com o tipo QJsonValue será gerado e métodos privados de gravação e leitura - get_json_digit e set_json_digit serão definidos, eles se tornarão condutores para serializar / desserializar o campo de dígito usando JSON.
Como isso acontece?
name digit, ('##') digit — .
type int. , type int . QVariant int .
type int. , type int . QVariant int .
E aqui está a geração de uma descrição JSON para uma estrutura complexa:
/* 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)
Objetos complexos são um conjunto de propriedades aninhadas, que para a classe externa funcionarão como uma propriedade "grande", porque tais objetos também terão métodos de ligação. Tudo que você precisa fazer para isso é chamar o método de guia apropriado nos métodos de leitura e gravação de estruturas complexas.
Criação de classe
Portanto, temos uma infraestrutura bastante simples para a criação de uma classe serializável.
Assim, por exemplo, você pode tornar uma classe serializável herdando de QSerializer:
class SerializableClass : public QSerializer {
Q_GADGET
QS_SERIALIZER
QS_FIELD(int, digit)
QS_COLLECTION(QList, QString, strings)
};
Ou assim, usando a macro QS_CLASS :
class SerializableClass {
Q_GADGET
QS_CLASS
QS_FIELD(int, digit)
QS_COLLECTION(QList, QString, strings)
};
Exemplo de serialização JSON
:
, :
JSON:
— , XML , toJson toXml.
example.
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.
Limitações
Campos únicos
Os tipos primitivos ou definidos pelo usuário devem fornecer um construtor padrão.
Coleções
A classe de coleção deve ser modelada e fornecer métodos clear, at, size e append. Você pode usar suas próprias coleções, sujeito às condições. Coleções Qt que satisfazem estas condições: QVector, QStack, QList, QQueue.
Versões Qt
Versão mínima do Qt versão 5.5.0 Versão
mínima testada Qt 5.9.0 Versão
máxima testada Qt 5.15.0
NOTA: você pode participar de testes e testar o QSerializer em versões anteriores do Qt
Resultado
Ao retrabalhar o QSerializer, absolutamente não me propus a reduzir significativamente. No entanto, seu tamanho caiu de 9 para 1, o que também reduziu sua complexidade. Agora QSerializer não é mais uma biblioteca em nossa forma usual, agora é apenas um arquivo de cabeçalho que você só precisa incluir no projeto e obter todas as funcionalidades para serialização / desserialização confortável. O desenvolvimento começou em março, uma arquitetura complicada foi inventada e o projeto foi dominado por dependências, muletas, reescrito a partir de 0 várias vezes. E tudo para finalmente se transformar em um pequeno arquivo.
Perguntando-me: "Valeu a pena o esforço despendido?", Respondo: "Valeu sim." Já experimentei em meus projetos de combate e o resultado me agradou.
Links
GitHub: link
Versão mais recente: v1.1
Artigo anterior: QSerializer: solução para serialização JSON / XML simples
Lista de futuros
- Redução substancial de custo (pode ser feito ainda mais barato)
- Compacidade
- Trabalhando com tipos significativos
- Descrição básica de dados serializáveis
- Suporte para qualquer coleção modelada que forneça métodos claros, em, de tamanho e de acréscimo. Até os seus próprios
- Coleções totalmente mutáveis na desserialização
- Suporte para todos os tipos primitivos populares
- Suporte para qualquer tipo personalizado descrito usando QSerializer
- Não há necessidade de registrar tipos personalizados