【Delphi】TStreamReader / TStreamWriter の使い方 #プログラミング

皆さんは Delphi でのテキストファイルの読み書きに何を使ってますか?TStringList ですか?TFileStremですか?昔ながらの Read(ln) / Write(ln) でしょうか?

今回は Unicode 版 Delphi であればもれなく使える TStreamReaderTStreamWriter のお話です。

.NET 互換の TStreamReader / TStreamWriter は Delphi 2009 で実装されました。

TStreamReader / TStreamWriter は TStringList と違ってバッファをメモリに溜め込まないので大きなファイルを処理するのに向いています。

.NET 由来ではありますが Pascal の伝統的なファイル操作に似ている所もあって、実際に使ってみると意外と馴染みます。

基本的な TStreamReader / TStreamWriter の使い方

■ TStreamWriter の基本的な使い方

TStreamWriter の方から説明します。最も簡単な使い方は次のようになります。実行すると UTF-8 のテキストファイルが生成されます。

uses
  ..., Classes;

var
  Writer: TStreamWriter;
begin
  Writer := TStreamWriter.Create('hello.txt'); // 上書きモード
  try
    Writer.WriteLine('Hello,');
    Writer.WriteLine('world.');
  finally
    Writer.Free;
  end;
end;

TStreamWriter のコンストラクタ (パラメータとしてファイル名を受け付ける)

パラメータ #1 #2 #3 #4
名前 Filename Append Encoding BufferSize
string Boolean TEncoding Integer
デフォルト (なし) False TEncoding.UTF-8 4096 1

コンストラクタ Create() の 2 番目のパラメータとして True を指定すると追記モードになります (デフォルトで False です)。

  // 追記モード, UTF-8
  Writer := TStreamWriter.Create('hello.txt', True); 

コンストラクタ Create() の 3 番目のパラメータとして TEncoding を渡して文字コードを指定する事もできます。

  // 上書モード, UTF-8
  Writer := TStreamWriter.Create('hello.txt', False, TEncoding.UTF8); 

注意点ですが、上書きモード (正確にはストリームポインタがファイルの先頭にある状態) かつ Encoding パラメータに TEncoding.UTF8 が指定された場合には BOM が書き込まれます。つまり、次の 2 つは同等ではありません

  // 上書きモード, UTF-8 (BOM なし UTF-8)
  Writer := TStreamWriter.Create('hello.txt'); 
  
  // 上書きモード, UTF-8 (BOM あり UTF-8)
  Writer := TStreamWriter.Create('hello.txt', False, TEncoding.UTF8); 

ストリームの先頭へ移動すればこの問題を回避できます。

  // 上書きモード, UTF-8 (BOM なし UTF-8)
  Writer := TStreamWriter.Create('hello.txt'); 

  // 上書きモード, UTF-8 (BOM あり UTF-8)
  Writer := TStreamWriter.Create('hello.txt', False, TEncoding.UTF8); 
  Writer.BaseStream.Seek(0, TSeekOrigin.soBeginning); // ストリームの先頭に移動 (BOM を書き込まない)

TStreamWriter のプロパティ

プロパティ デフォルト 説明
AutoFlush Boolean True 自動でフラッシュ (ファイルへの書き出し) するか?
BaseStream TStream 書き込みに使われている TStream 書き込みに使われている TStream
Encoding TEncoding TEncoding.UTF8 文字エンコーディング
NewLine string sLineBreak 改行文字

TStreamWriter のメソッド

メソッド 説明
Close() TStreamWriter を閉じる。このメソッドはデストラクタで呼ばれ、明示的に呼んでも TStreamWriter のインスタンスは破棄されない
Flush() フラッシュ (ファイルへの書き出し) を行う。AutoFlush プロパティが True の場合には呼び出す必要がない
Free() TStreamWriter を破棄する。デストラクタで Close() が呼ばれる
OwnStream() 書き込みに使われている TStream のオーナーを TStreamWriter に設定する
Write() データを文字列として書き込む
WriteLine() データを行の終端文字で終わる文字列として書き込む

■ TStreamReader の基本的な使い方

次は TStreamReader です。最も簡単な使い方は次のようになります。実行すると (BOM なし) UTF-8 のテキストファイルを読み込みます。

uses
  ..., Classes;

var
  Reader: TStreamReader;
  LineStr: string;
begin
  Reader := TStreamReader.Create('hello.txt');
  try
    while Reader.EndOfStream do
    begin
      LineStr := Reader.ReadLine;
      ...
    end;
  finally
    Reader.Free;
  end;
end;  

TStreamReader のコンストラクタ (パラメータとしてファイル名を受け付ける)

