C#文件和流
本文主要是对C#中的流进行详细讲解,关于C#中的文件操作,考虑到后期.net core跨平台,相关操作可能会发生很大变化,所以此处不对文件系统(包括目录、文件)过多的讲解,只会描述出在.net framework下常用的类,具体用法请参见官方API文档。
管理文件系统
在Windows上,用于浏览文件系统和执行操作的相关类有:
-
FileSystemInfo
:这是表示任何文件系统对象的基类。 -
FileInfo
和File
:这些类表示文件系统上的文件。 -
DirectoryInfo
和Directory
:这些类表示文件系统上的目录。 -
Path
:这个类包含的静态成员可以用于处理路径名。 -
DriveInfo
:这个类的属性和方法提供了指定驱动器的信息。
Directory
类和File
类只包含静态方法,不能被实例化。如果只对文件夹或文件执行一个操作,使用这个类就很有效,因为这样可以省去创建.NET对象的系统开销。
DirectoryInfo
类和FileInfo
类的成员都不是静态的,使用时需要被实例化。如果使用同一个对象执行多个操作,使用这些类就比较有效。【这是因为在构造它们将读取合适文件系统对象的身份验证和其他信息,无论对每个对象(类实例)调用多少方法,都不需要再次读取这些信息】
检查驱动器信息
有时在处理文件和目录之前,需要检查驱动器信息,可以使用DriveInfo
类实现。
DriveInfo
类可以扫描系统,提供可用驱动器的列表,还可以进一步提供任何驱动器的大量细节信息。
关于DriveInfo
的用法,请参考官方API说明:https://docs.microsoft.com/zh-cn/dotnet/api/system.io.driveinfo
使用Path类处理文件和目录的路径
如果只是单纯的使用字符串连接操作符合并多个文件夹和文件时,很容易遗漏单个分隔符或使用太多的字符。可以使用Path
类代替字符串拼接路径,Path
类会添加缺少的分隔符,而且可以基于Windows和Unix系统,处理不同的平台需求。
Path
类可以使用以下几个属性处理Windows和Unix平台的路径特殊符号:
-
Path.VolumeSeparatorChar
:提供特定于平台的卷分隔符。 此字段的值在Windows和Macintosh上为冒号(:
),在UNIX操作系统上为斜杠(/
)。这对于解析诸如“c:\ windows
”或“MacVolume:System Folder
”之类的路径非常有用。 -
Path.DirectorySeparatorChar
:提供特定于平台的字符,用于分隔反映分层文件系统组织的路径字符串中的目录级别 ,该属性的值为左斜杠(\
)。 -
Path.AltDirectorySeparatorChar
:提供特定于平台的备用字符,用于分隔反映分层文件系统组织的路径字符串中的目录级别 。此字段可以设置为与DirectorySeparatorChar
相同的值。AltDirectorySeparatorChar
和DirectorySeparatorChar
都可用于分隔路径字符串中的目录级别。 Windows、UNIX和Macintosh上该字段的值是斜杠(/
)。 -
Path.PathSeparator
:特定于平台的分隔符,用于分隔环境变量中的路径字符串。 在基于Windows的桌面平台上,默认情况下,此字段的值为分号(;
),但在其他平台上可能会有所不同。
除此之外,Path
类还有如下几个非常适用的方法:
-
Path.GetInvalidPathChars()
:获取包含不允许在路径名中使用的字符的数组。 -
Path.GetInvalidFileNameChars()
:获取包含不允许在文件名中使用的字符的数组。 -
Path.GetTempPath()
:返回当前用户的临时文件夹的路径。 -
Path.GetTempFileName()
:在磁盘上创建磁唯一命名的零字节的临时文件并返回该文件的完整路径。 -
Path.GetRandomFileName()
:返回随机文件夹名或文件名。 -
Path.Combine()
:合并路径。 -
Path.ChangeExtension()
:更改路径字符串的扩展名。
使用Environment类处理特殊文件夹
Environment
类定义了一组特殊的文件夹。注意:该类不能用于.net core中。
Environment
的SpecialFolder
属性是一个巨大的枚举,通过该属性,可以得到window系统上的音乐、图片、程序文件、应用程序数据、以及许多其他文件夹的路径值。
除此之外,还可以调用Environment.GetEnvironmentVariable()
方法,根据指定的环境变量名得到该变量的具体值。
关于Environment
的具体使用,请参见官方API说明:https://docs.microsoft.com/zh-cn/dotnet/api/system.environment
使用File、FileInfo和Directory、DirectoryInfo类处理文件和文件夹
File
、FileInfo
、Directory
、DirectoryInfo
的详细操作,可直接参见官方API文档。这里只对其中需要注意的地方进行叙述。
File
类提供了简单创建文件并写入和读取文本的操作方法,如File.WriteAllText()
、File.ReadAllText()
方法,但是需要注意的是,使用File
在字符串中读写文件只适用于小型文本文件。并且,以这种方式读取、保存完整的文件是有限制的。.NET字符串的限制是2GB,虽然对于许多文本文件而言,这已经足够了,但是最好不要让用户等待将1GB的文件加载到字符串中,这将非常耗时和耗资源,而应该使用流进行读写操作。
使用流处理文件
流是一个用于传输数据的对象,分为读取流和写入流。传输数据可以基于文件、内存、网络或其他任意数据源。.NET中提供了一下几种用来操作的流:
-
MemoryStream
:创建其后备存储为内存的流。 用来读写内存。 -
System.Net.Sockets.NetworkStream
:为网络访问提供基础数据流。 用来处理网络数据。 -
FileStream
:创建用来处理文件的流,支持同步和异步。这个类主要用于在二进制文件中读写二进制数据。 -
System.IO.Compression.DeflateStream
:提供使用Deflate
算法压缩和解压缩流的方法和属性。 -
System.Security.Cryptography.CryptoStream
: 定义将数据流链接到加密转换的流。
上述的这些类都派生自基类Stream
,多个流之间可以相互链接(转换),相互写入。
对于文件的读写,最常用的类如下:
-
FileStream
:文件流,这个类主要用于在二进制文件中读写二进制数据。 -
StreamReader
和StreamWriter
:这两个类专门用于读写文本格式的流产品API。可以通过它们的基类看出主要针对的是文本格式的文件。 -
BinaryReader
和BinaryWriter
:这两个类专门用于读写二进制格式的流产品API。
各种流如何选择
在不使用其他流的情况下,单纯的使用FileStream
既可以处理文本格式的文件也可以处理二进制数据文件,它是基于字节来读取或写入数据的。对于文本文件,可能需要先分析文本文件的编码,以便能够正确读取和写入不同编码格式的文本。
如果内容是文本格式的文件,推荐使用StreamReader
和SreamWriter
进行读写操作,不需要考虑编码格式问题,默认采用UTF-8
编码(可在构造函数中自定义编码格式)。
如果内容是二进制格式的文件,推荐使用BinaryReader和BinaryWriter进行读写操作,将以二进制格式而不是文本格式写入文件,并且不需要使用任何编码。
使用文件流FileStream
可以使用FileStream
的构造函数来创建FileStream
对象,除了这种方式外,还可以直接使用File
类的OpenRead()
方法创建FileStream
,OpenRead()
方法打开一个文件(类似于FileMode.Open
),返回一个可以读取的流(FileAccess.Read
),也允许其他进程执行读取访问(FileShare.Read
)。对应的写入流可以使用File.OpenWrite()
方法得到。
无论是读取流还是写入流,它们都是FileStream
对象,只是对应的FileAccess
代表的操作不同,建议在给流对象命名时,最好有是读取还是写入的标识名。
获取流相关信息
在下面的示例中,分别获取Stream
类的成员属性,得到流处理相关信息。
private void ShowStreamInfomation(FileStream stream)
{
Console.WriteLine("当前流是否可读取:" + stream.CanRead);
Console.WriteLine("当前流是否可写入:" + stream.CanWrite);
Console.WriteLine("当前流是否支持搜索:" + stream.CanSeek);
Console.WriteLine("当前流是否可以超时:" + stream.CanTimeout);
Console.WriteLine("当前流长度:" + stream.Length);
Console.WriteLine("当前流的位置:" + stream.Position);
//如果可以超时
if (stream.CanTimeout)
{
Console.WriteLine("流在尝试读取多少毫秒后超时:" + stream.ReadTimeout);
Console.WriteLine("流在尝试写入多少毫秒后超时:" + stream.WriteTimeout);
}
}
分析文本文件的编码
对于文本文件,首先是读取流中的第一个字节——序言。序言提供了文件如何编码的信息(使用的文本编码格式),这也称为字节顺序标记(Byte Order Mark,BOM
)。可以使用如下方法,获取BOM
:
private Encoding GetEncoding(FileStream stream)
{
//如果当前流不支持检索就抛出异常
if (!stream.CanSeek) throw new ArgumentException("require a stream that can seek");
Encoding encoding = Encoding.ASCII;
//定义缓冲区,这里只是为了将BOM格式写入到该字节数组中
byte[] bom = new byte[5];
//从流中读取字节块,填充bom字节数组的同时,返回读入缓冲区的总字节数
//注意流可能小于缓冲区。如果没有更多的字符可用于读取,Read()方法就返回0,此时没有数据写入到缓冲区
int nRead = stream.Read(bom, offset: 0, count: 5);
if (bom[0] == 0xff && bom[1] == 0xfe && bom[2] == 0 && bom[3] == 0)
{
Console.WriteLine("UTF-32");
//将该流的当前位置设置为给定值,从流的开始位置起
stream.Seek(4, SeekOrigin.Begin);
return Encoding.UTF32;
}
else if (bom[0] == 0xff && bom[1] == 0xfe)
{
Console.WriteLine("UTF-16, little endian");
stream.Seek(2, SeekOrigin.Begin);
return Encoding.Unicode;
}
else if (bom[0] == 0xfe && bom[1] == 0xff)
{
Console.WriteLine("UTF-16,big endian");
stream.Seek(2,