Приходилось часто работать с классом 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. Порядок для чтение практически ничем не отличается.
Заключение
Несмотря на высокий уровень проектирования класса в нем тоже есть свои подводные камни, о которых надо помнить. Следует всегда помнить, что класс является функциональной оберткой над структурой данных и пользоваться им имеет смысл, когда требуется непосредственно производить операции чтения/записи, а для передачи данных использовать другие средства. Кроме того, хорошим тоном будет явно указывать параметры работы с потоком. Пара строчек сэкономит в будущем много нервов тем, кому придется работать с кодом после вас.