パラメータ #1 #2
名前 Filename DetectBOM
string Boolean
デフォルト (なし)
パラメータ #1 #2 #3 #4
名前 Filename Encoding DetectBOM BufferSize
string TEncoding Boolean Integer
デフォルト (なし) TEncoding.UTF-8 False 4096 1

コンストラクタ Create() の DetectBOM パラメータに True を指定すると UTF-8 (BOM あり) を検出して読み込めます。つまり、次のような記述であれば、BOM の有無を気にせず UTF-8 形式のファイルを読み込めます。

  // UTF-8, BOM 自動検出
  Reader := TStreamReader.Create('hello.txt', True);

コンストラクタ Create() の Encoding パラメータに TEncoding を指定し、DetectBOM パラメータに True を設定すると UTF-8 (BOM あり) だった場合には UTF-8 (BOM あり) で、それ以外の場合には指定された文字エンコーディングで読み込みます。

  // 文字エンコーディング指定, BOM 自動検出
  Reader := TStreamReader.Create('hello.txt', TEncoding.Default, True);

Delphi XE7〜10.2 Tokyo で、TEncoding を指定し、かつ DetectBOM を有効にしたコンストラクタを使って BOM あり UTF-8 ファイルを読み込むとエラーになる事があるようです。

根本的な解決方法はありませんが、問題が起こる環境では自前で BOM を判定すればいいだけなので、解っていれば対処は難しくないと思います。

TStreamReader のプロパティ

プロパティ デフォルト 説明
BaseStream TStream 読み込みに使われている TStream 読み込みに使われている TStream
CurrentEncoding TEncoding 現在の文字エンコーディング
EndOfStream Boolean ストリームの終端ならば True

TStreamReader のメソッド

メソッド 説明
Close() TStreamReader を閉じる。このメソッドはデストラクタで呼ばれ、明示的に呼んでも TStreamReader のインスタンスは破棄されない
DiscardBufferedData() バッファされているすべてのデータを破棄する
Free() TStreamReader を破棄する。デストラクタで Close() が呼ばれる
OwnStream() 2 読み込みに使われている TStream のオーナーを TStreamReader に設定する
Peek() ストリームポインタを変更せずに、次の文字を取得する。ストリーム終端ならば -1 が返る
Read() ストリームポインタを変更し、次の文字を取得する。ストリーム終端ならば -1 が返る
ReadBlock() 一連の文字を読み込む
ReadLine() 1 行分の文字列を読み込む
ReadToEnd() 行末までの文字列を読み込む
Rewind() 3 バッファされているすべてのデータを破棄し、ストリームポインタをストリームの先頭へ移動する

Rewind() は古いバージョンには存在しませんが、次のコードと同等です。

  Reader.DiscardBufferedData;
  Reader.BaseStream.Seek(0, TSeekOrigin.soBeginning);

■ コンストラクタに TStream を指定する場合

TStreamReader / TStreamWriter には TStream をパラメータとして受け付けるオーバーロードされたコンストラクタがあります。

TStreamWriter のコンストラクタ (パラメータとして TStream を受け付ける)

パラメータ #1 #2 #3
名前 Stream Encoding BufferSize
TStream TEncoding Integer
デフォルト (なし) TEncoding.UTF-8 4096 1

TStreamReader のコンストラクタ (パラメータとして TStream を受け付ける)

パラメータ #1 #2
名前 Stream DetectBOM
TStream Boolean
デフォルト (なし)
パラメータ #1 #2 #3 #4
名前 Stream Encoding DetectBOM BufferSize
TStream TEncoding Boolean Integer
デフォルト (なし) TEncoding.UTF-8 False 4096 1

パラメータに TStream が渡された場合、デフォルトでは TStreamReader / TStreamWriter を破棄しても TStream は破棄されません。

例えば IOUtils.TFile 4 を使ってファイルストリームを作って渡す場合には、ファイルストリームも破棄する必要があります。

  Writer := TStreamWriter.Create(TFile.Create('hello.txt'));
  try
    Writer.WriteLine('Hello,');
    Writer.WriteLine('world.');
  finally
    Writer.BaseStream.Free; // コンストラクタに渡された TStream を破棄
    Writer.Free;
  end;

コンストラクタに渡された TStream のオーナーを TStreamReader / TStreamWriter に変更すれば TStreamReader / TStreamWriter が破棄されたと同時に破棄されます 2

  Writer := TStreamWriter.Create(TFile.Create('hello.txt'));
  try
    Writer.OwnStream; // Stream のオーナーを TStreamWriter に
    Writer.WriteLine('Hello,');
    Writer.WriteLine('world.');
  finally
    Writer.Free; // デストラクタで Close() が呼ばれ、Stream が破棄される。
  end;          

