CodeGym /コース /C# SELF /入出力ストリーム: Stream

入出力ストリーム: Stream

C# SELF
レベル 36, レッスン 1
使用可能

1. はじめに

ちょっとケトル(やかん)を想像してみて。蛇口をひねると水が流れ出すよね。いっぱい水をためて一気に注ぐこともできるし、ちょっとずつケトルに入れることもできる。ファイルも同じで、全部を一気にメモリに読み込むのが便利な時もあれば、そうじゃない時もある。ファイルがめっちゃ大きいこともあるし、データソースがファイルじゃなくてネットワーク接続みたいに、データが少しずつ届く場合もあるんだ。

もし常にバイト配列だけでやり取りしようとしたら、大きなファイルだとすぐメモリが足りなくなるし、「無限」データストリーム(たとえば動画や音声ストリーム)だとこのやり方は全然ダメ。そこでストリームのコンセプトが役立つってわけ!

.NETのストリームは、データに順番にアクセスするための抽象化されたもの。データソースがファイルでもネットワークでもメモリでも、あるいは圧縮アーカイブみたいなちょっと変わったものでもOK。ストリームを使えば、データを部分的に(たいていはブロックやバイト単位で)読み書きできるんだ。

基本アイデア:

  • ストリームはデータをやり取りするためのチャンネルみたいなもの。コンベアみたいに「置く」(書き込む)か「取る」(読む)だけで、中身がどこにどう保存されてるかは気にしなくていい。
  • データは順番にやってくる:前の部分を読んでからじゃないと次の部分は読めない(逆もできる場合はシーク対応)。
  • たいていの場合、全部のデータを一気にメモリに持たない(パソコンもそれを喜ぶよ)。

この抽象化は.NETのほぼすべての入出力操作の基礎になってる:ファイル操作、ネットワーク、アーカイブ、コンソール操作まで!

2. ストリーム System.IO.Stream

継承とアーキテクチャ: System.IO.Stream

.NETのほぼすべてのストリームは抽象クラスSystem.IO.Streamを継承してる。これが、読み書きやシーク、ストリームの管理のための基本メソッドを定義してるんだ。


classDiagram
    class Stream {
        +Read()
        +Write()
        +Seek()
        +CanRead
        +CanWrite
        +CanSeek
        +Length
        +Position
    }
    class FileStream
    class MemoryStream
    class NetworkStream
    class CryptoStream
    Stream <|-- FileStream
    Stream <|-- MemoryStream
    Stream <|-- NetworkStream
    Stream <|-- CryptoStream
.NETにおけるストリームの継承図
  • Stream — 基本の抽象クラス
  • FileStream — ファイル操作用
  • MemoryStream — メモリ上のデータ操作用
  • NetworkStream — ネットワーク通信用
  • CryptoStream — 暗号化/復号化用

ストリームの主なプロパティとメソッドざっくり紹介

プロパティ / メソッド 説明
CanRead
このストリームから読み込みできるかどうか
CanWrite
このストリームに書き込みできるかどうか
CanSeek
ストリーム内で位置移動できるか(全部が対応してるわけじゃない)
Length
ストリームの長さ(対応してる場合のみ)
Position
ストリーム内の現在位置
Read(...)
データの読み込み
Write(...)
データの書き込み
Seek(...)
ストリーム内の位置移動
Flush()
バッファのフラッシュ(たまったデータを一気にストリームへ書き出す)
Close()
/
Dispose()
ストリームを閉じてリソースを解放

じゃあ、実際にどう使うのか見てみよう。

3. 例: Streamでファイルを読み書きしてみる

ストリームが「実際どう動くか」っていう最小限の例を紹介するね:


// 書き込み用にファイルを開く
using var stream = new FileStream("numbers.bin", FileMode.Create);

// 1から10までの数字をファイルに書き込むとする
for (int i = 1; i <= 10; i++)
{
    byte val = (byte)i;
    stream.WriteByte(val); // 1バイトずつ書き込む
}

// 明示的にファイルを閉じて、今度は読み込み用に開く
stream.Close(); 

// じゃあ今度はその数字を読み込んでみよう
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
    Console.WriteLine(value); // 1, 2, ... 10 が出力される
}

