C# 文件读写系列二

读取文件原则上非常简单,但它不是通过FileInfo和DirectoryInfo来完成的,关于FileInfo和DirectoryInfo请参考C# 文件操作系列一,在.Net Framework4.5中可以通过File类来读写文件,在.Net Framework2.0推出之前,读写文件是相当费劲的,但是在.Net Framework2.0推出之后,它对File类进行了扩展,只要编写一行代码,就能对文件进行读写,下面通过一个窗体应用程序来展示文件的读写功能。

一、文件读取

1、通过File类的静态方法ReadAllText()进行文件的读取。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace FileReadWrite
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            ReadFile();
        }
        protected void ReadFile()
        {
            if (string.IsNullOrEmpty(textBox1.Text))
            {
                MessageBox.Show("请输入文件路径!");
            }
            else
            {
                string res = File.ReadAllText(textBox1.Text,Encoding.Default);
                textBox2.Text = res;
            }
        }
    }
}

2、通过File类的静态方法ReadAllLines()进行文件的读取,代码如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace FileReadWrite
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            ReadFile();
        }
        protected void ReadFile()
        {
            if (string.IsNullOrEmpty(textBox1.Text))
            {
                MessageBox.Show("请输入文件路径!");
            }
            else
            {
                string[] res = File.ReadAllLines(textBox1.Text,Encoding.Default);
                StringBuilder result = new StringBuilder();
                foreach (string re in res)
                {
                    result.Append(re);
                }
                textBox2.Text = result.ToString();
            }
        }
    }
}

3、通过File类的静态方法ReadAllBytes()进行文件的读取,代码如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace FileReadWrite
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            ReadFile();
        }
        protected void ReadFile()
        {
            if (string.IsNullOrEmpty(textBox1.Text))
            {
                MessageBox.Show("请输入文件路径!");
            }
            else
            {
                byte[] res = File.ReadAllBytes(textBox1.Text);
                StringBuilder sb = new StringBuilder();
                foreach (var b in res)
                {
                    sb.Append(b.ToString());
                }
                textBox2.Text = sb.ToString();
            }
        }
    }
}

二、文件写入

File提供了ReadAllBytes()、ReadAllLines()、ReadAllText()等读取文件的方式,所以对应的就会有相对应的写入文件的方式WriteAllBytes()、WriteAllLines()、WriteAllText()。

这里就不多做阐述.

三、通过流来操作文件

1、流的概念相信大家都不陌生,无论是哪种语言、哪种平台都会有流的存在,流是一个用于传输数据的对象,流可以双向传输,分为读取流和写入流。

a、读取流:数据从外部源传输到程序中

b、写入流:数据从程序传输到外部源中

外部源通常是一个文件,但也不都是一个文件,它也可能是:

a、网络上的数据(可通过一些网络协议进行读写)

.Net提供了一个System.Net.Sockets.NetworkStream类,可以通过它来处理网络数据。

b、读写到内存区域上的数据

.Net提供了一个System.IO.MemoryStream类,可以通过它来读取内存.

c、读写到命名管道上的数据

读写管道没有提供基本的流类,但是有一个泛型流类System.IO.Stream,如果要编写这样一个类,就可以从这个基类继承.

d、另一个计算机上发送的数据

e、外部源甚至可以代码中的一个变量,使用流在变量之间传输数据是一个常用的技巧,可以在数据类型之间转换数据。

使用一个独立的对象来传输数据,比使用FileInfo和DirectoryInfo类更好,应为把传输数据(名词)的概念和特定数据源分离开来,可以更容易的交换数据源。流对象本身包含许多通用的代码,可以在外部数据源和代码中的变量之间移动数据,把这些代码与特定数据源的概念区分开来,可以实现不同环境下代码的重用(通过继承).例如像StringReader和StringWriter,StreamReader和StreamWriter一样,都是同一继承树的一部分,这些类的后台一定共享很多的代码.

2、FileStream类

(1)、FileStream类的作用

a、这个类只要用于读取二进制文件中的二进制数据,当然也可以使用它读取任何文件,通常读取二进制文件要使用FileStream

b、FileStream对象实例表示在磁盘或网络路径上指向文件的流,这个类提供了在文件中读取字节的方法,但是经常使用StreamReader和StreamWriter来执行这些功能,因为FileStream操作的是字节和字节数组,而Stream类操作的是字符数据.

(2)、FileStream的构造方式

