1. 类 FileStream:分块操作
你可以把FileStream想象成一根直接连到你硬盘文件的水管。通过这根管子你可以有节奏地传输数据:往文件里写字节(写入)或者从文件里读字节(读取)。和那些高级方法只给你“一杯水”不同,FileStream直接让你拧水龙头,想放多少水(字节)自己说了算。
FileStream是字节级别的。这意思就是它根本不关心你是文本还是图片;对它来说,一切都是字节流。如果你用FileStream操作文本文件,你得自己把字符串转成字节(用编码,比如UTF-8)写进去,读出来时再转回字符串。
什么时候一定要用FileStream?
- 处理超大文件:当文件大到根本没法或者没必要一次性读进内存(比如几G的日志、视频文件)。FileStream可以分块读写数据,内存用得更高效。
- 二进制数据:如果你搞的不是普通文本(比如图片、音频、视频、序列化对象、数据库文件),FileStream就是你的主力,因为它能直接操作字节。
- 精细控制访问模式:你想同时读*和*写?或者想让别的程序根本打不开这个文件?还是想让多个进程一起读?FileStream都能让你完全掌控这些模式。
- 异步操作:在现代高性能应用(比如web服务器)里,文件操作不能卡主主线程很重要。FileStream支持异步方法(ReadAsync、WriteAsync),让你的程序一直很灵活。
- 部分读写或随机访问:你想从文件某个位置(比如500字节处)读数据,或者往文件中间写?FileStream可以让你随意移动文件指针。
新手常犯的错:用FileStream去做最简单的事,比如往小文件里写一行文本。这种场景用它就太重了。FileStream适合更复杂、更特殊的需求。
2. 创建和打开FileStream
想用FileStream操作文件,得先new一个实例。构造函数有几个重要参数,决定你怎么和文件打交道。
using System;
using System.IO; // 用FileStream必须加这个
using System.Text; // 处理编码(字符串和字节互转)
用FileStream基础读取文本文件的例子:
先创建个文件,方便后面读取。
// 创建一个简单的文本文件做演示
string path = "example_filestream.txt";
// 打开流准备读取
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
// 创建缓冲区(字节数组)临时存读取到的数据
byte[] buffer = new byte[fs.Length];
// 从流里读字节到缓冲区
int bytesRead = fs.Read(buffer, 0, buffer.Length);
// 用指定编码(比如UTF-8)把字节转回字符串
string content = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("通过FileStream从文件读取到: " + content);
fs.Close(); //关闭流
主要参数
- FileMode:怎么打开文件(
Open、Create、Append、OpenOrCreate等) - FileAccess:对文件做什么(
Read、Write、ReadWrite) - FileShare:别的进程能不能同时用这个文件(一般简单场景用不到)
常见错误:如果你忘了关闭FileStream,文件可能会“被锁住”——你之后删不了也打不开!
3. FileStream构造函数参数
FileStream的构造函数可以很细致地设置你想怎么打开文件。主要参数如下:
new FileStream(
string path, // 文件路径(绝对或相对)
FileMode mode, // 怎么打开文件(创建、打开、覆盖等)
FileAccess access, // 允许什么操作(只读、只写、读写)
FileShare share // 别的进程怎么访问这个文件(可选)
);
下面来看看FileMode和FileAccess枚举的取值:
FileMode(文件打开模式):
决定操作系统打开文件时怎么处理。
FileMode.Open:打开已存在的文件。如果找不到文件,会抛出FileNotFoundException。FileMode.Create:创建新文件。如果同名文件已存在,会完全覆盖(内容全没了)。
FileAccess(文件访问权限):
决定你程序对打开的文件能做什么。
FileAccess.Read:文件只可读,不能写。FileAccess.Write:文件只可写,不能读。FileAccess.ReadWrite:文件可读可写。最灵活,但你得自己管理流的位置。
FileShare(共享访问):
决定别的进程在你程序打开文件时能不能也打开。很重要,能防止锁死。
FileShare.Read:别的进程可以读文件,但不能写。FileShare.Write:别的进程可以写文件,但不能读。FileShare.ReadWrite:别的进程可以读写文件。最开放,但如果多个程序同时改文件可能会冲突。
更多模式细节下节课讲。
4. 用FileStream读写数据
用FileStream时,你操作的都是字节。这意味着处理文本时要用编码类(比如System.Text.Encoding.UTF8)把字符串和字节数组互转。
用FileStream写数据到文件
写数据时,你先把数据(比如字符串)转成字节数组,再写进流。
string outputPath = "user_data.txt";
string userName = "伊万 彼得罗夫";
// 用UTF-8编码把字符串转成字节数组
byte[] userNameBytes = Encoding.UTF8.GetBytes(userName);
// 打开FileStream准备写入
FileStream fsWrite = new FileStream(outputPath, FileMode.Create, FileAccess.Write);
// 把字节数组写进流
fsWrite.Write(userNameBytes, 0, userNameBytes.Length);
Console.WriteLine($"名字 '{userName}' 已成功写入文件 '{outputPath}'。");
fsWrite.Close(); //关闭流
用FileStream从文件读数据
读数据时,你把字节读到缓冲区,再转成你想要的格式(比如字符串)。
string inputPath = "user_data.txt";
// 打开FileStream准备读取
FileStream fsRead = new FileStream(inputPath, FileMode.Open, FileAccess.Read);
// 创建和文件一样大的缓冲区
byte[] buffer = new byte[fsRead.Length];
// 从流里读字节到缓冲区
int bytesRead = fsRead.Read(buffer, 0, buffer.Length);
// 把读到的字节转成字符串
string loadedUserName = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"通过FileStream从文件读取到名字: {loadedUserName}");
fsRead.Close(); //关闭流
5. 操作大文件
FileStream比File.ReadAll...系列方法最大的优势就是能分块读写,这对内存装不下的大文件特别重要。
分块读取大文件
分块读文件时,你用固定大小的缓冲区(字节数组),每次读一块,直到文件结尾。
string bigFilePath = "bigfile.bin"; //大文件
int bufferSize = 4096; // 缓冲区大小,比如4KB
byte[] buffer = new byte[bufferSize]; // 创建缓冲区
int bytesRead; // 实际读到的字节数
long totalBytesRead = 0; // 总共读了多少字节
// 打开流
FileStream fs = new FileStream(bigFilePath, FileMode.Open, FileAccess.Read);
int chunkNumber = 1;
// 只要fs.Read()返回正数就继续循环
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
totalBytesRead += bytesRead;
Console.WriteLine($"第{chunkNumber++}块: 读了{bytesRead}字节。总共: {totalBytesRead}字节。");
// !!! 这里可以处理每一块读到的数据 !!!
}
fs.Close(); //关闭流
重要提示:fs.Read(buffer, offset, count)不保证会把buffer填满。它返回实际读到的字节数,可能比count小(尤其到文件结尾时)。一定要用bytesRead的返回值来正确处理数据。
分块写入大数据
和读一样,你也可以分块把大量数据写进文件。
string bigOutputPath = "big_output.txt";
string longText = new string('A', 1_000_000); // 一百万个'A'的字符串
byte[] longTextBytes = Encoding.UTF8.GetBytes(longText);
int writeBufferSize = 1024; // 每次写1KB
// FileMode.Create: 创建文件
FileStream fsWrite = new FileStream(bigOutputPath, FileMode.Create, FileAccess.Write);
for (int i = 0; i < longTextBytes.Length; i += writeBufferSize)
{
// 计算本次要写多少字节
int bytesToWrite = Math.Min(writeBufferSize, longTextBytes.Length - i);
// 从longTextBytes的i位置开始写bytesToWrite个字节
fsWrite.Write(longTextBytes, i, bytesToWrite);
Console.WriteLine($"写入了{bytesToWrite}字节");
}
fsWrite.Close(); //关闭流
6. 不容易发现的坑和常见错误
就算用FileStream这么强的工具,也有不少细节要注意:
缓冲和Flush():前面说过,数据可能会先留在内存缓冲区,没马上写到硬盘。如果你不用using,也没调用Close()/Dispose(),直接让程序结束,数据可能会丢。一定要调用Flush()(或者用using/Close())保证数据写到磁盘。
异常处理:输入/输出错误(比如磁盘满时的IOException,权限不够时的UnauthorizedAccessException,用Open模式打开不存在的文件时的FileNotFoundException)很常见。所有FileStream操作都要用try-catch包起来。
编码:用FileStream处理文本数据(它只认字节),你得自己选对编码(Encoding.UTF8、Encoding.ASCII、Encoding.Unicode等)把字符串和字节互转。选错编码就会出现乱码。
流位置管理:用Seek()时,参数offset和origin要小心,别跳到文件外面或者奇怪的位置。
文件锁(FileShare):如果你用FileShare.None打开文件,别的进程就打不开它。如果别的程序(甚至你自己程序的另一个线程)想用同一个文件就会出问题。一定要选合适的FileShare模式。
GO TO FULL VERSION