ここではFileStreamを使ってるけど、これはまさに「ストリーム」そのもの。データをブロックやバイト単位で読み書きできるよ。

ストリームの種類:どこで使われてる?

ストリームって、別にディスク上のファイルだけじゃない。こんな例があるよ:

  • ディスク上のファイル(たとえばFileStream — 一番よくあるやつ)
  • メモリ上のストリーム(MemoryStream — 一時的なデータや中間データに便利)
  • ネットワーク接続(NetworkStream)
  • 圧縮/アーカイブ(GZipStream, DeflateStream)
  • 暗号化(CryptoStream)
  • コンソール入出力(そう、これも!)— 技術的にはこれもストリーム

これのおかげで、データの出どころや行き先を気にせずコードを書ける。ストリームでやり取りできれば、どこでも動く汎用的なコードになる!

4. ちょっとしたコツ

読み書きは「部分的に」データをやり取りする操作。たいていはバイト配列とRead、Writeメソッドを使うよ。

例:ファイルをブロックごとに読む

byte[] buffer = new byte[1024]; // 1024バイト(1KB)のバッファ
using var stream = new FileStream("bigfile.bin", FileMode.Open);

int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
    // bufferの中のbytesReadバイトだけ処理する
    int sum = 0;
    for (int i = 0; i < bytesRead; i++)
        sum += buffer[i];

    Console.WriteLine($"ブロックの合計: {sum}");
}

このやり方はウイルス対策ソフトから音楽プレイヤーまで、いろんなところで使われてる。

ストリーム内の位置指定(Position, Seek)

多くのストリーム(たとえばファイル系)では、データの「次」だけじゃなくて、好きな位置にジャンプしてそこから読み書きできる。

using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // 6バイト目に移動(0から数える)
int value = stream.ReadByte();
Console.WriteLine($"ファイルの6バイト目: {value}");

ストリームには「読み込み専用」「書き込み専用」「両方対応」がある

ストリームによっては、どっちかしかできない場合もある:

  • 書き込み専用で開いたファイル:Write()だけ
  • ネットワークデータの読み込み用ストリーム:Read()だけ
  • ちょっと変わったケース(たとえばプリンタ出力用ストリーム)だと「巻き戻し」や位置移動(シーク)ができないこともある。

どんな操作ができるかは、CanRead、CanWrite、CanSeekプロパティでチェックしよう:

using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
    Console.WriteLine("読み込み対応");
if (stream.CanWrite)
    Console.WriteLine("書き込み対応");
if (stream.CanSeek)
    Console.WriteLine("ファイル内の位置移動OK");

ストリームのバッファリング

ほとんどのストリームはパフォーマンス向上のために内部バッファを使ってる。バッファリングでディスクやネットワークへのアクセス回数を減らせる:データを内部でためてからまとめてやり取りする感じ。

Flush()メソッドでバッファをフラッシュできる(たとえば、絶対にディスクに書き込みたいときとか):

using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hello, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // これで確実にディスクに書き込まれる

もし大事なデータ(たとえば決済トランザクション!)を書き込むなら、Flush()は必須だよ。

5. ストリーム操作でありがちなミス

初心者がよくやるミスはこんな感じ:

ストリームを閉じ忘れる(メモリリークや「ロックされた」ファイルなど、いろいろ面倒なことになる)。

テキストストリームとバイナリストリームを混同して、バイトメソッドで文字列を書き込んで「文字化け」する。

バッファが小さすぎる(またはバッファなし)で、処理がめっちゃ遅くなる。

Read()が必ず指定したバイト数だけ読むと思い込む—実はそれより少ない場合もあるので、戻り値は必ずチェックしよう。

全部のストリームが位置移動(Seek)に対応してると思い込む(特にネットワーク系は非対応が多い)。

たとえばこんな感じ:


// ダメな例:実際に読んだバイト数をチェックせずに全部読むつもりでいる
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
    int bytesRead = stream.Read(buffer, 0, 1024);
    // ファイルが小さい場合、bytesReadは1024より小さいかも!
}
2
タスク
C# SELF, レベル 36, レッスン 1
ロック未解除
ストリームのプロパティのチェック
ストリームのプロパティのチェック
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION