概要

これまで我々が見ていたものは、オブジェクトや変数として表された情報を操作するものだったが、本章は違う……。ディスク上のファイルや http レスポンスについては、「ストリーム API」を利用してプログラムの入出力を表現してるんだって。

 

流し読みノート

ストリームって何やねん

  • ストリームってのは、実態としてはただのバイト列だ。
    • バイトは8ビットのことだね。ただときおり7ビットを1バイトと言うこともあってややこしいから、8ビットのことをオクテットと表現することもネットワーク規格界隈ではあるらしい。
  • ストリーム API を使えばバイトデータを扱える。

 

ストリーム API ってどんなん

Stream クラスのことだ。データをバイト列として表現する機能を抽象化したものである。こんなん↓。

// Stream クラスの最重要メンバたち。

// 書き込み専用ストリームに使うと NotSupportedException
public abstract int Read(byte[] buffer, int offset, int count);

// 読み取り専用ストリームに使うと NotSupportedException
public abstract void Write(byte[] buffer, int offset, int count);

public abstract long Position { get; set; }

// まあ詳しい使い方はわからんけど、実際のところバイト列であるストリームに読み書きできるってことよね。
  • Read について。
    • int を返す点に注意。この int はストリームから読み取ったバイト数。べつにリクエストした数ぶんがかならず読み取れるわけではない。
    • うんうん、だから返り値になってるんだろうな。リクエストしたぶん読み取れるなら返り値いらないもんね。
    • たとえば Read の返り値が0であればもう読み取るものはないという判断ができるのだ。
    • ReadByte というメソッドもある。これは読み取る数をリクエストできないんだけど、かわりに、1バイトずつ絶対返してくれて、読み取るものがなくなれば-1を返す。
      • リクエストしたぶんが読み取れるとは限らないというややこしいのを回避できる。
      • ただし1個ずつしか読み取らないから、大きなデータの塊(チャンクと呼ぶらしい)を読むときはかなり効率が悪い。
  • Write について。
    • ストリームへのデータ書き込みは即時ではない。パフォーマンスのためらしい。
      • 即時に書いてほしいときは Flush メソッドを使うこと。トイレを流すときの動詞やんけ。覚えやすい。そのせいで書き込みデータを貯めるという行為(バッファリングというようだ)が、便を貯めるイメージになっちゃいましたけど。(ヤダサイテー)
  • Position について。
    • Position というプロパティがあることからわかるよう、ストリームには「現在の位置」というものがあるそうだ。(は?)
      • Seek メソッドによって、現在位置を相対的に変更できる。
      • 現在の位置……? たしかに Read では一度に読み取る量をコントロールできて、もう読み取るものがないときは0を返すとあるから、ちょっとずつ進んでいくものなのだろうけど。
  • ストリームのコピーには CopyTo メソッドを使う。
    • 「そりゃあるだろって感じだけど知らねー開発者たちが自作しちゃってるって話があるから明記しとくぞ」(意訳)

いやー、バイト列サンとは Python でも出会ったことがあるから馴染みはあるけれど、うんざりしますね。文字列ならわかりやすいのになー。本質的にはどちらも同じようなものなのだろうけれど、ぱっと見の親しみやすさが違うよ。と、ここで安心。 Stream は抽象クラスであり、実際に使う、これを継承した API の中には文字列を扱うようなものもある。助かるわぁ。

 

実際に使うのはどんなん

  • バイト列だとタルい、文字列にしろ、ってときは TextReaderTextWriter を使う。要素が byte ではなく char で表されているところが違う。
  • StringReaderStringWriterMemoryStream とよく似ていて、 TextReader 系と同じ感じで使えるけれど、すべてもメモリ上で処理できる。
  • StreamReaderStreamWriter がもっともよく使われる派生クラス。
    • エンコーディングを把握する必要がある。
  • 外部リソースを表す FileStream はファイルハンドルを取得したままにしちゃうと他のアプリケーションからファイルを操作できなくなるんで、ちゃんと Dispose しないとダメ。
  • クソややこしいことに Stream.Close というメソッドもありやがる。
    • .NET の初期パブリックベータリリースでは IDisposableusing ステートメントもなかった(using キーワード自体は違う用途で存在した)。まあ Python の with も後発やしわかるよ。
    • そのころにリソースを開放するために Close が実装されたけど、あとで Dispose が追加され、いまや Close は非推奨。
    • ほんまに歴史的な産物なのだな。
  • 改行を表す方法はいくつかあるが、 Windows では値13と10の2バイトを使用して表される。 Unix では13の1バイトで表される。
    • \r\n\n のことだ。
