<=
Сохранение ссылочных и особых типов в блочный файл.
В чём проблема особых типов
uses BlockFileOfT;
type
r1=record
ch: char;
constructor(ch: char) :=
self.ch := ch;
end;
begin
var f := new BlockFileOf<r1>('temp.bin');
f.Rewrite;
f.Write(new r1('a'));
f.Write(new r1('b'),new r1('c'));//Ошибка: Объект содержит данные, которые не являются примитивами или требуют преобразования.
f.Close;
end.
Тут второе Write вызовет ошибку описаную выше.
Это потому - что во второй Write передаётся array of r1.
Когда для копирования памяти из этого массива в массив байт запрашивается блокировка памяти -
.Net видит что r1 содержит поле типа - char. Я так и не нашёл однозначного определения что с ним не так.
Но, вроде, проблема в том - что в разных системах и версиях char может по разному хранится в памяти.
И эта ошибка - защита от дурака, чтоб не дать данные из 1 системы скопировать в другую.
char - не единственный такой "особый тип"
если получаете такую же ошибку - записывайте и считывайте по 1 элементу
или воспользуйтесь следующим способом обхода:
uses BlockFileOfT;
uses System.Runtime.InteropServices;
type
[StructLayout(LayoutKind.&Explicit)]
CWConv = record
[FieldOffset(0)] ch:char;//У обоих этих полей одинаковая позиция в памяти (0 байт от начала записси) и объём (2 байт)
[FieldOffset(0)] id:word;//Поэтому можно записать в 1 поле, а считать из другого, не теряя информацию
end;
r1=record
ch: word;
constructor(ch: char) :=
begin
var a: CWConv;
a.ch := ch;
self.ch := a.id;
end;
function GetCh:char;
begin
var a: CWConv;
a.id := self.ch;
Result := a.ch;
end;
end;
begin
var f := new BlockFileOf<r1>('temp.bin');
f.Rewrite;
f.Write(new r1('a'));
f.Write(new r1('b'),new r1('c'));
f.Close;
end.
Тип word занимает в памяти 2 байта, так же как char, поэтому char можно заменить на word.
Узнать объём типа можно вызвав sizeof(word), sizeof(char) и т.д.
Конечно, в данном случае создавать CWConv не обязательно, потому что word(ch) и char(id) и без того работает.
Но с другими особыми типами это может не сработать.
В чём проблема ссылочных типов
type
r1=record
b:byte;
i:integer;
s:string;
end;
begin
var a: r1;
a.b := 5;
a.i := 12345;
a.s := 'abcdef'*10000;//Это 60 000 символов, ~120KB
end.
Проблема заключается в том, как хранится элементы этой записи в оперативной памяти.
"a.b" и "a.i" будут находится прямо друг за другом.
Но "a.s" будет где то в случайной области памяти.
При этом, после "a.i", вместо "a.s" будет номер ячейки памяти (по сути - ссылка, отсюда название), в которую сохранило "a.s".
Блочное сохранение обычно работает потому, что все элементы записи хранит друг за другом, 1 блоком в памяти.
Этот блок можно целиком скопировать в файл, что является главным принципом "BlockFileOf<T>".
Но раз "a.s" находится не в теле записи - вместо него в файл сохранит только номер ячейки где "a.s" хранится.
Если, потом, попытаться загрузить эту запись назад - в "a.s" запишет номер ячейки где раньше была наша строка.
Но строки по тому адресу уже может и не быть. Если так - попытка получить её значение выведет мусор или даст ошибку чтения защищённой памяти.
Кроме всего прочего, сборщик мусора от .Net может перемещать ссылочные типы в памяти, для экономии места.
Если запись ещё находится в оперативной памяти в момент сборки мусора - адрес строки не забудет поменять.
Но если запись сохранили в файл, потом произошла сборка мусора, а затем мы загрузили запись назад - адрес строки будет уже устаревший.
В общем, сохранение записей с полями ссылочных типов - ни к чему хорошему не приведёт.
Но, все записи имеют неизменяемую длину, в то время как, к примеру, у массивов и строк - практически нету ограничения.
Это 1 из причин, почему ссылочные типы удобнее хранить отдельно, по ссылке.
И неизменяемая длина - не только ограничение записей, это так же ограничение алгоритмов "file of T" и "BlockFileOf<T>",
потому что для нормальной работы "Seek" - надо чтоб все элементы имели одинаковую длину.
По этому, в примерах сохранения ссылочных типов, описаных далее - всегда стоит определённое ограничение на рамер,
которое можно менять только перед компиляцией.
Массивы (статичные)
uses BlockFileOfT;
uses System.Runtime.InteropServices;
type
[StructLayout(LayoutKind.&Explicit)]
CharArr15 = record
private const MaxLength = 15;//Ограничение кол-ва элементов
private const TSize = 2;//То что возвращает sizeof(T) где T - тип элементов массива (в нашем случае - char)
private const Size = MaxLength * TSize;
private [FieldOffset(Size-1)] last: byte;
public class function operator explicit(a: array of char): CharArr15;
begin
if a.Length < MaxLength then
a := a + new char[MaxLength - a.Length];//Если не добавить тут лишние нули - копирование содержимого попытается прочитать после конца массива
var hnd := GCHandle.Alloc(a, GCHandleType.Pinned);//Надо заблокировать память, иначе сборщик мусора может очень невовремя переместить массив
try
var ptr: ^CharArr15 := pointer(hnd.AddrOfPinnedObject);
Result := ptr^;//Тут содержимое массива читает как CharArr15. Обычно так делать не стоит, но тут это лучший вариант, ибо единственный
finally
hnd.Free;//Если возникнет какая то ошибка - надо всё равно освободить дескриптор, иначе будет утечка памяти
end;
end;
public class function operator explicit(a: CharArr15): array of char;
begin
Result := new char[MaxLength];
var hnd := GCHandle.Alloc(Result, GCHandleType.Pinned);
try
var ptr: ^CharArr15 := pointer(hnd.AddrOfPinnedObject);
ptr^ := a;//А тут назад, содержимое "a" копируется в результат
finally
hnd.Free;
end;
end;
public function ToRefArr: array of char;
type
CharArr = array of char;
begin
Result := CharArr(self);
end;
end;
r1 = record
public a: CharArr15;
public i: integer;
public constructor(a: array of char; i: integer);
begin
self.a := CharArr15(a);
self.i := i;
end;
public constructor(s: string; i: integer) :=
Create(s.ToCharArray, i);
public function ToString: string; override :=
//Replace(#0,#32) заменяет пустые символы на пробелы
//IDE паскаля немного ломается, если попытаться вывести char(0)
$'r1("{(new string(a.ToRefArr)).Replace(#0,#32)}", {i})';
end;
begin
var f := new BlockFileOf<r1>('temp.bin');
var a1 := new r1(new char[]('a', 'b', 'd', 'q'), 6);
var b1 := new r1('Hello, world!', 278);
{
f.Reset;
{}
f.Rewrite;
f.Write(a1, b1);
{}
f.Pos := 0;
var a2 := f.Read;
var b2 := f.Read;
f.Close;
writeln(a1);
writeln(b1);
writeln(a1.a.ToRefArr.Length);
writeln(b1.a.ToRefArr.Length);
writeln(a2);
writeln(b2);
writeln(a2.a.ToRefArr.Length);
writeln(b2.a.ToRefArr.Length);
end.
Что делают "[StructLayout(LayoutKind.&Explicit)]" и "[FieldOffset(Size-1)]"?
"FieldOffset" указывает где в записи будет размещено поле "last: byte"
Таким образом, мы, как бы, резервируем "Size" байт для записи.
Они оказываются ничем не заняты, но в них можно сохранить информацию, что далее и происходит.
А "LayoutKind.&Explicit" разрешает использовать "FieldOffset" для "CharArr15".
"StructLayout", "LayoutKind.&Explicit" и "FieldOffset" находятся в пространстве имён
"System.Runtime.InteropServices", поэтому я подключил его в начале файла.
Но его можно не подключать, тогда придётся писать "System.Runtime.InteropServices.StructLayout" и т.п.
Хотя есть ещё третий вариант - добавить свои синонимы в начале файла:
type
StructLayout = System.Runtime.InteropServices.StructLayoutAttribute;
LayoutKind = System.Runtime.InteropServices.LayoutKind;
FieldOffset = System.Runtime.InteropServices.FieldOffsetAttribute;
Как видите, с тем что уже есть - можно хранить и строки, как массивы символов.
Но, хотя мы сохраняли массив из 4 элемента и строку на 13 символов - при чтении назад им дало длину 15.
Это вполне реально исправить, надо лишь сохранять - сколько символов было использовано.
Но, конечно, это будут дополнительные затраты памяти и производительности:
uses BlockFileOfT;
type
StructLayout = System.Runtime.InteropServices.StructLayoutAttribute;
LayoutKind = System.Runtime.InteropServices.LayoutKind;
FieldOffset = System.Runtime.InteropServices.FieldOffsetAttribute;
GCHandle = System.Runtime.InteropServices.GCHandle;
GCHandleType = System.Runtime.InteropServices.GCHandleType;
[StructLayout(LayoutKind.&Explicit)]
CharArr15Body = record
private const MaxLength = 15;
private const TSize = 2;
private const Size = MaxLength * TSize;
[FieldOffset(Size-1)] last: byte;
end;
[StructLayout(LayoutKind.&Explicit)]
CharArr15 = record
//shortint занимает 1 байт и принимает значения от -128 до 127
//У нас максимум 15 элементов, поэтому этого вполне хватит.
public [FieldOffset(0)] length: shortint;
//Тут FieldOffset=1, потому что length занимает 1 байт в памяти
//Если в вашей реализации "length" будет занимать бОльший объём - не забудьте поменять
public [FieldOffset(1)] body: CharArr15Body;
public class function operator explicit(a: array of char): CharArr15;
begin
Result.length := a.Length;//Если передать массив с длиной больше чем помещается в shortint - произвойдёт переполнение.
//Если такая возможность существует - надо поставить тут проверку, чтоб длине не присваивало ничего > 127
if a.Length < CharArr15Body.MaxLength then
a := a + new char[CharArr15Body.MaxLength - a.Length];
var hnd := GCHandle.Alloc(a, GCHandleType.Pinned);
try
var ptr: ^CharArr15Body := pointer(hnd.AddrOfPinnedObject);
Result.body := ptr^;
finally
hnd.Free;
end;
end;
public class function operator explicit(a: CharArr15): array of char;
begin
//Если кол-во сохранённых байт < MaxLength - a.body будет занимать бОльший объём в памяти, чем поместится в Result
//Поэтому надо сначала прочитать всё, включая пустые символы на конце, во временную переменную
//А затем скопировать нужное кол-во символов в Result
var temp := new char[CharArr15Body.MaxLength];
var hnd := GCHandle.Alloc(temp, GCHandleType.Pinned);
try
var ptr: ^CharArr15Body := pointer(hnd.AddrOfPinnedObject);
ptr^ := a.body;
finally
hnd.Free;
end;
Result := new char[a.length];
//Если в "explicit(a:array of char):CharArr15" вы запретили length>MaxLength,
//То "Min(a.length, CharArr15Body.MaxLength)" можно заменить на "a.length"
System.Array.Copy(temp, Result, Min(a.length, CharArr15Body.MaxLength));
end;
public function ToRefArr: array of char;
type
CharArr = array of char;
begin
Result := CharArr(self);
end;
end;
r1 = record
public a: CharArr15;
public i: integer;
public constructor(a: array of char; i: integer);
begin
self.a := CharArr15(a);
self.i := i;
end;
public constructor(s: string; i: integer) :=
Create(s.ToCharArray, i);
public function ToString: string; override :=
$'r1("{(new string(a.ToRefArr)).Replace(#0,#32)}", {i})';
end;
begin
var f := new BlockFileOf<r1>('temp.bin');
var a1 := new r1(new char[]('a', 'b', 'd', 'q'), 6);
var b1 := new r1('Hello, world!', 278);
{
f.Reset;
{}
f.Rewrite;
f.Write(a1, b1);
{}
f.Pos := 0;
var a2 := f.Read;
var b2 := f.Read;
f.Close;
writeln(a1);
writeln(b1);
writeln(a1.a.ToRefArr.Length);
writeln(b1.a.ToRefArr.Length);
writeln(a2);
writeln(b2);
writeln(a2.a.ToRefArr.Length);
writeln(b2.a.ToRefArr.Length);
end.
В этот раз, уже, нулевые символы в конеце строки обрезало при чтении,
потому что знало после какого символа надо обрезать.
Использование сериализаторов
uses BlockFileOfT;
uses System.Runtime.InteropServices;
type
[StructLayout(LayoutKind.&Explicit)]
Str15 = record
private const MaxLength = 15;
//Я провёл много тестов чтоб убедится:
//-BinaryFormatter всегда делает для string заголовок в 25 байт
//-В разных случаях тело строки может сохранять как MaxLength*1, MaxLength*2 и MaxLength*3 байт.
//-Предыдущее зависит от того, на сколько редкие символы были использованы. Для более редких символов надо больше памяти
//Поэтому, на всякий случай, я сделал Size с расчётом на то, что все MaxLength символов могут быть редкими.
private const Size = 25 + MaxLength * 3;
private [FieldOffset(Size-1)] last: byte;
private class f := new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter;
public class function operator explicit(s: string): Str15;
begin
if s.Length > MaxLength then s := s.Substring(0, MaxLength);
var str := new System.IO.MemoryStream(Size);
str.SetLength(Size);
f.Serialize(str, s);
var a := str.ToArray;
var hnd := GCHandle.Alloc(a, GCHandleType.Pinned);
try
var ptr: ^Str15 := pointer(hnd.AddrOfPinnedObject);
Result := ptr^;
finally
hnd.Free;
end;
end;
public class function operator explicit(s: Str15): string;
begin
var a := new byte[Size];
var hnd := GCHandle.Alloc(a, GCHandleType.Pinned);
try
var ptr: ^Str15 := pointer(hnd.AddrOfPinnedObject);
ptr^ := s;
finally
hnd.Free;
end;
var str := new System.IO.MemoryStream(a);
Result := string(f.Deserialize(str));
end;
end;
r1 = record
public s: Str15;
public i: integer;
public constructor(s: string; i: integer);
begin
self.s := Str15(s);
self.i := i;
end;
public function ToString: string; override :=
$'r1("{string(s)}", {i})';
end;
begin
var f := new BlockFileOf<r1>('temp.bin');
var a1 := new r1('abcd', 1234);
var b1 := new r1('abcd' * 4, 5678);
{
f.Reset;
{}
f.Rewrite;
f.Write(a1, b1);
{}
f.Pos := 0;
var a2 := f.Read;
var b2 := f.Read;
f.Close;
writeln(a1);
writeln(b1);
writeln(string(a1.s).Length);
writeln(string(b1.s).Length);
writeln(a2);
writeln(b2);
writeln(string(a2.s).Length);
writeln(string(b2.s).Length);
end.
Конечно, памяти будет нужен на много больший объём для такого сохранения.
А т.к. памяти будет больше - значит и записывать/читать придётся больше, то есть производительность тоже ниже.
Хотя в целом - такой способ должен быть проще в реализации.