File Handling In Pascal/ja

From Lazarus wiki
Revision as of 23:35, 14 February 2020 by Trev (talk | contribs) (Fixed syntax highlighting; deleted category included in page template)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

العربية (ar) English (en) español (es) suomi (fi) français (fr) 日本語 (ja) русский (ru) 中文(中国大陆)‎ (zh_CN) 中文(台灣)‎ (zh_TW)

ほとんどのプログラマにとって、ファイルの扱いは是非とも知らなければならないものです。ファイルはユーザの設定や、エラーのログや、その他諸々のものを保存するのに使えます。ここでは基本的なテクストファイルの扱い方を学びましょう。

オーソドックスな Pascal

Pascal は強固に型付けられた言語で、ファイル型もデータの型に file of を付けて作ります。

type
  fc = file of char;
  fi = file of integer;
  fr = file of real;

はそれぞれ、char 型、integer 型、real 型のデータを収めるファイルの宣言です。単純型だけではなく、配列型、レコード型、集合型といったユーザ定義の型のファイルも可能です。

テクスト型は文字からなるファイルですが、file of char や file of string とは異なった特徴をもっており、行の概念や文字列と数との間の自動変換などの機能を持っています。

テクスト型は、

Program TextWriteTest;
var t : text;
    i : integer;
begin
  assign(t, ファイル名);
  rewrite(t);
  for i := 1 to 100 do write(t, i : 4);
  writeln(t, ' は書式付出力の例');
  close(t)
end.

Program TextReadTest;
var t : text;
    i : integer;
    s : string;
begin
  assign(t, ファイル名);
  reset(t);
  for i := 1 to 100 do read(t, i);
  readln(t, s);
  close(t)
end.

のように用います。「ファイル名」のファイルをエディタその他で読むと、1から100までの数がならび、それぞれ四桁のスペースをとっているはずです。Free Pascal でコンソールアプリケーションを作成する場合はこのような伝統的なプログラミングが可能です。しかし Lazarus では assign, close, text といった識別子がウィジェットセットのプロパティ名やメソッド名として既に用いられていますので、system.text などと system ユニットの名前を明示するか、次に挙げられている TextFile 型などをお使いください。例えば TextFile 型は objpash.inc の中で TextFile = text として定義されており、AssignFile 手続きはユニット objpas で定義されていて、system.text を呼出します。


古い手続型スタイル

古典的な(オブジェクト指向ではない)Pascal では、TextFile 型を使って文字列を書き込むことができます。あるいは、自前のファイル型を定義することもできます。

...
type
 TIntegerFile = file of Integer; // Integer を書き込めます
 TPCharFile = file of PChar; // PChar を書き込めます
 TStringFile = file of String; // String を書き込めます
...

TStringFile = File とだけしてしまうと、何も書けません(訳注: バイト単位で読み書きする特殊なファイルになります)。TStringFile には整数を直接書くこともできません。というのもそれは string のファイルだからです。異なる型のデータを書き込むには、TextFile 型がよいでしょう。

IO エラーの扱い

コンパイラに IO エラーの場合どう処理するか知らせることができます: 例外を発生するか、例外を発生せず変数 IOResult に結果を格納するかです。これはコンパイラディレクティブです:

{$I-} // チェックを off にする。エラーが起きたら、全て変数 IOResult に行く
{$I+} // チェックを on に戻す。エラーが起きると EInOutError 例外になる

$I をディセーブル/オフにすると、ファイル操作の結果は変数 IOResult に行きます。これは符号無し整数型です。エラーの種類が格納されます。対応表は: [1] にあります。

ファイル手続き

System ユニットに含まれるファイル関係の手続き及び関数です。詳細は: Reference for 'System' unit をご覧下さい。前述のように、Lazarus ではプロパティ名やメソッド名で隠蔽されるものがあります。

  • Assign - ファイルとファイル名とを関連づけます
  • Append - 既存のファイルを開き、その最後からデータを追加していきます。
  • BlockRead - ファイルを指定のバイト数一気に読み、メモリに内容を格納します。型無しファイルも可能です。
  • BlockWrite - メモリの内容を、指定のバイト数一気にファイルに書き出します。型無しファイルも可能です。
  • Close - 開いたファイルを閉めます。
  • EOF - ファイル末に達したか調べます。
  • EOLN - テクストファイルで行末に達したか調べます。
  • Erase - ディスクからファイルを消去します。
  • FilePos - ファイル内の読み/書きする位置を得ます。
  • FileSize - ファイルの大きさを得ます。
  • Flush - ファイルバッファに残ったデータをディスクに書き出します。
  • IOResult - 最後のディスク IO 操作の結果を格納します。
  • Read - 変数にファイルの内容を読み込みます。テクストファイル以外にも使えます。
  • ReadLn - テクストファイルから行末まで読み、次の行に行きます。
  • Reset - 読み込み用にファイルを開きます。
  • Rewrite - 書き出し用にファイルを開きます。
  • Seek - ファイル内の読み/書きする位置を変更します。
  • SeekEOF - 空白を読み飛ばしながらファイル末か調べます。
  • SeekEOLn - 空白を読み飛ばしながら行末か調べます。
  • Truncate - 現在の位置でファイルを切り捨てます。
  • Write - ファイルに変数の内容を書き出します。テクストファイル以外にも使えます。
  • WriteLn - テクストファイルに変数の内容を書き出し、改行します。


