delphitlisttfilestream

Writing tList<string> to tFileStream


I use Berlin in Windows 10. I try to save tList<string> to a file.

I know how to handle tStringlist, tStreamWriter and tStreamReader but I need to use tFileStream because the other type of data should be added.

In the following code the loop of Button2Click which reads the data raises an eOutOfMemory exception. When I allocate simple string value to _String it works well but if I put tList value to the same _String it seems that wrong data were written on the file. I can't understand the difference between _String := _List.List[i] and _String := 'qwert'.

How can I write tList<string> to tFileSteam?

procedure TForm1.Button1Click(Sender: TObject);
var
  _List: TList<string>;
  _FileStream: TFileStream;
  _String: string;
  i: Integer;
begin
  _List := TList<string>.Create;

  _List.Add('abcde');
  _List.Add('abcde12345');

  _FileStream := TFileStream.Create('test', fmCreate);

  for i := 0 to 1 do
  begin
    _String := _List.List[i]; // _String := 'qwert' works well

    _FileStream.Write(_string, 4);
  end;

  _FileStream.Free;
  _List.Free;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  _FileStream: TFileStream;
  _String: string;
  i: Integer;
begin
  _FileStream := TFileStream.Create('test', fmOpenRead);

  for i := 0 to 1 do
  begin
    _FileStream.Read(_String, 4);

    Memo1.Lines.Add(_String);
  end;

  _FileStream.Free;
end;

Solution

  • If you lookup in the docs what TFileStream.Write does, it tells you (inherited from THandleStream.Write):

    function Write(const Buffer; Count: Longint): Longint; override;
    function Write(const Buffer: TBytes; Offset, Count: Longint): Longint; override;
    

    Writes Count bytes from the Buffer to the current position in the resource.

    Now, Buffer is untyped and as such is expected to be the memory address of the data to be written. You are passing a string variable which is a reference to the actual string data, the address of the variable holds a pointer to string data. You are therefore writing a pointer to the file.

    To correct it pass the strings first character for the buffer, ....write(_string[1], ... If you have compiler directive {$ZEROBASEDSTRINGS ON} you would use index 0. Alternatively, typecast the string to PChar and dereference it: ....write(PChar(_String)^, ...

    Then look at the second parameter, Count. As the docs say, it indicates the number of bytes to be written, specifically not characters. In Delphi 2009 and later strings are UnicodeString, so each character is 2 bytes. You need to pass the strings size in bytes.

    This will write 4 characters (8 bytes) to the file stream:

    _FileStream.Write(_String[1], 4 * SizeOf(Char));
    

    or better

    _FileStream.Write(PChar(_String)^, 4 * SizeOf(Char));
    

    For reading you need to make corresponding changes, but most notable, you need to set the strings length before reading (length is counted in characters).

      SetLength(_String, 4);
      for i := 0 to 1 do
      begin
        _FileStream.Read(_String[1], 4 * SizeOf(Char));
    
        Memo1.Lines.Add(_String);
      end;
    

    To continue with this low-level approach you could generalize string writing and reading as follows: Add a variable to hold the length of a string

    var
      _String: string;
      _Length: integer;
    

    then writing

    begin
      ...
      for ....
      begin
        _String := _List.List[i];
        _Length := Length(_String);
        _FileStream.Write(_Length, SizeOf(Integer));
        _FileStream.Write(PChar(_List.List[i])^, _Length * SizeOf(Char));
      end;
    

    and reading

    begin
      ...
      for ....
      begin
        _FileStream.Read(_Length, SizeOf(Integer));
        SetLength(_String, _Length);
        _FileStream.Read(_String[1], _Length * SizeOf(Char));
        Memo1.Lines.Add(_String);
      end;
    

    IOW, you write the length first and then the string. On reading you read the length and then the string.