读取文件原则上非常简单,但它不是通过FileInfo和DirectoryInfo来完成的,关于FileInfo和DirectoryInfo请参考,在.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的枚举值可以进行按位的"|"或者按位的"&"运算,关于这个不清楚,请参考
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.dllnamespace 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并不适用于应用程序频繁的切换读数据和写数据的情况).