TextFile 型を扱う例です。この例自体はコンソールアプリケーションですが、Lazarus で GUI アプリケーションを作成する場合は、こちらの例に従ってください。

program FileTest;
{$mode objfpc} // コンパイラをオブジェクト Pascal モードにします。これを忘れないで

uses
 Sysutils;

var
 FileVar: TextFile;  // textの代わりにこれを使います。

begin
  WriteLn('File Test');
  AssignFile(FileVar, 'Test.txt'); // assignの代わりにこれを使います。.txt はなくても構いませんが、ここでは付けておきましょう
  {$I+} // IOエラーの際、例外を発生させます
  try  
    Rewrite(FileVar);  // ファイルを新規に作成します  
    Writeln(FileVar,'Hello');
    CloseFile(FileVar);  // closeの代わりにこれを使います。
  except
    on E: EInOutError do
    begin
     Writeln('File handling error occurred. Details: '+E.ClassName+'/'+E.Message);
    end;    
  end;
  WriteLn('Program finished. Press enter to stop.');  
  ReadLn;  // ファイル変数を指定しないと標準入力から読みます。ここではリターンキーの空押しを待ちます。
end.

さあ、ファイルをテクストエディタで開いて、「Hello」と書かれているのを確認しましょう! 一度これを走らせると、エラー処理をテストすることができます。test.txt を読み取り専用にしてもう一度プログラムを走らせてみてください。

注意: ここでは {$I+} にして例外を発生させています。複数のファイル操作を行う際は、この方がエラーの扱いが簡単だからです。{$I-} にすることもできますが、ファイル操作を行うたびに IOResult をチェックしなければなりません。そうしないと次の操作で IOResult の内容が変わってしまいます。

テクストファイルに追加する方法です:

program EditFile;
{$mode objfpc}

uses
 Sysutils;

var
 File1: TextFile;
 
begin
  WriteLn('Append file');
  {$I+}
  try
    AssignFile(File1, 'File.txt');
    Append(File1, 'adding some text...');
    CloseFile(File1);
  except
    on E: EInOutError do
    begin
     Writeln('File handling error occurred. Details: '+E.ClassName+'/'+E.Message);
    end;    
  end;
  WriteLn('Program finished. Press enter to stop.');  
  Readln;
end.

ファイルを読む例:

program ReadFile;
{$mode objfpc}

uses
 Sysutils;

var
 File1: TextFile;
 Str: String;
 
begin
  Writeln('File Reading:');
  AssignFile(File1, 'File.txt');
  {$I+}
  try
    Reset(File1);
    repeat
      Readln(File1, Str); // 行全体を変数 Str に読み込みます
      Writeln(Str); // 読んだ行を書きます。ファイル変数を指定しない場合は標準出力に書きます。
    until(EOF(File1)); // ファイルの最後に達するまで繰り返して新しい行を読みます。
    CloseFile(File1);
  except
    on E: EInOutError do
    begin
     Writeln('File handling error occurred. Details: '+E.ClassName+'/'+E.Message);
    end;    
  end;
  WriteLn('Program finished. Press enter to stop.');  
  Readln;
end.

場合によっては、string ではなく char を使ってファイルを扱うこともできます。カッコいいですね (^_-)

オブジェクトスタイル

上述の古いスタイルのファイル操作に加えて、stream (データストリーム)の概念に基づく抽象度の高いシステムが用意されています。これは、ファイルの扱いをもっと少ないステップで行えるということです。

加えて、文字列を扱うほとんどのクラスにファイルからの読み書き能力が備わっています。それらは通常 SaveToFile と LoadFromFile という名前になっています。Lazarus grid のような多くの他のオブジェクトにも同様の機能があります。Lazarus dataset (DBExport) もそうです。そのドキュメントやソースコードにはインポート/エクスポートルーチンに取り組む前に一読する価値があります。

バイナリファイル