// TextReader の読み取りメソッド。
public virtual int Read(char[] buffer, int index, int count) {...}
public virtual int ReadBlock(char[] buffer, int index, int count) {...}
// あと ReadLine を使えば行単位で取得できる。

// StreamWriter でファイルにテキストを書き込む例。
using (var fw = new StreamWriter(@"C:\temp\out.txt"))
{
    fw.WriteLine("書き込み");
}

 

ファイルシステムについてはもっとある

  • FileStream クラスはファイルシステムにアクセスできるけど、次のクラスのほうが便利。
    • File クラス。これは静的クラス。ファイルを削除したり、移動したり、名前を変更したり、最終更新日を取得したりできる。
    • Directory クラス。ディレクトリ版。
    • Path クラス。ファイル名を含む文字列を操作する。ああ、名前でぴんときた。 Python の os.path にあたるものか。
    • FileInfoDirectoryInfoFileSystemInfo はファイルとかディレクトリをインスタンスとして表すから、ひとつのファイルから複数の情報を取得するみたいなときはこっちを使う。
var fi = FileInfo(@"C:\temp\log.txt");
Console.WriteLine("{0}, {1}バイト, 最終更新日 {2}",
    fi.FullName, fi.Length, fi.LastWriteTime);
  • デスクトップアプリの設定は AppData フォルダに置かれる。
  • システム全体向けの設定は C:\ProgramData フォルダに置かれる。
  • これらのフォルダを表す enum がある。 Environment.SpecialFolder だ。
  • あと、まっっったく使わないけど「ドキュメント」とか「ピクチャ」は Windows.Storage 名前空間の KnownFolders.DocumentsLibrary とか KnownFolders.PicturesLibrary にあるらしいよ。
// 設定を保存する場所を探し出す。
string appSettingsRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string myAppSettingsFolder = Path.Combine(appSettingsRoot, @"InteractSoftwareLtd\FrobnicatorPro");

 

データ長に関するうんちく

  • long Length プロパティのあるストリームもある。データ長っていうらしい。
    • 巨大なデータを書き込む前に SetLength メソッドで長さを決めておけば、データを格納するだけの空き容量があるかを事前にチェックできて、イイ。(わかりみ)
    • 以下、 SetLength の使用例 + IOException.HResult にかんするうんちくを含むコード。
const long gig = 1024 ^ 3;

// .NET ではディスクの空き容量不足に対して固有の例外がない。
// かわりに IOException に HResult がある。
//     これは例外の発生理由を表す COM エラーコードとやらが定義されてる。
//     ええやん、これで空き容量不足を判別したろ!
// そうは問屋がおろさなくて、 Windows にはディスクの空き容量がないことを表すエラーがふたつある。
//     そんならその両方で判別できるやん!
//     そのふたつは int で定義されてるから、
//     それぞれ DiskFullErrorCode と HandleDiskFullErrorCode ということにしてコーディングしたろ!
// とそうも簡単にはいかなくて、 COM エラーコードの数値は
// 最上位ビットがつねにセットされている(= C# の int の最大値を超えてやがる)からコンパイルエラーになる。
//     コンパイルエラーを防ぐため、 unchecked キーワードをつける。
const int DiskFullErrorCode = unchecked((int)0x80070070);
const int HandleDiskFullErrorCode = unchecked((int)0x80070027);

public static void Main(string[] args)
{
    // 見事に値が回り込んでますなあ。
    Console.WriteLine(DiskFullErrorCode);  // -2147024784
    Console.WriteLine(HandleDiskFullErrorCode);  // -2147024857

    try
    {
        using var fs = File.OpenWrite(@"C:\temp\long.txt");
        fs.SetLength(10000 * gig);
    }
    catch (IOException x)
    {
        if (x.HResult == DiskFullErrorCode || x.HResult == HandleDiskFullErrorCode)
        {
            Console.WriteLine("空き容量不足!");
        }
        else
        {
            Console.WriteLine(x);
        }
    }
}
  • バイト列や文字列だと要求を満たせない場合は .NET に用意されたシリアル化の機能を利用する……。
    • どんな場合やねん怖いわ。怖いからスキップしました。

 

小休憩

FileStream とか File あたりでなんか思い出してきた。以前に C# でアプリ作るに際してググっているときに思ったことなのだけど、ひとつのことをやる方法にバリエーションがありすぎ。 Python の禅のひとつ、 There should be one-- and preferably only one --obvious way to do it. (優れたたった一つの方法があるに違いない)に染まっているぼくにとって C# のそういう側面は拷問だ。この本を読んでるとき感じることなのだけど、「あれもある、これもある」とひたすら紹介してきてまるで退屈なテキストブックなんだ。『Effective Python』なんかには哲学とクールさがあった。