文件读写和流

 一 流的概念

 

在.NET中Stream 是所有流的抽象基类。流是字节序列的抽象概念,或者说是计算机在处理文件或数据时产生的二进制序列。例如文件、输入/输出设备、内部进程通信管道或者 TCP/IP 套接字。Stream 类及其派生类提供这些不同类型的输入和输出的一般视图,使程序员不必了解操作系统和基础设备的具体细节。简单的说流提供了不同介质之间的数据交互功能。

在.NET中常用的流有BufferedStream 、FileStream、MemoryStream和NetworkStream,他们都是位于System.IO和System.Net命名空间下。流涉及三个基本操作: 读取,写入和查找。根据基础数据源或储存库,流可能只支持这些功能中的一部分。有些流实现执行基础数据的本地缓冲以提高性能。对于这样的流,Flush 方法可用于清除所有内部缓冲区并确保将所有数据写入基础数据源或储存库。

 

 

二 文件读写

 

对于文件的读写,实际是把硬盘中的数据读入内存和把内存的数据写入硬盘,他们数据之间的交换就是通过流来完成的。在.NET中这个功能是由FileStream类完成的。他提供的Write和Read方法可以对文件进行读写操作。

 

1:FileStream读写文件

 

使用 FileStream 类对文件系统上的文件进行读取、写入、打开和关闭操作,并对其他与文件相关的操作系统句柄进行操作,如管道、标准输入和标准输出。读写操作可以指定为同步或异步操作。FileStream 对输入输出进行缓冲,从而提高性能。