■ TStreamReader / TStreamWriter を標準入出力に割り当てる (Windows)

次のコードでコンソールアプリケーションで TStreamReader / TStreamWriter を標準入出力に割り当てる事ができます。

uses
  ..., Classes, Windows;

var
  Reader: TStreamReader;
  Writer: TStreamWriter;
begin
  Reader := TStreamReader.Create(THandleStream.Create(GetStdHandle(STD_INPUT_HANDLE)));
  Writer := TStreamWriter.Create(THandleStream.Create(GetStdHandle(STD_OUTPUT_HANDLE)));
  try
    Writer.WriteLine('Hello,world.');
    Reader.ReadLine;
  finally
    Reader.BaseStream.Free;
    Reader.Free;
    Writer.BaseStream.Free;
    Writer.Free;
  end;
end;

■ TStreamWriter の Write() / WriteLine()

TStreamWriter の Write() / WriteLine() には多くのオーバーライドされたメソッドが存在するため、文字列へと変換する機会はそうそうないかと思います。

  // Write()
  procedure Write(Value: Boolean); override;
  procedure Write(Value: Char); override;
  procedure Write(const Value: TCharArray); override;
  procedure Write(Value: Double); override;
  procedure Write(Value: Integer); override;
  procedure Write(Value: Int64); override;
  procedure Write(Value: TObject); override;
  procedure Write(Value: Single); override;
  procedure Write(const Value: string); override;
  procedure Write(Value: Cardinal); override;
  procedure Write(Value: UInt64); override;
  procedure Write(Value: TCharArray; Index, Count: Integer); override;
  // WriteLine()
  procedure WriteLine; override;
  procedure WriteLine(Value: Boolean); override;
  procedure WriteLine(Value: Char); override;
  procedure WriteLine(const Value: TCharArray); override;
  procedure WriteLine(Value: Double); override;
  procedure WriteLine(Value: Integer); override;
  procedure WriteLine(Value: Int64); override;
  procedure WriteLine(Value: TObject); override;
  procedure WriteLine(Value: Single); override;
  procedure WriteLine(const Value: string); override;
  procedure WriteLine(Value: Cardinal); override;
  procedure WriteLine(Value: UInt64); override;
  procedure WriteLine(Value: TCharArray; Index, Count: Integer); override;

書式付き Write() / WriteLine()

Format() と同じ書式文字列とオープン配列コンストラクタをパラメータとして渡せる Write() / WriteLine() メソッドが用意されています。

  // Write()
  procedure Write(const Format: string; Args: array of const); override;
  // WriteLine()
  procedure WriteLine(const Format: string; Args: array of const); override;

標準手続きの Write() / Writeln() の可変パラメータと似たような記述が可能となっています。

  // Write()
  Writer.Write('%s_%.3d.txt', ['LOG', rev]);
  // WriteLine()
  Writer.WriteLine('%.4d: %s', [Id, Name]);

See also:

■ TStrem.Seek() によるストリームポインタの移動

関連しますが、TStrem.Seek() の 2 番目のパラメータ (Origin) には TSeekOrigin 列挙型を指定して、巨大なファイルでも問題なく移動できるようにすべきです。

具体的には soBeginning などではなく、TSeekOrigin.soBeginning のように明示します。

  Reader.BaseStream.Seek(FileSize, soBeginning); // NG
  Reader.BaseStream.Seek(FileSize, TSeekOrigin.soBeginning); // OK 

TSeekOrigin. で修飾すると常に Int64 の方の Seek() が選択されるからです。

  function Seek(Offset: Longint; Origin: Word): Longint; overload; virtual;
  function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; virtual;

See also:

TStringList が便利だからと、なんでもかんでも TStringList でやっているとパフォーマンスの低下につながる事があります。

ちょっとクセがあったりもしますが、テキストファイルの読み書きを TStreamReader / TStreamWriter に置き換えて高速化が行えないか検討してみてはいかがでしょうか?

余談

本記事のコードは可能な限り古いバージョンでも通るように記述しましたが、Delphi 10.3 以降のインライン変数宣言と型推論を使えばもっと簡潔に書けます。

最も簡単な使い方は次のようになります

まぁ、with 文を使えばもっと簡単になりますけれど。

TStreamWriter

  with TStreamWriter.Create('hello.txt') do
  try
    WriteLine('Hello,');
    WriteLine('world.');
  finally
    Free;
  end;

TStreamReader

  with TStreamReader.Create('hello.txt', True) do
  try
    while EndOfStream do
    begin
      var LineStr := ReadLine;
      ...
    end;
  finally
    Free;
  end;

See also:



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link