Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4505.html
 

Заметка о RTTI, TValue и real/double/extended

Опубликовано 24.01.2012 г. 19:07

Заканчивая свою работу над сериализацией записей в XML, и уже встраивая разработанные классы в программный продукт, столкнулся с неожиданным поведением, что выражалось некорректной загрузкой данных. Сначала было сложно понять в чем дело, ибо на тестовом примере все вроде как работало исправно.

Проблемы можно было избежать, не используя TValue в данном месте кода, но оно использовалось. Сначала при десериализации значение считывалось из XML в виде строки, затем преобразовывалось в TValue нужного типа. Выглядело это так:
function TRecordSerializer.StringToValue(tk: TTypeKind; value: string; valueTypeInfo : PTypeInfo = nil): TValue;
var iev : integer;
begin
    case tk of
        tkInteger,
        tkInt64    : result := StrToInt(value);
        tkFloat    : result := StrToFloat(value);

        tkEnumeration : begin
                          iev := GetEnumValue(valueTypeInfo , value);
                          result := TValue.FromOrdinal(valueTypeInfo, iev);
                        end
        else result := value;
    end;
end;
Т.е предполагается что конечные значения могут быть целочисленные, дробные, перечисления, либо строки. На вход поступал - тип значения который необходимо получить (tk: TTypeKind), его строковое представление (value) и TypeInfo в соответствии с типом (valueTypeInfo). И вроде как бы тут все работало исправно. Где после преобразования строки к TValue оно могло записываться как значение поля записи (f : TRttiField; value : TValue; Data : Pointer - указатель на поле):
 f.SetValue(data, value);
Либо как элемент статического массива (value : TValue; elPtr : pointer - адрес элемента массива):
value.ExtractRawData(elPtr); 
Либо как элемент динамического массива (arr - динамический массив, представленный тоже как TValue, evalue - полученное из строки значение элемента):
 arr.SetArrayElement(i, evalue);
В первом случае проблем не возникало. Третий не подход не использовался, поскольку в программе не было динамических массивов. А вот во втором случае проблема возникла. Достаточно долго просидел над разгадкой проблемы, отчасти от того, что была одна мысль: "в тестовом проекте все работало, проблема возникла при переносе в рабочий проект, значит проблема не в механизме сериализации а в проекте". Такая мысль оказалась ошибочна, и после осознания этого проблема решилась достаточно быстро. Элемент массива заполнялся с помощью value.ExtractRawData(buffer), т.е в указанный буфер копировались исходные данные TValue соответствующего размера. Причем если мы оперировали целыми числами, то проблем не возникало, а вот с real/double они были. При попытке извлечь данные в i-й элемент массива затрагивался и i+1-й элемент. Следовательно, размер хранимых данных для real/double больше чем должен быть, т.е больше 8 байт. Справка по TValue (В справочной системе Delphi 2010, как это обычно бывает, описания нет(: ) говорит, что исходный размер хранимых данных можно получить с помощью TValue.DataSize, и то, что для простых типов данных он равен sizeof:
Returns the number of bytes occupied by the stored value. DataSize returns the number of bytes occupied by the stored value. For simple types, DataSize is equal to the number of bytes that SizeOf returns.
Проверяем. Возьмем integer переменную, посмотрим ее размер sizeof (ожидаемо 4 байта); TValue переменную, присвоим ей значение первой. После проверим тип который она хранит, и размер исходных данных:
var i : integer;
    value : TValue;
...
    writeln('sizeof: ', sizeof(i));
    value := i;

    writeln('Value TypeKind: ', getEnumName(typeinfo(TTypeKind), integer(value.TypeInfo.Kind)));
    writeln('Value.DataSize : ', value.DataSize);
Вывод в консоль:
sizeof: 4
value TypeKind: tkInteger
value.DataSize : 4
А теперь то же самое, только вместо integer возьмем real/double:
var d : double;
    value : TValue;
...
    writeln('sizeof: ', sizeof(d));
    value := d;
и результат:
sizeof: 8
value TypeKind: tkFloat
value.DataSize : 10
Вот и разгадка, представление real/double в TValue соответствует типу Extended, что и подтверждается проверкой:
    writeln('TypeName:', value.TypeInfo.Name);
TypeName: Extended
Следовательно, в приведенном выше методе StringToValue строка value := StrToFloat(str) конвертируется не в real/double а Extended занимая 10 байт вместо положенных 8ми. Затем в ExtractRawData последние два байта элемента (при записи последнего элемента массива), изменяют чужую память, и например, если в структуре после float-массива шла строка, то будет удалено количество ссылок на нее (количество ссылок и длина строки хранятся как раз перед ее фактическим началом). И т.д. В действительности, чтобы корректно сформировать real/double значение с ипользованием TValue необходимо использовать классовый метод TValue.Make, который получает 2 входных, и один выходной параметр - само значнеие TValue. Входные параметры указывают адрес буфера, с которого берется значение, и тип данных, который там хранится. Поэтому вместо value := RealOrDoubleVar следует использовать следующий код:
    writeln('sizeof: ', sizeof(r));
    TValue.Make(@r, typeinfo(real), value);

    writeln('Type Name:', value.TypeInfo.Name);
    writeln('value TypeKind:', getEnumName(typeinfo(TTypeKind), integer(value.TypeInfo.Kind)));
    writeln('value.DataSize : ', value.DataSize);
sizeof: 8
value TypeKind: tkFloat
TypeName: Real
value.DataSize : 8
Метки:  rtti  |  TValue 

Комментарии

bes67
25.01.2012 в 14:11
Добрый день! С интересом прочитал статью, спасибо. Хотелось бы только немного защитить Delphi ;) Защитить от фразы: "столкнулся с неожиданным поведением".
Что касается того кода, из-за которого, собственно, случился информационный повод. Смотрим описание StrToFloat:

function StrToFloat ( FloatString : string ) : Extended;

Название функции зело обманчиво, наверное, StrToExtended было бы самое оно. Но... имеем то, что имеем.
Но это не главное в данном случае. Главное кроется за тем кодом, который Вы использовали для проверок: прямое присвоение.
Обратимся к первоисточнику (System.Rtti.pas). А там:
...
class operator Implicit(const Value: string): TValue;
class operator Implicit(Value: Integer): TValue;
class operator Implicit(Value: Extended): TValue;
class operator Implicit(Value: Int64): TValue;
class operator Implicit(Value: TObject): TValue;
class operator Implicit(Value: TClass): TValue;
class operator Implicit(Value: Boolean): TValue;
...
То есть, неявного присвоения для Double не предусмотрено, предусмотрено для Extended. Таким образом, для прямого присвоения сначала делается неявное преобразование Double в Extended, а этот Extended затем и присваивается TValue. Собственно, все происходит так, как и должно происходить. Поэтому фразу, на мой взгляд, следует переделать на: "столкнулся с ожидаемым поведением" :)
ter
25.01.2012 в 15:20
хех (: действительно, вы правы про StrToFloat, че то мне казалось что он double возвращает всегда (:
Спасибо за комментарий, век живи - век учись, как говорится (:

Сам видел, что в TValue нет свойства AsFloat но что то не придал этому значения (:

Но в итоге для хранения real/double в TValue все равно от неявной записи следует отказаться, иначе будет все равно extended, так что через TValue.Make.
bes67
25.01.2012 в 16:42
Абсолютно с Вами согласен, хочу только дополнить, что есть немного более удобная, на мой взгляд, функция, тоже, кстати, функция класса TValue:

class function From(const Value: T): TValue; static;

Конечно, внутри себя она использует тот же самый Make.
bes67
25.01.2012 в 16:18
Прошу прощения, движок ожидаемо "съел" все, что в угловых скобках... попробую так, может, получится:

class function From<T>(const Value: T): TValue; static;

Больше не буду засорять ленту
ter
25.01.2012 в 17:53
ну как бы да, сведется то к make все равно.
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно