О таких не слышал.
А их значения?
О таких не слышал.
А их значения?
type
t1=class
public поле1:real;
public field2:byte;
end;
begin
var a := new t1;
a.поле1 := 5.3;
a.field2 := 7;
var t := a.GetType;
var fields := t.GetFields;
fields
.Select(fld->(fld.Name,fld.FieldType,fld.GetValue(a)))
.PrintLines;
end.
Давайте вы для начала сами поэксперементируете, а потому уже вопросы будете задавать))
Кстати, именно таким способом сейчас перебираются поля записи при сохранении и чтении типизированных файлов.
Собственно, @Admin, надеюсь в этот раз вы прочитаете в чём проблема не совместимости, хотя бы перед тем как читать следующее:
Чтоб привести работу типизированных файлов к стандартной, той что закладывал Вирт - надо сделать чтоб записи читались блоками. Костыли из считывания по 1 полю может поверхностно и работают примерно так же, но это полностью избавляет типизированные файлы от всех их преимуществ. Кроме того, даже если исправить строки - метод которым сохранялись записи у Вирта - сохраняет все поля записи, даже приватные. А ваш сохраняет только публичные. Это уже можно исправить только читая блоками.
Я сделал коротенькую демонстрацию на сколько более простой становится запись файлов, если записывать переменные 1 блоком, а не по 1 полю. Конечно, короткие строки надо переделать, потому что если их оставлять ссылочными - кина не будет. Но это не так уж сложно + кода на новые короткие строки надо меньше, чем то сколько съэкономится если переделать типизированные файлы на чтение блоками:
uses System.Runtime.InteropServices;
type
[StructLayout(LayoutKind.&Explicit)]
MyShortString5 = record//короткая строка, реализованная на value-типе
public const MaxLength: byte = 5;//это единственное что надо поменять, чтоб получилась короткая строка с другой длинной
private [FieldOffset(0)]_length: byte;
private [FieldOffset(MaxLength)]last: byte;
public property Length: byte read _length;
public class function operator implicit(s: string): MyShortString5;
begin
Result._length := Min(MaxLength, s.Length);
var ptr_id := integer(@Result._length);//когда (если) добавят арифметику указателей - можно будет слегка упростить
foreach var ch in s.Take(Result._length) do
begin
ptr_id += 1;
PByte(pointer(ptr_id))^ := OrdAnsi(ch);
end;
end;
public class function operator implicit(s: MyShortString5): string;
begin
var sb := new StringBuilder(s._length);
var ptr_id := integer(@s._length);//когда (если) добавят арифметику указателей - можно будет слегка упростить
loop s._length do
begin
ptr_id += 1;
sb += ChrAnsi(PByte(pointer(ptr_id))^);
end;
Result := sb.ToString;
end;
public function ToString: string; override := self;
//ToDo реализовать остальной функционал, который нужен чтоб этот тип работал во всём как string. То есть наследование от sequence и т.п.
end;
procedure TestMyShortString5;
begin
writeln(sizeof(MyShortString5));//6, потому что length(1) + само значение( MaxLength(5)*byte_per_char(1) )
var s: MyShortString5;
s := 'abc';
writeln(s);//abc
//writeln(string(s));//не abc а мусор, потому что #1041
writeln(s.ToString);//abc
end;
type
r1 = record
public x1: byte;
internal s: MyShortString5;//internal и private поля тоже может сохранить, в отличии от вашего file of T
private x2: byte;
public function ToString:string; override :=
$'({x1}, {s}, {x2})';//чтоб writeln выводило internal и private поле. Их всё равно сохраняет в файл, но без этого не выведет в консоль
end;
type
MyFileOf<T>=class
where T: record;
private class sz: integer;
private fi:System.IO.FileInfo;
private bw:System.IO.BinaryWriter;
private br:System.IO.BinaryReader;
private class constructor;
begin
sz := Marshal.SizeOf(typeof(T));
end;
//procedure Rewrite;
public procedure Rewrite(fname:string);
begin
fi := new System.IO.FileInfo(fname);
bw := new System.IO.BinaryWriter(fi.Open(System.IO.FileMode.Create));
end;
public procedure Reset;
begin
br := new System.IO.BinaryReader(fi.Open(System.IO.FileMode.Open));
end;
//procedure Reset(fname:string);
public procedure Write(o: T);
begin
var a := new byte[sz];
var ptr:^T := pointer(@a[0]);
ptr^ := o;
bw.Write(a);
end;
public procedure Write(params a:array of T) :=
foreach var o in a do
Write(o);
public function Read:T;
begin
var a := br.ReadBytes(sz);
var ptr:^T := pointer(@a[0]);
Result := ptr^;
end;
public procedure Close;
begin
if bw <> nil then bw.Close;
if br <> nil then br.Close;
end;
//ToDo реализовать остальной функционал, который нужен чтоб этот тип работал во всём как file of T. К примеру, сделать Rewrite и т.п. Глобальными процедурами PABCSystem
end;
procedure TestMyFileOfT;
begin
var f := new MyFileOf<r1>;
f.Rewrite('temp.bin');
var a:r1;
a.x1 := 255;
a.s := 'abc';
a.x2 := 254;
f.Write(a);
f.Close;
f.Reset;
var b := f.Read;
f.Close;
writeln(a);//(255,abc,254)
writeln(b);//(255,abc,254)
end;
begin
//TestMyShortString5;
TestMyFileOfT;
end.
Эта программа записывает переменную типа записи r1
в файл и успешно загружает её назад. Делается это всё 1 блоком, надеюсь мне не придётся доказывать что это не только проще выглядит, но ещё и работает быстрее. (ну и, конечно, работает именно так как задумывал Вирт, в отличии от вашего file of T
)
Запись содержит короткую строку, реализованую в виде value
-типа (но т.к. это короткая демонстрация - я реализовал не все возможности, только основные, чтоб не забивать пример).
Формат сохранения (в том числе и строки) абсолютно идентичен тому что придумывал Вирт, для всех значений максимальной длинны короткой строки от 0 до 255. (я проверил не каждое значение, но несколько, в том числе крайние)
Я готов доделать такие типизированные файлы и короткие строки, добавив все остальные возможности которых у них не хватает в этом примере, мне нужно лишь ваше разрешение и небольшая помощь, приделать то что получится в итоге к синтаксису.
Прежде всего давайте зафиксируем ошибку. Я написал ее в Issue. Посмотрим, насколько сложно ее исправить.
Во-вторых, мне очень радостно, что впервые в нашем проекте изъявил желание что-то реализовать сторонний разработчик.
К сожалению, цели ориентированы на возможности 70-х годов, что меня слегка удручает. В NET есть возможность бинарной сериализации - и ей прежде всего надо пользоваться для подобных целей.
Использовать для реализации неуправляемые указатели я считаю неверным. Наличие многочисленных фиксаций в .NET-коде существенно его замедляет.
Словом, давайте вначале исправим ошибку, а потом будем обсуждать всё остальное.
Если реализовать это не костылями, а через блочное сохранение - у этого способа сохранения есть своя ниша, из за скорости и малого объёма в файле.
Я так понимаю, костылями это было сделано только потому что никто не догадался сделать короткие строки через value
-тип, как это сделал я в предыдущем ответе.
Да, это только как пример. В идеале это надо делать оператором box
в генерируемом IL
коде. Но не писать же всю демонстрационную программу в IL
коде)))
Хотя, стоит ещё провести все необходимые тесты, перед тем как утверждать что этот способ хуже. В конце концов, это всё равно будет на несколько порядков быстрее чем то как вы сейчас реализовали это, просто потому что блоками читать всегда быстрее.
Если переделать всё на блочное сохранение - ошибка сама пропадёт. А если исправлять её как то по другому - то только ужасными костылями.
Кроме того, то что сейчас file of T
реализовано не блочным сохранением, а по 1 полю - это уже кривота со стороны совместимости. Если смотреть с этой стороны - её стоит в первую очередь исправить, потому что если переделать способ сохранения в целом - ошибки будут совсем другие (или их не будет, если макс. тестов сразу написать).
Не надо ничего переделывать. И переменные должны считываться по элементно. Блочное считывание невозможно.
кривоты нет. как оно там пишется, это уже детали реализации. недочет со string[255] исправляется малой кровью. просто записать в начале длину строки (как это делает bw.Write(string), потом сериализованный массив байтов строки + оставшиеся нули.
Вы ошибаетесь. Разработчики думали и над этим. Но строки в .NET ссылочные, поэтому и string[255] ссылочный и работает почти так же быстро как обычный string. в отличие от вашего неэффективного кода с указателями.
Я не знаю, как там оно “внутрё” (с) происходит, но “правильный” алгоритм чтения записей из типизированного файла выглядит таким:
Ну, чтение у нас буферизованное, остальное детали реализации.
“У мене внутре… гм… не… неонка”. (с)
Я примерно так и думал.
Ну как не возможно, если я сделал.
Короткие строки работает не почти как string, их заменяет при компиляции на string. И хоть заполнение короткой строки может быть медленнее - с типизированными файлами она всё равно быстрее работает. Как сказал @RAlex - короткие строки нужны в типизированных файлах, вне их - использование коротких строк всё равно не оправданно. А вмести с ними - будет в целом быстрее.
Эта буферизация слепая, и ради её улучшения в коде в PABCSystem ничего нету. Она не даёт такого преимущества, как блочное чтение.
Это такое старое решение для древних машин со слабыми ресурсами. Фактически - вроде array of char[1…n], только даже немного хуже )))
Кстати, можно ещё программно создавать тип у которого будет объявлено по 1 полю на символ, 256 полей в 1 типе (пока он программно создан и они все приватные) это не так уж страшно, а тогда указатели вообще не нужны (ну кроме как в file of T
, но там именно часть про указатели выполняется только 1 раз за 1 прочитанную запись, поэтому это не считается).
А вообще, я не с той стороны начал…
type
r1=record
procedure p1 := exit;
end;
begin
var a:r1;
a.p1;
end.
Этот код уже создаёт 2 указателя на a
(для вызова методов $Init$
и p1
). И это не какие то супер оптимизированные указатели - это те же самые, что получаются в результате @a
. Если верить вам - всё в .Net ужасно медленное. С чего вы вообще вдруг решили что в .Net указатели делают программу медленнее? Ну, конечно, это:
begin
var i:byte;
var ptr := @i;
ptr^ := 6;
end.
Будет работать медленнее чем это:
begin
var i:byte;
i := 6;
end.
Но если программист так делает - это уже проблема программиста)))) И это не значит что использование указателей в .Net не может быть оправданно.
Раз мне всё равно не спится, решил вот устроить тест скорости. SpeedTest.rar (2,9 МБ)
Там ещё в комплекте тесты, показывающие чтоб сохранение проходит правильно. Просто разкомментируйте TestIntegrity
и закомментируйте TestSpeed
в Main
.
У меня разница в 5-6 раз (всегда больше 5, иногда приближается к 6). И это не в пользу file of T
)))
В общем - да, тут использование указателей таки оправдано.
Я, кроме всего прочего нашёл, пока делал это, ещё 1 ошибку file of T
(которой, конечно, в моём способе нет изначально, в моём без костылей всё нормально работает ):
Константные поля тоже сохраняет, а при загрузке из файла пытается загрузить, что вызывает исключение, конечно.
У меня есть два вопроса, не относящихся к PascalABC.NET.
Первое: какой смысл в старых паскалях в обязательном заголовке Program? Тут уже кто - то говорил, что это что - то типа объявления потоков ввода - вывода, но почему имя нельзя дать автоматически? Например по имени файла исходного кода?
Второе: почему в старых (да и новых) паскалях в статических массивах нельзя использовать переменные для задания его длины? В C++ это как раз таки возможно.
Правильно написанный код с указателями не может работать медленнее не менее правильного кода в стиле .NET. Указатели позволяют обходить массу NET-овских огородов, например проверку индексатора. Естественно, при этом всё будет работать быстрее.
Смотря какие переменные, константы можно и в паскале использовать для этого. А в C++ вроде ещё есть отдельно статические массивы которые статичны, а есть ещё отдельно которые динамичны. Помню что там очень всё усложнено, а как именно - не помню.
Я писал об этом. Это было требование операционной системы машины, на которой Йенсен и Вирт реализовали свой первый компилятор. Поскольку в то время оперативной памяти было ничтожно мало, а быстродействие процессоров по нынешним временам было ничтожным, эта директива подсказывала компилятору, будет ли нужно подключаться к файловой системе, а также определяла некоторые файловые спецификации. Позднее Турбо Паскаль стал использовать имя из заголовка program для целей линковки модулей. А далее все это умерло и program фактически стал восприниматься комментарием.
Константы - это не переменные))) Смысл в том. что память под такие массивы распределяется на этапе компиляции (раннее связывание).
Вот и я о том, что в Паскале только константы. А в C++ можно ввести с клавиатуры размер массива и поместить его в стек.
@RAlex, всё равно не понятно. Почему имя не мог задать сам компилятор?