前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C# 文件读写系列二

C# 文件读写系列二

作者头像
郑小超.
发布2018-01-26 16:06:40
1.2K0
发布2018-01-26 16:06:40
举报
文章被收录于专栏:GreenLeavesGreenLeaves

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

一、文件读取

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

代码语言:javascript
复制
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()进行文件的读取,代码如下:

代码语言:javascript
复制
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()进行文件的读取,代码如下:

代码语言:javascript
复制
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枚举

代码语言:javascript
复制
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的每个枚举值

代码语言:javascript
复制
// 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
    }
}

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

代码语言:javascript
复制
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();
}

一个是读文件的方法

代码语言:javascript
复制
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

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

代码语言:javascript
复制
WriteFile(FileMode.Create, FileAccess.Write, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.Read);
Console.ReadKey();

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

代码语言:javascript
复制
ReadFile(FileAccess.Read, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.Read);
Console.ReadKey();

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

代码语言:javascript
复制
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(只写共享)只有在连续写入文件是才有效,代码如下:

代码语言:javascript
复制
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,代码如下:

代码语言:javascript
复制
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()的源代码,代码如下:

代码语言:javascript
复制
public FileStream OpenRead()
{
   return new FileStream(base.FullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, false);
}

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

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

代码语言:javascript
复制
FileInfo fi = new FileInfo(FilePath);
FileStream fs = fi.OpenWrite();
byte[] bt = Encoding.Default.GetBytes("测试");
fs.Write(bt, 0, bt.Length);

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

代码语言:javascript
复制
public FileStream OpenWrite()
{
    return new FileStream(base.FullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
}

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

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

代码语言:javascript
复制
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源码如下:

代码语言:javascript
复制
        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源码如下:

代码语言:javascript
复制
public virtual void Close()
{
     this.Dispose(true);
     GC.SuppressFinalize(this);
}

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

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

a、ReadByte()

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

代码语言:javascript
复制
int nextByte = fs.ReadByte();

b、Read()

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

代码语言:javascript
复制
int nBytes=10;
int nextByte = fs.Read(bt, 0, nBytes);

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

a、WriteByte()

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

代码语言:javascript
复制
FileStream fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
byte bt = 100;
fs.WriteByte(bt);

b、Write()

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

代码语言:javascript
复制
FileStream fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
byte[] bytes = Encoding.Default.GetBytes("测试");
fs.Write(bytes, 0, bytes.Length);

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

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

代码如下:

代码语言:javascript
复制
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并不适用于应用程序频繁的切换读数据和写数据的情况).

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-07-16 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档