Приходилось часто работать с классом QDataStream. В результате накопил некоторый опыт, как правильно его использовать.
Неоднократно замечал, что в начале работы с классом бытует мнение, что раз класс имеет в своем названии stream, то просто обязан хранить данные. Строчка в помощи QDatastream::QDatastream(QByteArray * a, QIODevice::OpenMode mode)
Constructs a data stream that operates on a byte array, a. The mode describes how the device is to be used. поначалу мало у кого вызывает опасения. Но если взглянуть под капот, то можно увидеть что, никакие данные непосредственно в QDataStream не записываются. В конструкторе инициализируется класс QBuffer, который является в данном случае оберткой для переданного QByteArray’a. Замысел работы такой: данные хранятся исключительно в QByteArray, а все операции по (де)сериализации проводит класс QDataStream. При чтении данных из потока изменяется лишь указатель на текущий байт, при этом сами данные не теряются. При записи данные для QIODevice::WriteOnly затираются новыми значениями, для QIODevice::Append добавляются в конец. Из этого следует вывод о контроле времени жизни QByteArray.
Запись представлена стандартным оператором << определенный для всех основных типов. Однако, зачастую удобней записывать данные сразу в структуру данных. Следующий пример показывает как перегрузить оператор << для наших целей
Здесь все довольно просто: запись «сложной» структуры разбивается на запись более простых типов. Так же обратите внимание на QDataStream writeRawData(const char* s, int len). Можно было бы в цикле записать значения массивов, но зачем это делать, если есть более элегантный способ.Точно так же перегрузим оператор для чтения:
Здесь все тоже самое, но стоит обратить внимание на функцию QDataStream::setFloatingPointPrecision (FloatingPointPrecision precision). Дело в том, что начиная с версии Qt 4.6, требуется явно указывать точность типа с плавающей точкой. Как можно догадаться SinglePrecision нужен для типов с одинарной точностью, а DoublePrecision для типов с двойной. Для решения этой неприятной ситуации есть два пути: первый это перегрузить << и >> примерно так:
Или же в своем коде указывать перед чтением float и double как их считывать. По умолчанию используется DoublePrecision.
Теперь обратим внимание на QDataStream::skipRawData(int len). Данная функция просто пропускает указанное количество байт, что бывает крайне полезно при выравнивании структур.
Отдельно стоит сказать про порядок записи старшего бита. Метод QDataStream::setByteOrder( ByteOrder bo) устанавливает порядок следования. При ByteOrder::BigEndian запись идет старшим байтом вперед, и именно такой порядок применяется по умолчанию. При ByteOrder::LittleEndian запись идет младшим битом вперед.
Кроме стандартных типов С++ QDataStream позволяет записывать также некоторые классы Qt такие как QList и QVariant. Однако тут скрыта некоторые проблемы связанные с версией Qt. Однако, разработчики позаботились о сериализации классов различных версий. Ответственен за это метод QDataStream::setVersion ( int v ), где v указание версии Qt. Тем не менее, стоит помнить, что при возможности протащить классы через различные версии, будут доступны только те свойства класса, которые есть в актуальной версии библиотеки. Получить версию, с которой работает поток, можно при помощи QDataStream::version (). рассмотрим небольшой пример по записи в файл контейнера QHash.
Перед тем как перейти к примеру хочется уделить особое внимание такой вещи как flush(). При работе с некоторыми устройствами данные могут быть буферизированы, что означает что запись в них может произойти не в момент вызова функции записи. Тема буферизации заслуживает отдельной статьи, поэтому остановимся пока на выводе, что надо принудительно опустошать буфер для записи.
В примере мы создали класс simpleClass и перегрузили для него операторы QDataStream. Замете, что мы сделали операторы дружественными к классу, что бы иметь возможность напрямую обращаться к приватным секциям. Можно было бы городить огород перегружая операторы для класса или писать функции доступа к приватным свойствам, но лично мне решение с friend кажется более элегантным. Дальше все довольно просто: открываем файл на чтение, создаем поток с привязкой к файлу, заполняем QHash и записываем, не забывая при этом указать версию Qt. Порядок для чтение практически ничем не отличается.
Несмотря на высокий уровень проектирования класса в нем тоже есть свои подводные камни, о которых надо помнить. Следует всегда помнить, что класс является функциональной оберткой над структурой данных и пользоваться им имеет смысл, когда требуется непосредственно производить операции чтения/записи, а для передачи данных использовать другие средства. Кроме того, хорошим тоном будет явно указывать параметры работы с потоком. Пара строчек сэкономит в будущем много нервов тем, кому придется работать с кодом после вас.
Введение
Неоднократно замечал, что в начале работы с классом бытует мнение, что раз класс имеет в своем названии stream, то просто обязан хранить данные. Строчка в помощи QDatastream::QDatastream(QByteArray * a, QIODevice::OpenMode mode)
Constructs a data stream that operates on a byte array, a. The mode describes how the device is to be used. поначалу мало у кого вызывает опасения. Но если взглянуть под капот, то можно увидеть что, никакие данные непосредственно в QDataStream не записываются. В конструкторе инициализируется класс QBuffer, который является в данном случае оберткой для переданного QByteArray’a. Замысел работы такой: данные хранятся исключительно в QByteArray, а все операции по (де)сериализации проводит класс QDataStream. При чтении данных из потока изменяется лишь указатель на текущий байт, при этом сами данные не теряются. При записи данные для QIODevice::WriteOnly затираются новыми значениями, для QIODevice::Append добавляются в конец. Из этого следует вывод о контроле времени жизни QByteArray.
Чтение-Запись
Запись представлена стандартным оператором << определенный для всех основных типов. Однако, зачастую удобней записывать данные сразу в структуру данных. Следующий пример показывает как перегрузить оператор << для наших целей
sctruct anyStruct
{
short sVal;
float fVal;
double dVal;
short Empty;
char array[8];
}
QDataStream operator <<(QDataStream &out, const anyStruct &any)
{
out << any.sVal;
out << any.fVal;
out << any.dVal;
out << any.Empty;
out.writeRawData(any.array,sizeof(any.array));
return out;
}
Здесь все довольно просто: запись «сложной» структуры разбивается на запись более простых типов. Так же обратите внимание на QDataStream writeRawData(const char* s, int len). Можно было бы в цикле записать значения массивов, но зачем это делать, если есть более элегантный способ.Точно так же перегрузим оператор для чтения:
QDataStream operator >>(QDataStream &out, anyStruct &any)
{
out >> any.sVal;
out.setFloatingPointPrecision(QDataStream::FloatingPointPrecision);
out >> any.fVal;
out.setFloatingPointPrecision(QDataStream::DoublePrecision);
out >> any.dVal;
out.skipRawData(sizeof(any.Empty));
out.ReadRawData(any.array,sizeof(any.array));
return out;
}
Здесь все тоже самое, но стоит обратить внимание на функцию QDataStream::setFloatingPointPrecision (FloatingPointPrecision precision). Дело в том, что начиная с версии Qt 4.6, требуется явно указывать точность типа с плавающей точкой. Как можно догадаться SinglePrecision нужен для типов с одинарной точностью, а DoublePrecision для типов с двойной. Для решения этой неприятной ситуации есть два пути: первый это перегрузить << и >> примерно так:
QDataStream operator >>(QDataStream &out, float &val)
{
if(out. FloatingPointPrecision() != QDataStream:: SinglePrecision)
{
out.setFloatingPointPrecision(QDataStream::FloatingPointPrecision);
out >> val;
out.setFloatingPointPrecision(QDataStream::DoublePrecision);
}
}
Или же в своем коде указывать перед чтением float и double как их считывать. По умолчанию используется DoublePrecision.
Теперь обратим внимание на QDataStream::skipRawData(int len). Данная функция просто пропускает указанное количество байт, что бывает крайне полезно при выравнивании структур.
Отдельно стоит сказать про порядок записи старшего бита. Метод QDataStream::setByteOrder( ByteOrder bo) устанавливает порядок следования. При ByteOrder::BigEndian запись идет старшим байтом вперед, и именно такой порядок применяется по умолчанию. При ByteOrder::LittleEndian запись идет младшим битом вперед.
(Де)Сериализация классов Qt
Кроме стандартных типов С++ QDataStream позволяет записывать также некоторые классы Qt такие как QList и QVariant. Однако тут скрыта некоторые проблемы связанные с версией Qt. Однако, разработчики позаботились о сериализации классов различных версий. Ответственен за это метод QDataStream::setVersion ( int v ), где v указание версии Qt. Тем не менее, стоит помнить, что при возможности протащить классы через различные версии, будут доступны только те свойства класса, которые есть в актуальной версии библиотеки. Получить версию, с которой работает поток, можно при помощи QDataStream::version (). рассмотрим небольшой пример по записи в файл контейнера QHash.
#include <QtCore/QCoreApplication>
#include <QDataStream>
#include <QByteArray>
#include <QFile>
#include <QString>
#include <QHash>
#include <QDebug>
class simpleClass
{
public:
quint32 a,b;
quint32 func(quint32 arg1, quint32 arg2);
quint32 getC();
friend QDataStream &operator <<(QDataStream &stream,const simpleClass &sC);
friend QDataStream &operator >>(QDataStream &stream, simpleClass &sC);
protected:
quint32 c;
};
inline quint32 simpleClass::func(quint32 arg1, quint32 arg2)
{
a = arg1;
b = arg2;
c = a+b;
return c;
}
inline quint32 simpleClass::getC()
{
return c;
}
inline QDataStream &operator <<(QDataStream &stream,const simpleClass &sC) // сериализуем;
{
stream << sC.a;
stream << sC.b;
stream << sC.c;
return stream;
}
inline QDataStream &operator >>(QDataStream &stream, simpleClass &sC) // десериализуем;
{
stream >> sC.a;
stream >> sC.b;
stream >> sC.c;
return stream;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QFile appFile(QString("filename.data"));
appFile.open(QFile::Append); // открываем файл для дозаписи;
QDataStream inFile(&appFile); // передаем потоку указатель на QIODevice;
QHash <quint32,simpleClass> hash; // инициализируем контейнер и чем нибудь заполним его;
for(quint32 i = 0; i < 64; i++)
{
if(!hash.contains(i)) // Проверяем есть ли ключ, если нет добовляем его;
{
simpleClass sC; // создали объект;
sC.func(i,i+10); // изменили состояние объекта;
hash.insert(i,sC); // допавили в хеш;
}
}
inFile.setVersion(QDataStream::Qt_4_8); // явно указываем версию Qt, для сериализации;
inFile << hash; // ура записано;
appFile.flush(); // записываем весь буффер в файл;
appFile.close();
// теперь прочитаем что мы там записали;
QFile readFile(QString("filename.data"));
readFile.open(QFile::ReadOnly);
QDataStream outFile(&readFile);
outFile.setVersion(QDataStream::Qt_4_8);
QHash<quint32,simpleClass> readHash; // создаем аналогичный контейнер;
outFile >> readHash; // пишем в него из потока;
foreach(quint64 key, readHash.keys()) // обходим каждый элемент: readHash.keys() возвращает лист из ключей;
{
simpleClass sC = readHash.value(key); // присваиваем объекту значения из потока;
qDebug() << "Sum was " << sC.getC(); // смотрим что было;
qDebug() << "Sum is "<< sC.func(key,2*key); // смотрим что стало;
}
readFile.close();
return a.exec();
}
Перед тем как перейти к примеру хочется уделить особое внимание такой вещи как flush(). При работе с некоторыми устройствами данные могут быть буферизированы, что означает что запись в них может произойти не в момент вызова функции записи. Тема буферизации заслуживает отдельной статьи, поэтому остановимся пока на выводе, что надо принудительно опустошать буфер для записи.
В примере мы создали класс simpleClass и перегрузили для него операторы QDataStream. Замете, что мы сделали операторы дружественными к классу, что бы иметь возможность напрямую обращаться к приватным секциям. Можно было бы городить огород перегружая операторы для класса или писать функции доступа к приватным свойствам, но лично мне решение с friend кажется более элегантным. Дальше все довольно просто: открываем файл на чтение, создаем поток с привязкой к файлу, заполняем QHash и записываем, не забывая при этом указать версию Qt. Порядок для чтение практически ничем не отличается.
Заключение
Несмотря на высокий уровень проектирования класса в нем тоже есть свои подводные камни, о которых надо помнить. Следует всегда помнить, что класс является функциональной оберткой над структурой данных и пользоваться им имеет смысл, когда требуется непосредственно производить операции чтения/записи, а для передачи данных использовать другие средства. Кроме того, хорошим тоном будет явно указывать параметры работы с потоком. Пара строчек сэкономит в будущем много нервов тем, кому придется работать с кодом после вас.