Delphi Saving/Loading Dynamic Arrays Failed

2020-07-23 05:50发布

问题:

I think this will look like 'do my homework' kind of a question, but I'm still at the 'copy code, use it and try to understand it' phase, and this is the most active thing I know of for posting questions of this theme.

I have a record:

type
  Card = record
    Name: string;
    Up,Right,Left,Down,Mark: Single; 
    IDNumber: Integer;
    end;

And array of that record:

var
  ArrayCard: array of Card;

And I wanted to know how can a dynamic array of this kind be stored/loaded to/from a file.

Tried using this piece of code: http://www.pascalgamedevelopment.com/showthread.php?6319-save-load-a-dynamic-array like this:

Procedure TMainFrom.WriteMyData;

  Var
    FS : TFileStream;
    I,iSize : Integer;
    TmpPath: string;

  Begin
    TmpPath:= TPath.Combine(TPath.GetDocumentsPath, 'Cards.dat');
    FS := TFileStream.Create(TmpPath, fmOpenWrite);
    iSize:= Length(ArrayCard);
    FS.WriteBuffer(iSize,SizeOf(iSize));
  For I := 0 To iSize - 1 Do
    FS.WriteBuffer(ArrayCard[I],SizeOf(Card));
  FS.Free;
  End;

An it seems to work so far, but then I try to load it like this:

Procedure TMainFrom.ReadMyData;

  Var
    FS : TFileStream;
    I,iSize : Integer;
    TmpPath: string;
    TempCard : Card;

  Begin
    TmpPath:= TPath.Combine(TPath.GetDocumentsPath, 'Cards.dat');
    FS := TFileStream.Create(TmpPath, fmOpenRead);
    FS.ReadBuffer(iSize,SizeOf(iSize));
    SetLength(ArrayCard,iSize);
  For I := 0 To iSize - 1 Do
    Begin
    FS.ReadBuffer(TempCard,SizeOf(Card));
    ArrayCard[I] := TempCard; //It Breaks Here...The Watch List: TempCard Inaccessible value
    End;
  FS.Free;
  End;

And I get a Exception EAccessViolation in module...

Then I also tried something like this: delphi Save and Load dynamic array It loads the array with the correct amount of items, but they are all empty or blank:

procedure TMainFrom.SaveCardsToFile;
var
  Stream: TStream;
  L: Integer;
  TmpPath: string;
  ICount: Integer;
  strm: TMemoryStream;

begin
  TmpPath:= TPath.Combine(TPath.GetDocumentsPath, 'Cards.bin');

  Stream:= TFileStream.Create(TmpPath, fmCreate);
  L:= Length(ArrayCard);
  Stream.WriteBuffer(L, SizeOf(L));
  Stream.WriteBuffer(Pointer(ArrayCard)^, L * SizeOf(Card));
  Stream.Free;
end;

procedure TMainFrom.LoadCardsFromFile;
var
  Stream: TStream;
  L: LongWord;
  TmpPath: string;

begin
  TmpPath:= TPath.Combine(TPath.GetDocumentsPath, 'Cards.bin');

  Stream:= TFileStream.Create(TmpPath, fmOpenRead);
  Stream.ReadBuffer(L, SizeOf(L));
  SetLength(ArrayCard, L);
  Stream.ReadBuffer(Pointer(ArrayCard)^, L * SizeOf(Card));
  Stream.Free;
end;

回答1:

Do not use buffer operations with records which contains "normal" strings. Instead of shortstring, string is only pointer to string content. So, your code saves and loads only pointer to string content, not string. You can get Access Violation if loaded value points to unreachable memory. Change your code to separately save and load variables in record, like this:

type
  TmyRec = record
    str: string;
    i: integer;
    procedure SaveToStream(Stream: TStream);
    procedure LoadFromStream(Stream: TStream);
  end;

{ myRec }

procedure TmyRec.LoadFromStream(Stream: TStream);
var
  strLen: integer;
  strBuf: TBytes;