虽然.Net 提供了15种方式构建一个FileStream对象实例,但是构建一个FileStream对象实例,主要需要以下4条信息(也就是构造函数的参数):

a、要访问的文件

b、表示如何打开文件的模式。例如,新建一个文件或者打开一个现有的文件。如果打开一个现有的文件,写入操作是覆盖文件,还是追加到文件的末尾。

c、表示文件的访问方式------是只读,只写,还是读写?

d、共享访问------表示访问是否独占文件.如果允许其他流同时访问文件,则这些流是只读还是只写还是读写文件。

.Net提供的FileStream构造函数主要分为两类

一类是构造函数的第一个参数是一个文件的完整路径的字符串,其余的参数大致是FileMode、FileAcess、FileShare等...

一类是构造函数用老式的windows-api分格的windows句柄来处理文件...

本文主要用的是第一类,第一个参数是文件的完整路径的字符串的这一类的构造函数,构造形式如下:

接下来主要介绍如上构造函数中的三个参数:FileMode、FileAcess、FileShare

a、FileAccess枚举

注意,该枚举有一个Flags特性标签,说明FileAccess的枚举值可以进行按位的"|"或者按位的"&"运算,关于这个不清楚,请参考C# 特性(Attribute)之Flag特性

b、FileMode枚举

namespace System.IO
{
    using System;
    using System.Runtime.InteropServices;

    [Serializable, ComVisible(true)]
    public enum FileMode
    {
        /*
         * 创建新文件,此操作需要 System.Security.Permissions.FileIOPermissionAccess.Write权限
         * 如果文件已存在,将引发System.IO.IOException
         */
        CreateNew = 1,

        /*
         * 指定操作系统打开现有文件,文件一旦打开,就会截断成零字节大小
         * 此操作需要 System.Security.Permissions.FileIOPermissionAccess.Write
         * 尝试读取通过Truncate打开文件将导致异常.
         */
        Truncate = 5,

        /*
         * 指定操作系统创建新文件.如果文件已存在,那么它将被覆盖
         * 此操作需要 System.Security.Permissions.FileIOPermissionAccess.Write
         * FileMode.Create<=>相当于如果文件不存在则使用System.IO.FileMode.CreateNew
         * 如果文件存在,则使用System.IO.FileMode.Truncate.
         * 注:如果文件已存在但是为隐藏文件,则将引发System.UnauthorizedAccessException
         */
        Create = 2,

        /*
         * 若文件存在,则找到文件并定位到文件结尾.如果文件不存在,则创建一个新文件
         * FileMode.Append只能与FileAccess.Write一起使用.尝试查找文件尾之前的位置会引发System.IO.IOException
         * 并且任何尝试读取的操作都会失败并引发 System.NotSupportedException
         */
        Append = 6,

        /*
         * 指定操作系统打开现有的文件,打开文件的能力取决于System.IO.FileAccess.
         * 如果文件不存在,则引发System.IO.FileNotFoundException.
         */
        Open = 3,

        /*
         * 指定操作系统应打开文件.打开文件的能力取决于 System.IO.FileAccess 所指定的值.
         * 如果该文件不存在,则引发System.IO.FileNotFoundException.
         */
        OpenOrCreate = 4,

    }
}

c、FileShare枚举

这个枚举主要实现的是"文件读写锁"的功能,在开发过程中,我们往往需要大量读写文件的操作,在本地往往能完美运行(单线程),但是项目上线后,就会出现一系列的问题.(.Net本身是多线程环境),下面简单列举一些在多线程环境中会出现的问题.

i、写入一些内容到一个文件中,另一个线程/进程要读取文件的内容时报异常,提示System.IO.IOException:文件真由另一进程使用,因此该进程无法访问该文件.

ii、和上面i的顺序相反,在对一个文件进行读操作时,此时另一个线程/进程向该文件进行追加内容操作,也会报i中的异常.

iii、对一个文件进行简单的读写操作后,想删除文件,依然报上述的错误.

出现上述异常有两种可能的原因

第一种是对文件进行完一个操作后,没有进行资源的释放

第二种是当进行完一个操作后,可能要到某个时刻才真正结束,亦或是需要指定某个触发机制,操作才会真正结束

下面开始逐个解析FileShare的每个枚举值

// Generated by .NET Reflector from C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
namespace System.IO
{
    using System;
    using System.Runtime.InteropServices;
    
