概要
これまで我々が見ていたものは、オブジェクトや変数として表された情報を操作するものだったが、本章は違う……。ディスク上のファイルや 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』なんかには哲学とクールさがあった。