begin
  Stream.Read(strLen, SizeOf(Integer));
  SetLength(strBuf, strLen);
  Stream.Read(strBuf, strLen);

  str:=TEncoding.UTF8.GetString(strBuf);

  Stream.Read(i, SizeOf(Integer));
end;

procedure TmyRec.SaveToStream(Stream: TStream);
var
  strBuf: TBytes;
  strLen: integer;
begin
  // direct set encoding type helps to avoid problems with different platforms.
  // for example, Windows uses UCS2, Android and iOS - UTF8 encoding
  strBuf:=TEncoding.UTF8.GetBytes(str);
  strLen:=Length(strBuf);

  Stream.Write(strLen, SizeOf(Integer));
  Stream.Write(strBuf, strLen);
  Stream.Write(i, SizeOf(Integer));
end;

Update: do you read about generics? Instead of dynamic array, you can use TList and load/save records in it:

type
  TmyRecList = class(TList<TmyRec>)
  public
    procedure SaveToStream(Stream: TStream);
    procedure LoadFromStream(Stream: TStream);
  end;

{ TmyRecList }

procedure TmyRecList.LoadFromStream(Stream: TStream);
var
  Len: integer;
  i: Integer;
  rec: TmyRec;
begin
  Clear;

  Stream.Read(Len, SizeOf(integer));

  for i := 0 to Len-1 do
    begin
      Rec.LoadFromStream(Stream);
      Add(rec);
    end;
end;

procedure TmyRecList.SaveToStream(Stream: TStream);
var
  i: Integer;
begin
  Stream.Write(Count, SizeOf(Integer));
  for i := 0 to Count-1 do
    Items[i].SaveToStream(Stream);
end;

procedure THeaderFooterForm.FormCreate(Sender: TObject);
var
  Stream: TStream;
  rec: TmyRec;
  recList: TmyRecList;
begin
  Stream:=TMemoryStream.Create;
  try
    recList:=TmyRecList.Create;
    try
      rec.str:='sample text';
      rec.i:=123;
      recList.Add(rec);

      rec.str:='another text';
      rec.i:=234;
      recList.Add(rec);

      recList.SaveToStream(Stream);
      Stream.Seek(0, soBeginning);

      recList.LoadFromStream(Stream);
      ShowMessage('this is str value in second record: ' + recList[1].str);
    finally
      recList.Free;
    end;
  finally
    Stream.Free;
  end;


回答2:

With some help, I have managed to remake my code to work properly. Firstly I needed to make the string something like string[20] but that couldn't compile for android so I modified my record to use array of char like this:

type
  EventString= array [0..20] of Char;

  Card = record
    Name:  EventString; //string[20]
    Up,Right,Left,Down,Mark: Single;
    IDNumber: Integer;
    end; 

Then I modified my Saving/Loading procedures to use TFileStream instead of TStream and a for loop:

procedure TMainFrom.SaveCardsToFile;
var
  Stream: TFileStream;
  L: Integer;
  TmpPath: string;
  n: Integer;

begin
  TmpPath:= TPath.Combine(TPath.GetDocumentsPath, 'Cards.bin');
  Stream:= TFileStream.Create(TmpPath, fmCreate);
  L:= Length(ArrayCard)-1;
  for n:=0 to L do
  begin
    Stream.WriteBuffer(ArrayCard[n], SizeOf(Card)); //Saving
  end;
  Stream.Free;
end;

procedure TMainFrom.LoadCardsFromFile;
var
  Stream: TFileStream;
  L: LongWord;
  TmpPath: string;
  n: Integer;

begin
  TmpPath:= TPath.Combine(TPath.GetDocumentsPath, 'Cards.bin');
  Stream:= TFileStream.Create(TmpPath, fmOpenRead);
  SetLength(ArrayCard, Round(Stream.Size/SizeOf(Card)));
  L:= Length(ArrayCard)-1;
  for n:=0 to L do
  begin
    Stream.ReadBuffer(ArrayCard[n], SizeOf(Card)); //Loading
    ListCard.Items.Add(ArrayCard[n].Name); //Just adds card names to a listbox
  end;
  Stream.Free;

end;

And now it works just fine.