    [Serializable, Flags, ComVisible(true)]
    public enum FileShare
    {
        /*
         * 谢绝共享当前文件.文件关闭前,打开该文件的任何请求(由此进程或者另一进程)都将抛出异常
         */
        None = 0,

        /*
         * 允许随后打开文件读取,如果未指定此标记,则文件关闭前,,打开该文件的任何请求(由此进程或者另一进程)都将抛出异常
         * 但是,即使指定了该标记,仍可能需要附加权限才能够访问该文件.
         */
        Read = 1,

        /*
         * 允许随后打开文件写入,如果未指定此标记,则文件关闭前,,打开该文件的任何请求(由此进程或者另一进程)都将抛出异常
         * 但是,即使指定了该标记,仍可能需要附加权限才能够访问该文件.
         */
        Write = 2,

        /*
         * 允许随后打开文件写入或读取,如果未指定此标记,则文件关闭前,,打开该文件的任何请求(由此进程或者另一进程)都将抛出异常
         * 但是,即使指定了该标记,仍可能需要附加权限才能够访问该文件.
         */
        ReadWrite = 3,

        /*
         * 允许随后删除文件
         */
        Delete = 4,

        /*
         * 使文件可由子进程继承,Win32不直接支持此功能.
         */
        Inheritable = 0x10
    }
}

在讲解各个枚举值之前,先提供两个方法,用于测试,一个是写文件的方法

static void WriteFile(FileMode filemode,FileAccess fileacess,FileShare fileshare)
{
      var content = "测试";
      FileStream fs = new FileStream(FilePath, filemode, fileacess, fileshare);
      var buffer=Encoding.Default.GetBytes(content);
      fs.Write(buffer, 0,buffer.Length);
      fs.Flush();
}

一个是读文件的方法

