如何将经典 Delphi 字符串保存到磁盘(并读回)?

How to save classic Delphi string to disk (and read them back)?

我想在 Delphi 中完成一项非常非常基本的任务:将字符串保存到磁盘并将其加载回来。这看起来微不足道,但自从我升级到 IOUtils 之后,我在执行此操作时遇到了两次问题(之前还有一次……这就是为什么我做出 'brilliant' 决定升级到 IOUtils 的原因)。

我使用这样的东西:

procedure WriteToFile(CONST FileName: string; CONST uString: string; CONST WriteOp: WriteOperation);    
begin
   if WriteOp= (woOverwrite)
   then IOUtils.TFile.WriteAllText (FileName, uString)  //overwrite
   else IOUtils.TFile.AppendAllText(FileName, uString); //append
end;    

简单吧?会出什么问题?好吧,我最近遇到了 IOUtils 中的一个(另一个)错误。所以,TFile 是 . The bug is detailed .

任何人都可以分享一个不基于 IOUtils 且已知 有效的替代方案(或只是您的 thoughts/ideas)?嗯...上面的代码对我也有用过一段时间...所以,我知道很难保证一段代码(无论多小)是否真的有用!

此外,我真的很想让我的 WriteToFile 程序在可能的情况下将字符串保存到 ANSI 文件(uString 仅包含 ANSI 字符),否则保存为 Unicode。
然后 ReadAFile 函数应该自动检测编码并正确读回字符串。
这个想法是仍然有文本编辑器会错误地 open/interpret 一个 Unicode/UTF 文件。因此,只要有可能,就给用户一个好的旧 ANSI 文本文件。

所以:
- Overwrite/Append
- 尽可能保存为 ANSI
- 内存效率高(当要加载的文件为 2GB 时不要占用 4GB 内存)
- 应该适用于任何文本文件(显然最大 2GB)
- 没有 IOUtils(错误太多无法使用)

Inifiles 单元应该支持 unicode。至少根据这个答案:How do I read a UTF8 encoded INI file?

ini 文件常用于存储字符串、整数、布尔值甚至字符串列表。

    procedure TConfig.ReadValues();
    var
        appINI: TIniFile;
    begin
        appINI := TIniFile.Create(ChangeFileExt(Application.ExeName,'.ini'));

        try
            FMainScreen_Top := appINI.ReadInteger('Options', 'MainScreen_Top', -1);
            FMainScreen_Left := appINI.ReadInteger('Options', 'MainScreen_Left', -1);
            FUserName := appINI.ReadString('Login', 'UserName', '');
            FDevMode := appINI.ReadBool('Globals', 'DevMode', False);
        finally
            appINI.Free;
        end;
    end;

    procedure TConfig.WriteValues(OnlyWriteAnalyzer: Boolean);
    var
        appINI: TIniFile;
    begin
        appINI := TIniFile.Create(ChangeFileExt(Application.ExeName,'.ini'));

        try
            appINI.WriteInteger('Options', 'MainScreen_Top', FMainScreen_Top);
            appINI.WriteInteger('Options', 'MainScreen_Left', FMainScreen_Left);
            appINI.WriteString('Login', 'UserName', FUserName);
            appINI.WriteBool('Globals', 'DevMode', FDevMode);
        finally
            appINI.Free;
        end;
    end;

另请参阅关于 ini 文件的 embarcadero 文档:http://docwiki.embarcadero.com/Libraries/Seattle/en/System.IniFiles.TIniFile

只需使用 TStringList,直到文件大小 < ~50-100Mb(这取决于 CPU 速度):

procedure ReadTextFromFile(const AFileName: string; SL: TStringList);
begin
  SL.Clear;
  SL.DefaultEncoding:=TEncoding.ANSI; // we know, that old files has this encoding
  SL.LoadFromFile(AFileName, nil); // let TStringList detect real encoding.
  // if not - it just use DefaultEncoding.
end;

procedure WriteTextToFile(const AFileName: string; const TextToWrite: string);
var
  SL: TStringList;
begin
  SL:=TStringList.Create;
  try
    ReadTextFromFile(AFileName, SL); // read all file with encoding detection
    SL.Add(TextToWrite);
    SL.SaveToFile(AFileName, TEncoding.UTF8); // write file with new encoding.
    // DO NOT SET SL.WriteBOM to False!!!
  finally
    SL.Free;
  end;
end;

Then the ReadAFile function should automagically detect the encoding and correctly read the string back.

这是不可能的。如果解释为任何文本编码,则存在格式正确的文件。例如参见 [​​=13=].

这意味着你的目标无法实现,你需要改变它们。

我的建议是执行以下操作:

  • 选择单一编码,UTF-8,并坚持使用。
  • 如果文件不存在,创建它并写入UTF-8 字节。
  • 如果文件存在,打开它,查找到末尾,并追加UTF-8 字节。

不支持 UTF-8 的文本编辑器不值得支持。如果您愿意,可以在创建文件时包括一个 UTF-8 BOM。使用 TEncoding.UTF8.GetBytesTEncoding.UTF8.GetString 进行编码和解码。

代码基于 David 的建议:

{--------------------------------------------------------------------------------------------------
 READ/WRITE UNICODE
--------------------------------------------------------------------------------------------------}

procedure WriteToFile(CONST FileName: string; CONST aString: String; CONST WriteOp: WriteOperation= woOverwrite; WritePreamble: Boolean= FALSE); { Write Unicode strings to a UTF8 file. It can also write a preamble }
VAR
   Stream: TFileStream;
   Preamble: TBytes;
   sUTF8: RawByteString;
   aMode: Integer;
begin
 ForceDirectories(ExtractFilePath(FileName));

 if (WriteOp= woAppend) AND FileExists(FileName)
 then aMode := fmOpenReadWrite
 else aMode := fmCreate;

 Stream := TFileStream.Create(filename, aMode, fmShareDenyWrite);   { Allow read during our writes }
 TRY
  sUTF8 := Utf8Encode(aString);                                     { UTF16 to UTF8 encoding conversion. It will convert UnicodeString to WideString }

  if (aMode = fmCreate) AND WritePreamble then
   begin
    preamble := TEncoding.UTF8.GetPreamble;
    Stream.WriteBuffer( PAnsiChar(preamble)^, Length(preamble));
   end;

  if aMode = fmOpenReadWrite
  then Stream.Position:= Stream.Size;                               { Go to the end }

  Stream.WriteBuffer( PAnsiChar(sUTF8)^, Length(sUTF8) );
 FINALLY
   FreeAndNil(Stream);
 END;
end;


procedure WriteToFile (CONST FileName: string; CONST aString: AnsiString; CONST WriteOp: WriteOperation);
begin
 WriteToFile(FileName, String(aString), WriteOp, FALSE);
end;


function ReadFile(CONST FileName: string): String;  {Tries to autodetermine the file type (ANSI, UTF8, UTF16, etc). Works with UNC paths }
begin
 Result:= System.IOUtils.TFile.ReadAllText(FileName);
end;