C# 流和文件

流(Stream)是对数据读写的一种抽象,在C#中,使用System.IO.Stream类表示流的基类。

流包含读、写、定位功能。不是所有的流都支持所有功能,比如网络流不支持定位,只读文件流不支持写。

流的成员

  [ComVisible(true)]
    public abstract class Stream : MarshalByRefObject, IDisposable
    {
        //无后备存储区的 Stream。
        public static readonly Stream Null;

        //     初始化 System.IO.Stream 类的新实例。
        protected Stream();

        //获取或设置当前流中的位置。并不是所有的流都支持设置位置,一般要支持定位才支持。
        public abstract Int64 Position { get; set; }

        //获取流长度。并不是所有的流都支持获取长度,一般要支持定位才支持。
        public abstract Int64 Length { get; }

        //指示流是否支持写入。不支持写入的流不能调用写相关方法。
        public abstract Boolean CanWrite { get; }

        //当前流是否可以超时。支持超时的流可以设置超时时间。
        [ComVisible(false)]
        public virtual Boolean CanTimeout { get; }

        //当前流是否支持定位。
        public abstract Boolean CanSeek { get; }

        //当前流是否支持读取。不支持读取的流不能调用读相关方法。
        public abstract Boolean CanRead { get; }

        //获取或设置一个读超时时间(毫秒)。
        [ComVisible(false)]
        public virtual Int32 ReadTimeout { get; set; }

        //获取或设置一个写超时时间(毫秒)。
        [ComVisible(false)]
        public virtual Int32 WriteTimeout { get; set; }

        //在指定的 System.IO.Stream 对象周围创建线程安全(同步)包装。
        public static Stream Synchronized(Stream stream);

        //     开始异步读操作。
        public virtual IAsyncResult BeginRead(Byte[] buffer, Int32 offset, Int32 count, AsyncCallback callback, Object state);
        //

        //开始异步写操作。
        public virtual IAsyncResult BeginWrite(Byte[] buffer, Int32 offset, Int32 count, AsyncCallback callback, Object state);

        //关闭当前流并释放与之关联的所有资源(如套接字和文件句柄)。
        public virtual void Close();

        //从当前流中读取字节并将其写入到另一流中。
        public void CopyTo(Stream destination);

        //使用指定的缓冲区大小,从当前流中读取字节并将其写入到另一流中。
        public void CopyTo(Stream destination, Int32 bufferSize);

        //释放由 System.IO.Stream 使用的所有资源。
        public void Dispose();

        //等待挂起的异步读取完成。
        public virtual Int32 EndRead(IAsyncResult asyncResult);

        //结束异步写操作。
        public virtual void EndWrite(IAsyncResult asyncResult);

        //将清除该流的所有缓冲区,并使得所有缓冲数据被写入到基础设备。
        public abstract void Flush();

        //从当前流读取字节序列,并将此流中的位置提升读取的字节数。
        public abstract Int32 Read(Byte[] buffer, Int32 offset, Int32 count);

        //从流中读取一个字节,并将流内的位置向前提升一个字节,或者如果已到达流结尾,则返回 -1。
        public virtual Int32 ReadByte();

        //设置当前流中的位置。
        public abstract Int64 Seek(Int64 offset, SeekOrigin origin);

        //设置当前流的长度。
        public abstract void SetLength(Int64 value);

        //当在派生类中重写时,向当前流中写入字节序列,并将此流中的当前位置提升写入的字节数。
        public abstract void Write(Byte[] buffer, Int32 offset, Int32 count);

        //将一个字节写入流内的当前位置,并将流内的位置向前提升一个字节。
        public virtual void WriteByte(Byte value);
    }

流的基本方法是读、写数据,读写前需要通过对于的属性检查一下是否支持。

某些流支持定位,可以通过重写定位重复读写数据,例如文件流。

有些流支持设置长度,例如文件流。

流实现了IDisposable接口,在使用时一般使用using语句保证释放资源。

内存流

内存流(MemoryStream)是将字节数组作为流来使用。

内存流支持读写和定位。

当使用一个数组作为初始化的时候,内存流是大小固定的;当使用无参构造函数时,内存流是动态增长的。

static void Main(string[] args){
    var ms = new MemoryStream();
    var bytes = new byte[] { 1, 2, 3 };
    ms.Write(bytes, 0, bytes.Length);//3
    Console.WriteLine(ms.Length);

    //重设位置,以便后续读取
    ms.Position = 0;

    var bytes2 = new byte[3];

    int n = ms.Read(bytes2, 0, bytes2.Length);//3

    //将内存流的数据转换成数组
    var bytes3 = ms.ToArray();
}