static void ReadFile(FileAccess fileaccess,FileShare fileshare)
{
    FileStream fs=default(FileStream);
    try
    {
         fs = new FileStream(FilePath, FileMode.Open, fileaccess, fileshare);
         var buffer = new byte[fs.Length];
         fs.Position = 0;
         fs.Read(buffer, 0, buffer.Length);
         Console.WriteLine(Encoding.Default.GetString(buffer));
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}

FileShare.Read

允许随后打开文件读取,所以有如下示例代码:

WriteFile(FileMode.Create, FileAccess.Write, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.Read);
Console.ReadKey();

按照FileShare.Read的关法用法说明,出现上述问题只有一个原因,就是"允许随后打开文件读取"之前的操作可能不能是WriteFile,亦或只能是ReadFile,修改代码如下:

ReadFile(FileAccess.Read, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.Read);
Console.ReadKey();

到这一步还不能确认上面的假设,现在修改代码如下:

ReadFile(FileAccess.Read, FileShare.Read);
WriteFile(FileMode.Create, FileAccess.Write, FileShare.Read);
Console.ReadKey();

ok,通过上面的3个demo说明FileShare.Read(只读共享)只有在连续读取文件才有效.

FileShare.Write

允许随后打开文件写入,和FileShare.Read一样,FileShare.Write(只写共享)只有在连续写入文件是才有效,代码如下:

WriteFile(FileMode.Create, FileAccess.Write, FileShare.Write,"测试");
WriteFile(FileMode.Append, FileAccess.Write, FileShare.Write,"追加内容");

注:设置文件的共享方式为Write后,使用windows记事本也无法打开了.

FileShare.ReadWrite

综合FileShare.Read和FileShare.Write的特性

FileShare.None/FileShare.Delete

有了上面的经验,相信这两个你也很容易的就理解了,None则为不允许后续有任何操作,而Delete则是允许你随后进行删除操作。

(3)、通过FileInfo构建FileStream

a、通过FileInfo的实例方法OpenRead构建FileStream,代码如下:

FileInfo fi = new FileInfo(FilePath);
FileStream fs = fi.OpenRead();
byte[] bt = new byte[fs.Length];
fs.Read(bt, 0, bt.Length);
Console.WriteLine(Encoding.Default.GetString(bt));

查看OpenRead()的源代码,代码如下:

public FileStream OpenRead()
{
   return new FileStream(base.FullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, false);
}

现在一目了然,其实FileInfo的实例方法OpenRead其实是构建了一个FileStream的实例.

b、通过FileInfo的实例方法OpenWrite构建FileStream,代码如下:

FileInfo fi = new FileInfo(FilePath);
FileStream fs = fi.OpenWrite();
byte[] bt = Encoding.Default.GetBytes("测试");
fs.Write(bt, 0, bt.Length);

查看OpenWrite方法的源码,如下:

public FileStream OpenWrite()
{
    return new FileStream(base.FullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
}

和OpenRead()一样,它的内部原理也是构建了一个FileStream实例.

c、通过FileInfo的实例方法Open构建FileStream,代码如下:

FileInfo fi = new FileInfo(FilePath);
FileStream fs = fi.Open(FileMode.Create, FileAccess.Write, FileShare.None);
byte[] bt = Encoding.Default.GetBytes("测试");
fs.Write(bt, 0, bt.Length);

和OpenRead()、OpenWrite()一样,它的内部原理也是构建了一个FileStream实例.但是Open的参数是可配置的,不像OpenRead()、OpenWrite()

Open源码如下:

        public FileStream Open(FileMode mode)
        {
            return this.Open(mode, FileAccess.ReadWrite, FileShare.None);
        }
        
        public FileStream Open(FileMode mode, FileAccess access)
        {
            return this.Open(mode, access, FileShare.None);
        }
        
        public FileStream Open(FileMode mode, FileAccess access, FileShare share)
        {
            return new FileStream(base.FullPath, mode, access, share);
        }

(4)、关闭流

通过上面的内容了解了构建一个FileStream其实就是构建了一个文件流,通过查阅源码可以发现FileStream继承了Stream,而Stream实现了IDisposable接口,所以当我们使用完FileStream,必须显示的调用Dispose或者使用using语句块,来释放资源.

FileStream提供了一个Close实例方法,来释放资源,和完成GC的回收,Close源码如下:

public virtual void Close()
{
     this.Dispose(true);
     GC.SuppressFinalize(this);
}

关闭流会释放与它相关的资源,允许其他应用程序为同一个文件设置流,这个操作也会刷新缓冲区.在打开和关闭流之间,可以读写其中的数据.

(4)、通过FileStream的实例方法读取流

a、ReadByte()

ReadByte()是读取流数据的最简单的方式,他从流中读取一个字节,把结果转换成0~255之间的整数.如果达到该流的末尾,就返回-1,ReadByte()返回的是下一个流中的下一个字节,代码如下:

int nextByte = fs.ReadByte();

b、Read()

如果要一次读取多个字节,就调用Read(),把特定数量的字节读入到一个数组中,Read()返回实际读取的字节数,如果这个数是0,就表示达到了流的末尾,代码如下:

int nBytes=10;
int nextByte = fs.Read(bt, 0, nBytes);

(5)、通过FileStream的实例方法写入流(给文件写入内容流)

a、WriteByte()

将一个字节写入流中,代码如下:

FileStream fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
byte bt = 100;
fs.WriteByte(bt);

b、Write()

将一个字节数组写入流中,代码如下:

FileStream fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
byte[] bytes = Encoding.Default.GetBytes("测试");
fs.Write(bytes, 0, bytes.Length);

(6)、二进制文件读取器

新建一个windows窗体应用程序,主要是将选中的文件,转换成二进制形式.

代码如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace BinaryFileReader
{
    public partial class Form1 : Form
    {
        private readonly FileDialog choseOpenFileDialog = new OpenFileDialog();
        private string choseFilePath;
        public Form1()
        {
            InitializeComponent();
            menuFileOpen.Click += menuFileOpen_Click;
            choseOpenFileDialog.FileOk += choseOpenFileDialog_FileOk;
        }

        private void choseOpenFileDialog_FileOk(object sender, CancelEventArgs e)
        {
            choseFilePath = choseOpenFileDialog.FileName;
            this.Text = Path.GetFileName(choseFilePath);
            textBox1.Text = choseFilePath;
            DisplayFileName();
        }

        private void menuFileOpen_Click(object sender, EventArgs e)
        {
            choseOpenFileDialog.ShowDialog();
        }

        private void DisplayFileName()
        {
            int sizePerRow = 16;//每行显示的字符数
            FileStream fs = new FileStream(choseFilePath, FileMode.Open, FileAccess.Read);
            long nBytesToRead = fs.Length;
            if (nBytesToRead > 65536 / 4)
                nBytesToRead = 65536 / 4;//需要显示的字符数
            int nLines = (int)(nBytesToRead / sizePerRow) + 1;//总行数
            string[] lines = new string[nLines];
            int nBytesRead = 0;
            for (int i = 0; i < nLines; i++)
            {
                StringBuilder nextLine = new StringBuilder();
                nextLine.Capacity = 4 * sizePerRow;
                for (int j = 0; j < sizePerRow; j++)
                {
                    int nextByte = fs.ReadByte();
                    nBytesRead++;
                    if (nextByte == 0 || nBytesRead > 65535)
                        break;
                    char nextChar = (char)nextByte;
                    if (nextChar < 16)//char值小于16的字符都不是可打印字符
                        nextLine.Append(" x0" + string.Format("{0,1:X}", (int)nextChar));
                    else if (char.IsLetterOrDigit(nextChar) || char.IsPunctuation(nextChar))//字母数字十进制标点正常显示
                        nextLine.Append(" " + nextChar + " ");
                    else
                        nextLine.Append(" x" + string.Format("{0,2:X}", (int)nextChar));
                }
                lines[i] = nextLine.ToString();
            }
            fs.Close();
            this.textBox2.Lines = lines;

        }
    }
}

3、关于流缓存的问题

如果一个C#或者.Net程序需要读取Windows操作系统下面的一个文件,那么就可以通过文件流的方式,而如果需要读取文件流中的两个字节,那么该流则会把请求传递给Windows,注意此时Windows不会直接连接文件系统,在定位文件,并完成读取操作。而是在一次读取过程中,检索文件中的一个大块,并把该块保存到一个内存区域即缓冲区上。(后面系列的StreamReader和StreamWriter将会用到缓冲区.)以后对流中数据的请求,就会从该缓冲区中读取,直到读取完该缓冲区位置。此时windows会从文件中在获取另一个数据块.写入文件的方式与此相同,对于文件,操作系统会自动完成读写操作。

注:如果需要编写一个流类从没有缓存的设备中读取数据。如果是这样,就应从BufferedStream派生一个类,它实现一个缓冲区(但BufferedStream并不适用于应用程序频繁的切换读数据和写数据的情况).

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏蓝天

可epoll队列

就可以使用epoll来监控队列中是否有数据的队列,当然也支持select和poll。

1032
来自专栏大内老A

WCF中关于可靠会话的BUG!!

对WCF的可靠会话编程有一定了解的人应该知道,我们可以使用 DeliveryRequirementsAttribute 可以指示WCF确认绑定提供服务或客户端实...

19510
来自专栏大内老A

让我们的ASP.NET MVC应用可以单独维护验证消息

在项目开发中,我们会使用到很多的描述性文字,比如验证消息、错误消息和确认消息等,让这些文本消息具有可维护性具有重要的意义。虽然我们可以将它们存储于资源文件中,并...

2007
来自专栏逸鹏说道

c# 温故而知新: 线程篇(一) 下

Abort 方法: 其实 Abort 方法并没有像字面上的那么简单,释放并终止调用线程,其实当一个线程调用 Abort方法时,会在调用此方法的线程上引发一个异常...

2636
来自专栏MasiMaro 的技术博文

Windows平台下的内存泄漏检测

在C/C++中内存泄漏是一个不可避免的问题,很多新手甚至有许多老手也会犯这样的错误,下面说明一下在windows平台下如何检测内存泄漏。 在windows平...

2242
来自专栏分布式系统和大数据处理

Command模式入门

提起Command模式,我想没有什么比遥控器的例子更能说明问题了,本文将通过它来一步步实现GOF的Command模式。

1022
来自专栏张善友的专栏

编写前置和后置条件的连贯接口库:CuttingEdge.Conditions

当调用一个方法时,在其执行之前期望其处于特定状态; 也需要完成一些工作之后验证结果的状态。 这些假设被称为前置条件(pre-conditions )和 后置条件...

2097
来自专栏芋道源码1024

哪个更快:Java 堆还是本地内存

使用Java的一个好处就是你可以不用亲自来管理内存的分配和释放。当你用new关键字来实例化一个对象时,它所需的内存会自动的在Java堆中分配。堆会被垃圾回收器进...

1174
来自专栏开发 & 算法杂谈

Hiredis源码阅读(一)

Hiredis库主要包含三类API:同步api、异步api以及回复解析api。首先介绍一下同步api以及回复解析api。

53611
来自专栏GreenLeaves

Linq基础知识之延迟执行

Linq中的绝大多数查询运算符都有延迟执行的特性,查询并不是在查询创建的时候执行,而是在遍历的时候执行,也就是在enumerator的MoveNext()方法被...

24710

扫码关注云+社区

领取腾讯云代金券