概要

これまで我々が見ていたものは、オブジェクトや変数として表された情報を操作するものだったが、本章は違う……。ディスク上のファイルや 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 の中には文字列を扱うようなものもある。助かるわぁ。
実際に使うのはどんなん
- バイト列だとタルい、文字列にしろ、ってときは
TextReaderとTextWriterを使う。要素がbyteではなくcharで表されているところが違う。 StringReader、StringWriterとMemoryStreamとよく似ていて、TextReader系と同じ感じで使えるけれど、すべてもメモリ上で処理できる。StreamReaderとStreamWriterがもっともよく使われる派生クラス。- エンコーディングを把握する必要がある。
- 外部リソースを表す
FileStreamはファイルハンドルを取得したままにしちゃうと他のアプリケーションからファイルを操作できなくなるんで、ちゃんとDisposeしないとダメ。- 7章でやった、 Python の
closeとかusingにあたるやつね。わかる。(『プログラミング C#』 その7 オブジェクトの生存期間 - Python の with にあたるハナシ)
- 7章でやった、 Python の
- クソややこしいことに
Stream.Closeというメソッドもありやがる。- .NET の初期パブリックベータリリースでは
IDisposableもusingステートメントもなかった(usingキーワード自体は違う用途で存在した)。まあ Python のwithも後発やしわかるよ。 - そのころにリソースを開放するために
Closeが実装されたけど、あとでDisposeが追加され、いまやCloseは非推奨。 - ほんまに歴史的な産物なのだな。
- .NET の初期パブリックベータリリースでは
- 改行を表す方法はいくつかあるが、 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にあたるものか。FileInfo、DirectoryInfo、FileSystemInfoはファイルとかディレクトリをインスタンスとして表すから、ひとつのファイルから複数の情報を取得するみたいなときはこっちを使う。
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』なんかには哲学とクールさがあった。