在使用支持定位的流时,要记得位置。

流读写器

直接通过流来读写数据,都需要通过字节数组来操作,这不是很方便,通过流读写器,可以更方便的读写流中的数据。

流读写器分为两种,一种是以字节的方式读写,一种是以字符的方式读写。

字节流读写器

BinaryReaderBinaryWriter以二进制(字节)的方式读写流。

BinaryReader包含多个ReadXX方法,用来以字节的方式读取对应类型数据。

BinaryWriter包含过个Write方法重载,用来以字节的方式写入数据。

var ms = new MemoryStream();

BinaryWriter writer = new BinaryWriter(ms);

writer.Write(100);//写入 0x64 0x00 0x00 0x00
writer.Write(true);// 0x01
writer.Flush();

//重设位置以便读写
ms.Position = 0;

BinaryReader reader = new BinaryReader(ms);

int value = reader.ReadInt32();//100
bool flag = reader.ReadBoolean();//true

在使用写入器写入的时候,需要注意在最后调用Flush方法,确保数据被刷新到基础流。

读和写需要一一对应。

字符流读写器

StreamReaderStreamWriter以字符的方式读写流。

它们的基类分别是TextReaderTextWriter

StreamReader包含ReadLine方法,用来以字符的方式读取一行字符。

StreamWriter包含过个WriteWriteLine方法重载,用来以字符的方式写入数据。

在使用字符流读写器的时候,需要提供字符编码,这个编码默认是UTF8。

public static void TextRW()
{
    var ms = new MemoryStream();

    var writer = new StreamWriter(ms);

    writer.WriteLine(100);
    writer.WriteLine(true);
    writer.Flush();

    ms.Position = 0;

    var reader = new StreamReader(ms);

    string value = reader.ReadLine();//"100"
    string flag = reader.ReadLine();//"True"

}

字符串读写器

字符串读写器StringReader,StringWriter同样继承自TextReaderTextWriter,和字符流读写器拥有相似的方法。

可以像读写字符流一样读取字符串。

public static void StringRw()
{
    StringBuilder b = new StringBuilder();

    StringWriter writer = new StringWriter(b);

    writer.WriteLine(100);
    writer.WriteLine(true);

    StringReader reader = new StringReader(b.ToString());

    string value = reader.ReadLine();
    string flag = reader.ReadLine();
}

字符流和字节流

使用字符流处理文本文件,字节流处理二进制文件。

使用字节流更节约空间,字符流便于人查看。定义自己的文件类型时按照需要选择。

读写器和资源所有权

通常,在通过流构造读写器时,流的所有权给了读写器,这时,只需要通过using释放读写器即可,无需释放流自身。

如果要使用多个读写器操作流,那么不要通过读写器来释放流,通过流自身来释放,并注意所有的写入器都要调用Flush确保数据刷新到流。

控制台和流

控制台也通过流来操作的。

控制台的输入流是键盘,输出流和错误流是屏幕,对应Console.In,Console.OutConsole.Err

Console.InTextReader

Console.OutConsole.ErrTextWriter

Console.WriteLine就是Console.Out.WriteLine的快捷写法。

Console.ReadLine就是Console.In.ReadLine的快捷写法。

文件流

使用文件流(FileStream)可以进行对文件读写。 文件流支持读、写、定位和设置长度。

写入数据并读取。

   public static void Fs()
        {
            using (FileStream fs = new FileStream(@"test.txt",
                  FileMode.OpenOrCreate,
                  FileAccess.ReadWrite))
            {
                var writer = new StreamWriter(fs);

                writer.WriteLine("hello world!");
                writer.Flush();

                fs.Position = 0;

                var reader = new StreamReader(fs);

                string s = reader.ReadLine();

            }
        }

打开方式

    public enum FileMode
    {
        //指定操作系统应创建一个新的文件。 这要求文件不存在。
        CreateNew = 1,
        //指定操作系统应创建一个新的文件。 如果该文件已存在,则会覆盖它。
        Create = 2,
        //指定操作系统应打开现有文件。 需要文件存在并且有权限。
        Open = 3,
        //指定操作系统应打开一个文件,不存在则创建一个新的文件。
        OpenOrCreate = 4,
        //指定操作系统应打开现有文件。 当打开文件时,应被截断成零字节。
        Truncate = 5,
        //如果它存在,定位到该文件的末尾,否则创建一个新文件,请打开该文件。
        Append = 6
    }

