Ошибки PascalABC.NET

О таких не слышал.

А их значения?

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. (я проверил не каждое значение, но несколько, в том числе крайние)


Я готов доделать такие типизированные файлы и короткие строки, добавив все остальные возможности которых у них не хватает в этом примере, мне нужно лишь ваше разрешение и небольшая помощь, приделать то что получится в итоге к синтаксису.

2 лайка

Прежде всего давайте зафиксируем ошибку. Я написал ее в Issue. Посмотрим, насколько сложно ее исправить.

Во-вторых, мне очень радостно, что впервые в нашем проекте изъявил желание что-то реализовать сторонний разработчик.

К сожалению, цели ориентированы на возможности 70-х годов, что меня слегка удручает. В NET есть возможность бинарной сериализации - и ей прежде всего надо пользоваться для подобных целей.

Использовать для реализации неуправляемые указатели я считаю неверным. Наличие многочисленных фиксаций в .NET-коде существенно его замедляет.

Словом, давайте вначале исправим ошибку, а потом будем обсуждать всё остальное.

Если реализовать это не костылями, а через блочное сохранение - у этого способа сохранения есть своя ниша, из за скорости и малого объёма в файле.
Я так понимаю, костылями это было сделано только потому что никто не догадался сделать короткие строки через value-тип, как это сделал я в предыдущем ответе.

Да, это только как пример. В идеале это надо делать оператором box в генерируемом IL коде. Но не писать же всю демонстрационную программу в IL коде)))

Хотя, стоит ещё провести все необходимые тесты, перед тем как утверждать что этот способ хуже. В конце концов, это всё равно будет на несколько порядков быстрее чем то как вы сейчас реализовали это, просто потому что блоками читать всегда быстрее.

Если переделать всё на блочное сохранение - ошибка сама пропадёт. А если исправлять её как то по другому - то только ужасными костылями.
Кроме того, то что сейчас file of T реализовано не блочным сохранением, а по 1 полю - это уже кривота со стороны совместимости. Если смотреть с этой стороны - её стоит в первую очередь исправить, потому что если переделать способ сохранения в целом - ошибки будут совсем другие (или их не будет, если макс. тестов сразу написать).

Не надо ничего переделывать. И переменные должны считываться по элементно. Блочное считывание невозможно.

кривоты нет. как оно там пишется, это уже детали реализации. недочет со string[255] исправляется малой кровью. просто записать в начале длину строки (как это делает bw.Write(string), потом сериализованный массив байтов строки + оставшиеся нули.

Вы ошибаетесь. Разработчики думали и над этим. Но строки в .NET ссылочные, поэтому и string[255] ссылочный и работает почти так же быстро как обычный string. в отличие от вашего неэффективного кода с указателями.

Я не знаю, как там оно “внутрё” (с) происходит, но “правильный” алгоритм чтения записей из типизированного файла выглядит таким:

  • определяем длину записи LR, для чего находим сумму длин всех полей связанного с файловой переменной типа T (var f:file of T) и добавляем к ней число символьных полей;
  • в случае последовательного чтения читаем из файла LR байт в некое буферное поле (возможно, внутреннее). Далее из этого поля копируем значения в переменные, указанные в списке Read. Это всего одна операция ввода-вывода с внешним устройством и она идет на порядки быстрее, чем чтение с устройства по отдельным полям. Фактически, мы читаем в кэш;
  • в случае выполнения Seek(f,n) определяем смещение от начала файла в виде Offset:=LR*n и либо читаем “в никуда” (возможно, максимально допустимыми блоками) число байт, равное Offset. А дальше -буферизованное чтение. Ну а если система ввода-вывода позволяет прямой доступ по номеру записи - ну, тем проще, подводим сразу нужное место.

Ну, чтение у нас буферизованное, остальное детали реализации.

“У мене внутре… гм… не… неонка”. (с)

Я примерно так и думал.

Ну как не возможно, если я сделал.

Короткие строки работает не почти как string, их заменяет при компиляции на string. И хоть заполнение короткой строки может быть медленнее - с типизированными файлами она всё равно быстрее работает. Как сказал @RAlex - короткие строки нужны в типизированных файлах, вне их - использование коротких строк всё равно не оправданно. А вмести с ними - будет в целом быстрее.

Эта буферизация слепая, и ради её улучшения в коде в PABCSystem ничего нету. Она не даёт такого преимущества, как блочное чтение.

Это такое старое решение для древних машин со слабыми ресурсами. Фактически - вроде array of char[1…n], только даже немного хуже )))

1 лайк

Кстати, можно ещё программно создавать тип у которого будет объявлено по 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 (которой, конечно, в моём способе нет изначально, в моём без костылей всё нормально работает :wink:):
Константные поля тоже сохраняет, а при загрузке из файла пытается загрузить, что вызывает исключение, конечно.

У меня есть два вопроса, не относящихся к PascalABC.NET.

Первое: какой смысл в старых паскалях в обязательном заголовке Program? Тут уже кто - то говорил, что это что - то типа объявления потоков ввода - вывода, но почему имя нельзя дать автоматически? Например по имени файла исходного кода?

Второе: почему в старых (да и новых) паскалях в статических массивах нельзя использовать переменные для задания его длины? В C++ это как раз таки возможно.

Правильно написанный код с указателями не может работать медленнее не менее правильного кода в стиле .NET. Указатели позволяют обходить массу NET-овских огородов, например проверку индексатора. Естественно, при этом всё будет работать быстрее.

Смотря какие переменные, константы можно и в паскале использовать для этого. А в C++ вроде ещё есть отдельно статические массивы которые статичны, а есть ещё отдельно которые динамичны. Помню что там очень всё усложнено, а как именно - не помню.

Я писал об этом. Это было требование операционной системы машины, на которой Йенсен и Вирт реализовали свой первый компилятор. Поскольку в то время оперативной памяти было ничтожно мало, а быстродействие процессоров по нынешним временам было ничтожным, эта директива подсказывала компилятору, будет ли нужно подключаться к файловой системе, а также определяла некоторые файловые спецификации. Позднее Турбо Паскаль стал использовать имя из заголовка program для целей линковки модулей. А далее все это умерло и program фактически стал восприниматься комментарием.

Константы - это не переменные))) Смысл в том. что память под такие массивы распределяется на этапе компиляции (раннее связывание).

Вот и я о том, что в Паскале только константы. А в C++ можно ввести с клавиатуры размер массива и поместить его в стек.

@RAlex, всё равно не понятно. Почему имя не мог задать сам компилятор?