ファイルを直接開くには、TFileStream を使います。このクラスは、FileUtil ユニットにある FileOpen、FileCreate、FileRead、FileWrite、FileSeek、FileClose というシステム手続きをカプセル化したものです。

IO routines

下の例で、ファイル操作をどのように try..finally ブロックの中にカプセル化するかご覧下さい。こうすれば、ファイル操作その他でエラーが起きたとしても、Filestream オブジェクトは確実に解放されます(finally パート)。

var
  Buffer: array[0..9999] of Byte;
begin
  with TFileStream.Create('SomeFile.bin', fmCreate) do 
  try
    Seek('Hello');
    Write(Buffer, SizeOf(Buffer));
  finally
    Free;
  end;
end;

システムのメモリ容量と比較して小さなファイルは、その全体をメモリに読み込むこともできます。大きなファイルの読み込みもできますが、オペレーティングシステムはページスワップを始めるでしょうから、処理の効率という観点から無駄になります。

begin
  with TMemoryStream.Create do 
  try
    LoadFromFile('SomeFile.bin');
    Seek(0, soEnd);
    Write(Ord('A'), 1);
    SaveToFile('SomeFile.bin');
  finally
    Free;
  end;
end;

ギガバイトクラスのファイルになると、例えば 4096 バイトずつバッファに読み込み(ファイルシステムのクラスタあるいはブロックサイズの倍数になるようにすると効率が上がります)、そのバッファに対して処理を行うようにしたくなるでしょう。

var
  TotalBytesRead, BytesRead : Int64;
  Buffer : array [0..4095] of byte;  // または array [0..4095] of char
  FileStream : TFileStream;

try
  FileStream := TFileStream.Create;
  FileStream.Position := 0;  // ファイルの先頭に読み書き点を持っていく
  while TotalBytesRead <= FileStream.Size do  // 読み込み量 <= ストリームのサイズの間繰り返す
  begin
    BytesRead := FileStream.Read(Buffer,sizeof(Buffer));  // 4096 バイト読む
    inc(TotalBytesRead, BytesRead);                       // バッファの大きさ分(4096) TotalByteRead を増やす
    // バッファのデータに対しなにかする
  end;

ファイルのコピー

上記の例を使って、簡単なファイルコピー関数を実装することができます(Lazarus のランタイムライブラリにはこれがありますが、Free Pascal のにはありません)。必要に応じて、大きなファイルに対応するように改造する等してください:

function FileCopy(Source, Target: string): boolean;
// Source から Target に上書きコピーする。
// ファイル全体をメモリに読み込む。
// 成功すれば true を、失敗すれば false を返す。
var
  MemBuffer: TMemoryStream;
begin
  result:=false;
  MemBuffer:=TMemoryStream.Create;
  try
    try
      MemBuffer.LoadFromFile(Source);
      MemBuffer.Position:=0;
      MemBuffer.SaveToFile(Target); // ソースと同じはず
      result:=true;
    except
      result:=false; // 例外を飲んでエラーコードにする
    end;
  finally
    MemBuffer.Free;
  end;
end;

テクストファイル

一般に、テクストファイルに対しては TStringList クラスを用いてファイル全体をメモリに読み込みます。そうすれば、各行に容易にアクセスできます。もちろん、StringList をファイルに書き戻すこともできます:

begin
  with TStringList.Create do 
  try
    Add('Hello');
    SaveToFile('SomeFile.txt');
  finally
    Free;
  end;
end;

ストリームに一行書き込むには、次の手続きを使いたくなるかもしれません(訳注: Pascal の string 型の実装は、歴史的に変遷してきました。この例では、{$H+}コンパイラディレクティブが使われていないので、伝統的な実装を用いていると思われます。すなわち、最初の1バイトが文字列の長さをしめし、第二バイト以降が文字列の実体を格納するという方法です。このタイプの文字列型を明示的に指定したい場合は、shortstring 型を用います。{$H+}ディレクティブを指定すると、string 型は C 言語のように、ヌル文字を終端とする文字列の先頭のアドレスを示すポインタとして実装されるため、以下のような手続きは不要になるはずです。TStringList 自体はこちらの方法で実装されています):

procedure SaveStringToPath(theString, filePath: String);
var
  textFile: TFileStream = nil;
  textLength: integer;
  stringBuffer: ^String;
begin
  textLength := length(theString);
  try
    textFile := TFileStream.Create(filePath, fmOpenWrite or fmCreate);
    { write string to stream while avoiding to write the initial length }
    stringBuffer := @theString + 1;
    textFile.WriteBuffer(stringBuffer^, textLength);
  finally
    if textFile <> nil then textFile.Free;
  end;
end;

関連項目