文件访问权限

    [Flags]
    public enum FileAccess
    {
        //对文件的读取访问权限。
        Read = 1,
        //对文件的写入访问权限。
        Write = 2,
        //读取和写入到文件的访问。
        ReadWrite = 3
    }

异步读写文件

Stream类中提供了读写方法对于的异步方式,但这些异步方法默认实现是使用线程来调用对于的同步方法。

在构建FileStream时,设置isAsynctrue,然后调用其异步方法,这样在在文件读写的中间不需要额外的线程参与。一般用于读写大文件。

文件、文件夹、驱动器和路径

文件

通过File类的静态方法,可以快速打开,创建,删除文件。

ReadAllTextWriterAllText以文本的方式读取写入整个文件。 ReadAllBytesWriterAllBytes以字节的方式读取写入整个文件。 Open,OpenRead,OpenWrite快速打开文件。 Exists判断文件是否存在。 Move移动或重命名文件。 Delete删除文件。

还有相关的读写文本属性,访问时间,创建时间等方法。

FileInfo类用来操作特定的一个文件,需要通过路径来实例化,后续操作都是针对这个文件。

需要注意的是,FileInfo类型的属性不是实时的,如果需要最新状态,需要调用Refresh方法。

文件夹

Directory类的静态方法,可以快速创建,删除,文件夹,并可以枚举其下的文件和自文件夹。 Exists文件夹是否存在。 CreateDirectory创建文件夹,支持创建多级目录。 Delete删除文件夹。 GetFiles获取文件夹下的文件,支持通配符。 GetDirectories获取文件夹下的子文件夹,支持通配符。

还有相关的读写文本属性,访问时间,创建时间等方法。

DirectoryInfo类用来操作特定的一个文件夹,需要通过路径来实例化,后续操作都是针对这个文件夹。

需要注意的是,DirectoryInfo类型的属性不是实时的,如果需要最新状态,需要调用Refresh方法。

驱动器

通的DriveInfo类的静态方法GetDrives,可以获得当前机器的所有驱动器。

DriveType获取驱动器类型,比如固定或者可移动 VolumeLabel获取或设置卷标。 TotalFreeSpace 获取总容量 AvailableFreeSpace 获取可以容量 IsReady 是否只读

路径

通过Path类的静态方法,可以方便的操作路径。

Combine用来合并多个路径。在合并路径时,最后的绝对路径生效。 Combine不能用来操作Url,操作Url使用Uri类。

string path = Path.Combine("F:/","myfile","D:/","test.txt");//"d:/test.txt"

GetInvalidFileNameChars获取非法文件名字符。 GetInvalidPathChars获取非法路径字符。 Path.GetTempPath获取临时路径。 GetTempFileName获取临时文件路径。

网络流

System.Net.Sockets命名空间包含Socket套接字相关类型。

NetworkStream是在使用TCP协议时使用的网络流,读是接收数据,写是发送数据。

NetworkStream不支持定位。

缓冲流

缓冲流(BufferedStream)可以为流提供缓存功能。

缓冲流需要另一个流作为构造函数的参数,后续操作在缓冲流上进行操作。

对于文件,操作系统默认有缓冲,对于内存流,无需缓冲,这个类不是必须的。

压缩流

System.IO.Compression命名空间包含压缩解压缩的流。

DeflateStreamGZipStream使用方式相似,支持写入压缩流和读取压缩流。

压缩

public static void Compress()
{
    var fs = new FileStream("test.gs", FileMode.OpenOrCreate, FileAccess.ReadWrite);

    var gs = new GZipStream(fs, CompressionMode.Compress);

    using (var writer = new StreamWriter(gs))
    {
        //压缩数据并写入到文件流
        writer.WriteLine("hello world!");
    }
}

解压缩

    public static void Decompress()
    {
        var fs = new FileStream("test.gs", FileMode.Open, FileAccess.ReadWrite);

        var gs = new GZipStream(fs, CompressionMode.Decompress);

        using (var reader = new StreamReader(gs))
        {
            //自文件流读取已压缩的数据并解压。
            Console.WriteLine(reader.ReadLine());
        }
    }

压缩可以减少文件的体积,对于文本文件效果明显。