static void Main(string[] args)
{
    try
    {
        FileStream fs = new FileStream(@"c:/text.txt", FileMode.Create);
        string message = "This is example for filestream";
        byte[] writeMesaage = Encoding.UTF8.GetBytes(message);
        fs.Write(writeMesaage, 0, writeMesaage.Length);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    finally
    {
        Console.ReadKey();
    }
}

上面是一个简单的例子,把一条字符串写入到文件中。首先建立一个FileStream对象,指定文件和读写方式(具体读写方式和权限可以参加MSDN)。接下来把要写入的字符串以一定的编码格式存入一个字节数组中,然后调用Writer方法写入文件。运行程序,当程序执行到Console.ReadKey方法时去查看文件发现文件中内容是空的。也就是说调用Writer方法后内容并没有被写入到文件中。

这里就要谈到流中的缓冲区的问题了。缓冲区是为了提高I/O效率而设置的,我们知道读写的I/O操作是很费时的,如果每一个字节都马上写入到文件中整个过程就会很慢,所以设置缓冲区,写把要写入的内容写入到缓冲区中,然后在一次性写入到文件中,来提高写入的效率和速度。而Write方法实际上只是把数据写入到流的缓冲区中,而不是真正的写入到文件中。所以调用Writer方法并不能完成文件的写入。于是FileStream对象提供了一个把缓冲区写入文件的方法,那就是Flush方法。

Flush:清除该流的所有缓冲区会使得所有缓冲的数据都将写入到文件系统。这是MSDN给出的定义,可以看到,只有调用了Flush方法后数据才会被真正的写入到文件中。所以这里就又另外一个问题,那就是可能存在写入失败。比如上面在Writer方法结束后发生了异常,那么数据就无法写入到文件中了。所以我们在调用Writer方法后可以显式的调用Flush方法来把数据写入到文件中。但是上面的方法结束后又会发现数据被写入了。其实这是因为在程序结束时,销毁FileStream对象时,系统自动调用了Flush方法来保证内容被写入到文件中。而在FileStream对象中,很多地方都调用了这个方法,比如Close方法和Dispose方法。所以在程序中,调用这2个方法销毁对象时也会把数据从缓冲区写入文件。所以使用FileStream对象Writer方法后只要不抛出异常,缓冲区数据总会被写入文件(当然也可能因为磁盘已满而在写入是抛出异常)。但是我们最好还是显示的调用Close方法或使用using块关闭对象,使数据写入。或是调用Flush方法。Flush方法内部调用API的internal static extern unsafe int WriteFile方法实现文件写入。

对于读取文件内容也是类似的,要先把数据读取到字节数组中。而且还提供了BeginRead和BeginWrite方法进行异步读写操作。

 

 

 2 StreamWriter写文件

 

上面的FileStream操作文件读写,每次都需要使用字节数组,因为FileStream操作对象是字节。而.NET提供了StreamWriter和StreamReader对象来对流进行读写操作。他的构造函数可以接受一个Stream对象。从而对流进行操作。他们的内部有个一Stream对象来维护传入的各种流对象。并且也提供了Write和Read方法。实际上这2个类是对流读写的的一个包装,方便我们使用。当我们传一个流对象时,调用读写方法是,实际调用该对象自己重写的方法。而当我们在构造函数中传入的是文件路径时,他就成为了对文件读写的操作。因为他在内部构建了一个FileStream对象,并交给内部的Stream对象维护。

public StreamWriter(string path) : this(path, false, UTF8NoBOM, 0x400)
{
}

 
public StreamWriter(string path, bool append, Encoding encoding, int bufferSize) : base(null)
{
    if ((path == null) || (encoding == null))
    {
        throw new ArgumentNullException((path == null) ? "path" : "encoding");
    }
    if (bufferSize <= 0)
    {
        throw new ArgumentOutOfRangeException("bufferSize", Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum"));
    }
    Stream stream = CreateFile(path, append);
    this.Init(stream, encoding, bufferSize);
}


private static Stream CreateFile(string path, bool append)
{
    return new FileStream(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, 0x1000, FileOptions.SequentialScan);
}

通过上面的代码,可以看到我们使用 public StreamWriter(string path)构造方法和我们自己新建一个FileStream对象传递给StreamWriter(Stream)构造方法是一样的。不同的是后者还可对其他继承与Stream的流进行操作。而且可以指定文件读取的方式和访问权限以及缓冲区大小。

static void Main(string[] args)
{
    try
    {
        StreamWriter sw = new StreamWriter(@"c:/text.txt");
        sw.Write("This is StreamWriter");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    finally
    {
        Console.ReadKey();
    }
}

上面的代码是使用StreamWriter对文件进行写操作,当执行到ReadKey时,我们发现文件没有被写入,这个和FileStream是一样的,但是当程序执行完后我们发现,数据还是没有被写入。如果我们写入的数据量比较大时,数据也被写入到文件中,但是会发现写入的数据可能并不完整。因为只有当StreamWriter内部的缓冲区充满或调用Flush时,才会把数据写入Stream对象中。StreamWriter 未将最后 1 至 4 KB 数据写到文件。后面会具体解释。

MSDN中对此的解释是:

StreamWriter 在内部缓冲数据,这需要调用 Close 或 Flush 方法将缓冲数据写到基础数据存储区。如果没有适当地调用 Close 或 Flush,StreamWriter 实例中缓冲的数据可能不会按预期写出。

在StreamWriter中也有Flush方法,清理当前编写器的所有缓冲区,并使所有缓冲数据写入基础流。对于StreamWriter来说,也有自己的缓冲区,而不同的是StreamWriter缓冲区是char[]而不是byte[]。而StreamWriter的write方法只是把数据写入到自己的缓冲区中,所以我们必须条用Flush方法来写入到文件中,而Flush方法中则是先调用了FileStream的write方法把StreamWriter缓冲区的数据写入到FileStream的缓冲区中,最后在调用FileStream的Flush方法写入文件。

//StreamWriter.write把数据写入StreamWriter缓冲区中
public override void Write(string value)
{
    if (value != null)
    {
        int length = value.Length;
        int sourceIndex = 0;
        while (length > 0)
        {
            if (this.charPos == this.charLen)
            {
                this.Flush(false, false);
            }
            int count = this.charLen - this.charPos;
            if (count > length)
            {
                count = length;
            }
            value.CopyTo(sourceIndex, this.charBuffer, this.charPos, count);
            this.charPos += count;
            sourceIndex += count;
            length -= count;
        }
        if (this.autoFlush)
        {
            this.Flush(true, false);
        }
    }
}

//StreamWriter.Flush把StreamWriter缓冲区内容写入Stream的缓冲区
private void Flush(bool flushStream, bool flushEncoder)
{
    if (this.stream == null)
    {
        __Error.WriterClosed();
    }
    if (((this.charPos != 0) || flushStream) || flushEncoder)
    {
        if (!this.haveWrittenPreamble)
        {
            this.haveWrittenPreamble = true;
            byte[] preamble = this.encoding.GetPreamble();
            if (preamble.Length > 0)
            {
                this.stream.Write(preamble, 0, preamble.Length);
            }
        }
        int count = this.encoder.GetBytes(this.charBuffer, 0, this.charPos, this.byteBuffer, 0, flushEncoder);
        this.charPos = 0;
        if (count > 0)
        {
            this.stream.Write(this.byteBuffer, 0, count);
        }
        if (flushStream)
        {
            this.stream.Flush();
        }
    }
}

通过上面的代码可以明白,真正完成写入文件的也是Flush方法,因为它的工作是调用了FileStream的write和flush方法。而在StreamWriter的Close和Dispose的方法中则是调用了StreamWriter的Flush方法写入文件,然后用FileStream.Close方法关闭流。所以在关闭具有 StreamWriter 的实例的应用程序或任何代码块之前,确保调用 StreamWriter 的 Close 或 Flush。达到此目的的最佳机制之一是用 C# using 块创建该实例,这样将确保调用编写器的 Dispose 方法,从而正确关闭该实例。另外在StreamWriter中有一个AutoFlush属性,如果设置为True,则在调用writer方法后会自动调用Flush方法。

 

 

3 FileStream和StreamWriter的依赖关系

 

如果我们使用public StreamWriter(string path)构造方法不会存在这个问题,因为FileStrem对象是内部控制的,如果我们用StreamWriter(Stream)构造方法就可能存在一些问题。

static void Main(string[] args)
{
    FileStream fs = null;
        StreamWriter sw = null;
    try
    {
        fs = new FileStream(@"c:/text.txt", FileMode.Create);
        sw = new StreamWriter(fs);
        string message = "This is StreamWriter/r/n";
        for (int i = 0; i < 10; i++)
        {
            message += message;
        }
        sw.Write(message);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    finally
    {
        fs.Close();
        sw.Close();
        Console.ReadKey();
    }
}

执行上面的代码的时候会出现Dispose异常,无法访问已经关闭的文件。这是因为我们先关闭了文件流,然后在关闭StreamWriter对象。而StreamWriter对象的Close方法实际是关闭当前的 StreamWriter 对象和基础流。也就是说我们只需调用这一个方法就可以了。而如果在数据写入前调用了FileStream的Close方法,那么数据最终是无法写入的,还会引发异常。所以在写入文件时,最好只调用StreamWriter对象的Close方法就行了。

上面说过了没有调用Close方法导致部分数据没有写入,这是因垃圾回收造成的。当我们调用完write方法后,没有调用close,系统发现StreamWriter和FileStream对象不可达,会对他们进行终结操作,但是终结的顺序是不确定的。如果先关闭了FileStream会出现数据无法写入。微软为了避免这种情况,就不让StreamWriter方法实现Finalize方法,这样,在程序结束时,没有执行StreamWriter的Finalize方法,也就无法把缓冲区的数据写入FileStream中。而FileStream内部实现了Finalize方法。这也就是为什么FileStream不关闭仍然可以把数据写入文件。所以在使用StreamWriter对象时不显调用Close方法时,缓冲区的数据一定会丢失。

而且WriterStream的内部缓冲区填满后会自动写入到Stream流中。所以当我们写入的数据很少时,不够填充满数据缓冲区,而且不关闭对象,必然无法写入文件。而当我们写大量数据时,一部分数据在缓冲区满的时候被写入了Stream中,当我们不关闭对象,直接结束程序时,Stream会执行Finalize方法,把数据写入文件,而StreamWriter没有此方法,而且默认的缓冲区大小为4K。如果此时缓冲区中还有数据必定无法被写入,而且大小是1-4K。

 

 

3 BinaryWriter

 

BinaryWriter对象也可以用写文件,以二进制形式将基元类型写入流,并支持用特定的编码写入字符串。与StreamWriter不同的是,他不存在缓冲区丢失的问题。因为他每次调用Write方法以后说首先把数据写入自己的char[]数组,然后转换为指定编码的Byte[]数组,最后调用Stream的Write方法写入到流的缓冲区。

BinaryWriter对象也有Flush方法,但是只是简单的调用了Stream的Flush方法,而他的Close和Dispose方法则是调用了Stream的Close方法。和上面一样 BinaryWriter对象也没有实现Finalize方法,但是因为他没有把数据放到自己的缓冲区,每次都是立即写入到流中。所以即便不调用Flush方法或是显式关闭对象,最后也会全部被写入到文件中,因为数据全部在FileStream的缓冲区中,而程序结束时Finalize方法会调用Flush把数据写入文件。


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

2 评论

  1. 解析得很透彻,第3部分关于StreamWriter.Close()方法的调用,当构造时使用的是Path,有很多程序员易犯错。在看了你绘制的类关系图后,配合MSDN的解释,就很容易理解了。
    (1)Close()方法重写的是TextWrite.Close();
    (2)TextWrite.Close()方法将释放FileStream所占用资源。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注