連載
» 2018年06月27日 05時00分 公開

.NET TIPS:バイナリファイルを非同期的に読み書きするには?[C#/VB、.NET 4.5]

.NET Framework 4.5以降でFileStreamクラスに追加されたReadAsync/WriteAsyncメソッドを使い、非同期的にバイナリファイルを読み書きする方法を説明する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載「.NET TIPS」

 ファイルの読み書きは、特にそのファイルサイズが大きいと時間がかかるものだ。その待ち時間中にUIが無応答になってしまうのを避けるには、読み書き処理を非同期的に行えばよい。.NET Framework 4.5からは、そのようなコードが簡単に書けるようになっている。本稿では、非同期的にバイナリファイルを読み書きする方法を解説する。

POINT バイナリファイルの内容を非同期的に読み書きする方法

バイナリファイルの内容を非同期的に読み書きする方法まとめ バイナリファイルの内容を非同期的に読み書きする方法まとめ


 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

関連TIPS

 テキストファイルの内容を非同期的に読み書きする方法は、次のTIPSを参照してほしい。

 バイナリファイルを読み書きするには、その目的や利用している.NET Frameworkのバージョンによって、さまざまな方法がある。適切な方法を選んでほしい。


 なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2015以降が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。また、サンプルということで、例外処理は省略している。実際には、指定したパスが存在していなかったり、アクセス権がなくて書き込めなかったりしたときなどに例外が発生するので、適切にtry〜catchしていただきたい。

using static System.Console;

Imports System.Console

本稿のサンプルコードに必要な宣言(上:C#、下:VB)

バイナリファイルを非同期的に読み書きするメソッド

 バイナリファイルを非同期的に読み書きするには、FileStreamクラス(System.IO名前空間)を利用する。

 同期的に読み書きするメソッドとしてReadメソッドとWriteメソッドがあり、その非同期バージョンとしてReadAsyncメソッドとWriteAsyncメソッドが.NET Framework 4.5で追加された。

  • ReadAsyncメソッド:ファイルの内容を読み取り、引数に与えられたバイト配列へ格納する
  • WriteAsyncメソッド:引数に与えられたバイト配列をファイルに非同期的に書き込む

 その引数や使い方は同期バージョンであるReadメソッド/Writeメソッドと同様なので、そちらについて解説した「バイナリ・ファイルを読み書きするには?[C#、VB]」をご覧いただきたい。

 同期バージョンとの大きな違いは、非同期バージョンでは積極的な理由がない限り一気に読み書きしてしまえばよいところだ。同期バージョンでは、UIがフリーズしないように、ある程度の小さいサイズに分けて読み書きする必要があった。非同期バージョンではその心配がないので、大きなファイルでもいっぺんに読み書きしてしまえばよい。小分けにしたい積極的な理由としては、メモリに載り切らない大きなファイルを扱うときや、読み書きの進捗表示を出したいときなどが考えられる。

 なお、(ファイルを上書きするのではなく)ファイルの末尾に追加したいときは、ファイルを開いた後でSeekメソッドを使って書き込み位置を末尾に移動するか、あるいは、ファイルを開くときにFileStreamコンストラクタ引数でFileMode.Appendを指定する。指定するFileMode列挙体(System.IO名前空間)は次の通りだ。

  • Append:既存ファイルを書き込み用に開く(ないときは新規作成)。書き込みは追加になる(開いた時点の末尾より前にSeekメソッドで戻れない)
  • Create:書き込み用に新しくファイルを作る(既存ファイルがあるときは上書き)
  • CreateNew:書き込み用に新しくファイルを作る(既存ファイルがあるときは例外)
  • Open:既存ファイルを開く(ないときは例外)
  • OpenOrCreate:既存ファイルを開く(ないときは新規作成)
  • Truncate:既存ファイルを書き込み用に開き、ファイルサイズをゼロにする

 そして、WriteAsyncメソッドで書き込んだ後、FileStreamオブジェクトをクローズする(=明示的にCloseメソッドを呼び出すか、あるいは、usingブロックから抜ける)と、完全にファイルへ書き出される。クローズするまでは、ファイルに書き出されていないデータがバッファリングされてメモリ上に残っている可能性があるので、注意してほしい。例えばログの書き出しなどのように、ファイルを開いたままで(=FileStreamオブジェクトをクローズすることなく)しばらく書き込みを中断するときには、FileStreamオブジェクトのFlushAsyncメソッドを呼び出すようにする。そうすれば、そこまでの内容が完全にファイルへ書き出される。

 また、複数のスレッドから同時に書き込む可能性がある場合には、スレッド間の排他ロックが必要になるが、非同期メソッドに対してはlockステートメントが使えない。代わりにSemaphoreSlimクラス(System.Threading名前空間)などを使う。詳しくは次のTIPSをご覧いただきたい。

非同期的に読み書きするには?

 実際にバイナリファイルへ書き出して、それを読み込むサンプルを次のコードに示す。この例では、書き込み用に開くときにFileMode.Createを指定しているので上書きになる(追加にしたいときはFileMode.Appendにする)。

static async void BinaryReadWriteAsync(byte[] data)
{
  // 読み書きするファイル(実行ファイルと同じフォルダに作られる)
  const string FilePath = @".\sample.dat";

  // バイナリファイル書き込み

  // ファイルを上書きモードで開く(ファイルがないときは作る)
  // 追加モードにするにはFileModeをAppendに変える
  using (var fs = new System.IO.FileStream(FilePath,
    System.IO.FileMode.Create, System.IO.FileAccess.Write))
  {
    // バイナリデータを非同期的に書き込む
    await fs.WriteAsync(data, 0, data.Length);

  } // usingを抜けるとき、ファイルへ完全に書き込まれる

  // バイナリファイル読み込み

  byte[] result; // データを格納する配列

  // ファイルを読み取りモードで開く
  using (var fs = new System.IO.FileStream(FilePath,
    System.IO.FileMode.Open, System.IO.FileAccess.Read))
  {
    // データ格納用の配列を確保する
    result = new byte[fs.Length];
      
    // バイナリデータを非同期的に読み込む
    await fs.ReadAsync(result, 0, (int)fs.Length);
  }

  // 読み込んだ内容をコンソールへ出力する
  for(int i=0; i<result.Length; i++)
  {
    Write($"{result[i]:X2} ");
    if (i % 16 == 7)
      Write(" ");
    if (i % 16 == 15)
      WriteLine();
  }
  WriteLine();
}

Async Sub BinaryReadWriteAsync(data As Byte())

  ' 読み書きするファイル(実行ファイルと同じフォルダに作られる)
  Const FilePath As String = ".\sample.dat"

  ' バイナリファイル書き込み

  ' ファイルを上書きモードで開く(ファイルがないときは作る)
  ' 追加モードにするにはFileModeをAppendに変える
  Using fs = New System.IO.FileStream(FilePath,
    System.IO.FileMode.Create, System.IO.FileAccess.Write)

    ' バイナリデータを非同期的に書き込む
    Await fs.WriteAsync(data, 0, data.Length)

  End Using 'usingを抜けるとき、ファイルへ完全に書き込まれる

  ' バイナリファイル読み込み

  Dim result() As Byte ' データを格納する配列

  ' ファイルを読み取りモードで開く
  Using fs = New System.IO.FileStream(FilePath,
    System.IO.FileMode.Open, System.IO.FileAccess.Read)

    ' データ格納用の配列を確保する
    ReDim result(fs.Length - 1)

    ' バイナリデータを非同期的に読み込む
    Await fs.ReadAsync(result, 0, fs.Length)
  End Using

  ' 読み込んだ内容をコンソールへ出力する
  For i As Integer = 0 To (result.Length - 1)
    Write($"{result(i):X2} ")
    If (i Mod 16 = 7) Then
      Write(" ")
    End If
    If (i Mod 16 = 15) Then
      WriteLine()
    End If
  Next
End Sub

バイナリファイルを読み書きするメソッドの例(上:C#、下:VB)
ReadAsyncメソッド/WriteAsyncメソッドの引数については、「バイナリ・ファイルを読み書きするには?[C#、VB]」をご覧いただきたい。
非同期メソッドを呼び出してその完了を待機するには、呼び出す部分にawaitキーワードが、また、メソッドのシグネチャにasyncキーワードが必要だ。また、FileStreamクラスはIDisposableインタフェースを実装しているので、このコードのようにusing句を使って、間違いなくファイルが書き出されてリソースが解放されるようにする。
なお、このメソッドを呼び出す側で、このメソッドの完了を待機したい場合は、このメソッドの返値をTask型(System.Threading.Tasks名前空間)に変える(メソッド本体のコードは変わらず)。

 上のメソッドをコンソールアプリのMainメソッド内から呼び出してみると、次のコードのようになる。

// 書き込むデータ
string src = ".NET TIPS:バイナリ・ファイルを読み書きするには?";
byte[] bytes = new byte[src.Length * sizeof(char)];
System.Buffer.BlockCopy(src.ToCharArray(), 0, bytes, 0, bytes.Length);

// 非同期読み書きメソッドを呼び出し
BinaryReadWriteAsync(bytes);
WriteLine("BinaryReadWriteAsyncメソッドの呼び出し完了");
// 出力例:
// BinaryReadWriteAsyncメソッドの呼び出し完了
// 2E 00 4E 00 45 00 54 00  20 00 54 00 49 00 50 00
// 53 00 1A FF D0 30 A4 30  CA 30 EA 30 FB 30 D5 30
// A1 30 A4 30 EB 30 92 30  AD 8A 7F 30 F8 66 4D 30
// 59 30 8B 30 6B 30 6F 30  1F FF

' 書き込むデータ
Dim src As String = ".NET TIPS:バイナリ・ファイルを読み書きするには?"
Dim bytes(src.Length * Len(New Char()) - 1) As Byte
System.Buffer.BlockCopy(src.ToCharArray(), 0, bytes, 0, bytes.Length)

' 非同期読み書きメソッドを呼び出し
BinaryReadWriteAsync(bytes)
WriteLine("BinaryReadWriteAsyncメソッドの呼び出し完了")
' 出力例:
' BinaryReadWriteAsyncメソッドの呼び出し完了
' 2E 00 4E 00 45 00 54 00  20 00 54 00 49 00 50 00
' 53 00 1A FF D0 30 A4 30  CA 30 EA 30 FB 30 D5 30
' A1 30 A4 30 EB 30 92 30  AD 8A 7F 30 F8 66 4D 30
' 59 30 8B 30 6B 30 6F 30  1F FF

上記メソッドの使用例(上:C#、下:VB)
書き込むデータとして、文字列をバイト配列にコピーしたものを使っている。出力結果を見ると、UTF-16になっているのが分かる。
また、出力例を見ると、ファイルの読み書き処理が終わる前に「呼び出し完了」のメッセージが出力されている。ファイルの読み書き処理が非同期的に実行されているためだ。ファイルの読み書きは非同期に実行されるが、場合によっては、読み書きが先に完了して上の出力例とは異なる順番で表示されることもある。
ファイルには上書きしているので、このコンソールアプリを何度呼び出してもファイルに書き込まれる内容は同じになる。

まとめ

 バイナリファイルを非同期的に読み書きするには、FileStreamクラスを使う。書き込み用としてFileStreamオブジェクトを作るときに、上書き/追加の区別を指定できる。分割して読み書きする積極的な理由がなければ、ファイル全体をまとめて読み書きすればよい。非同期的な処理はUIをブロックしないからだ。

利用可能バージョン:.NET Framework 4.5以降
カテゴリ:クラスライブラリ 処理対象:バイナリファイル
使用ライブラリ:FileStreamクラス(System.IO名前空間)
関連TIPS:バイナリ・ファイルを読み書きするには?
関連TIPS:バイナリ・ファイルを簡単に読み書きするには?[2.0のみ、C#、VB]
関連TIPS:テキストファイルの内容を非同期的に読み込むには?[C#/VB、.NET 4.5]
関連TIPS:テキストファイルの内容を非同期的に書き込むには?[C#/VB、.NET 4.5]
関連TIPS:ファイルにテキストを書き込むには?[C#、VB]
関連TIPS:テキスト・ファイルの内容を簡単に書き込むには?[2.0のみ、C#、VB]
関連TIPS:オープン中のファイルにアクセスするには?[C#、VB]
関連TIPS:async/awaitで例外処理をするには?[C#/VB]
関連TIPS:非同期:awaitを含むコードをロックするには?(SemaphoreSlim編)[C#、VB]
関連TIPS:非同期:awaitを含むコードをロックするには?(AsyncLock編)[C#、VB]
関連TIPS:ファイルをコピー/削除/リネーム/移動するには?
関連TIPS:VB.NETで配列を宣言するには?
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2018 Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。