CodeGym /课程 /C# SELF /输入输出流: Stream

输入输出流: Stream

C# SELF
第 36 级, 课程 1
可用

1. 入门

想象一下烧水壶。你打开水龙头——水就流出来了。你可以一次接满一壶水,也可以慢慢一点点加。文件也是一样——有时候没法或者不方便一下子把整个文件都读进内存。文件可能很大,有时候数据源甚至不是文件,比如网络连接,数据是慢慢流进来的。

如果我们总是直接用字节数组来处理数据,遇到大文件内存很快就爆了,而且对于“无限”数据流(比如视频或音频流)这种方式根本行不通。这时候,流的概念就派上用场啦!

在.NET里,流就是一种顺序访问数据的抽象:不管背后是文件、网络、内存,还是啥奇葩玩意儿比如压缩包。流让你可以分块(通常是按块或按字节)读写数据。

核心思想:

  • 流就是数据传输的通道。它像传送带一样:你可以“放上去”(写入)或者“拿下来”(读取)数据,根本不用管数据到底存哪、怎么存。
  • 数据是顺序来的:你只能在读完上一个块后才能读下一个(当然有的流支持倒带)。
  • 大多数情况下你不会把所有数据一次性全放进内存(电脑会感谢你的)。

这个抽象几乎是.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); // 一次写一个字节
}

// 显式关闭文件,为了后面能读
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]; // 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("可以定位文件");

流的缓冲

几乎所有流内部都有缓冲区来提升性能。缓冲能减少磁盘/网络的访问次数:数据先在内部攒着,然后一次性读/写。

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