C# 篇基础知识6——文件和流
计算机以文件的形式把数据存储在磁盘、光盘等存储设备上。文件的管理和操作是操作系统的一个重要组成部分,.NET 框架提供了一组功能强大的类,可以方便地对文件进行操作和管理。
1.文件操作相关的类
用于文件操作的类位于System.IO 命名空间中,用这些类可以方便地对文件进行创建、读写、复制、删除、移动、打开等操作。
2.File类和FileInfo类
命名空间 System.IO 中的File 类用于对文件进行创建、打开、复制、移动、删除、重命名等典型操作,并能获取或设置文件的属性信息。File 类中所有的方法都是静态的,使用起来非常简单,File 类的部分方法如下图所示:
File类的方法使用起来非常方便:
using System;
using System.IO;
static void Main(string[] args)
{ string path = @"D:\test.txt";
if(File.Exists(path)) //如果文件已经存在,则读取文件内容
{ //读取文件
string contents = File.ReadAllText(path);
Console.WriteLine("读取文件:\n" + contents); }
else //如果文件不存在,则创建文件并写入内容
{ string contents = "无可奈何花落去,\n 似曾相识燕归来,\n 小园香径独徘徊。";
File.WriteAllText(path, contents); //写入文件
Console.WriteLine("文件已写入。" ); }
}
注意 WriteAllText()、WriteAllLines()和WriteAllBytes()方法都会覆盖以前的文件,使用时要特别小心。要想在文件尾部追加新文本,请使用AppendAllText()方法。
FileInfo 类和File 类相似,可以创建、复制、移动、删除文件,可以获取或设置文件的属性,只是少了读写文件的功能。FileInfo 类的成员方法都是非静态的,使用方法前需要先创建一个FileInfo 类的对象,然后通过FileInfo 对象调用方法。当频繁操作某文件时,使用FileInfo 类的效率会更高。
(1)关于文件的异常
操作文件时经常会发生异常,比如指定文件不存在,路径无效,没有权限等。关于文件的异常的方法如图示,因此关于文件的操作一般要放在 try-catch 结构中,以处理可能发生的异常情况。
try
{ string path = @"D:\test.txt";
//如果文件已经存在,则读取文件内容
if(File.Exists(path))
{ //读取文件
string contents = File.ReadAllText(path);
Console.WriteLine("读取文件:\n" + contents);
}
else //如果文件不存在,则创建文件并写入内容
{
string contents = "无可奈何花落去,\n 似曾相识燕归来,\n 小园香径独徘徊。";
//写入文件
File.WriteAllText(path, contents);
Console.WriteLine("文件已写入。" );
}
}
catch (Exception e)
{ //异常处理
Console.WriteLine(e.Message);
}
3.Directory类和DirectoryInfo类
System.IO 命名空间中的Directory 类用于执行对目录(文件夹)的操作,比如创建、移动、删除、重命名等,也可通过它获取或设置目录的属性。Directory 类的部分方法如图所示,所有 Directory 类中的方法都是静态的,使用起来非常方便。
using System;
using System.IO;
static void Main(string[] args)
{ try
{ string path = @"D:\Program Files\Windows Media Player";
if (Directory.Exists(path)
{ //获取子目录
string[] dirs = Directory.GetDirectories(path);
Console.WriteLine("子目录:");
foreach (string dir in dirs) {
Console.WriteLine(dir); }
//获取文件
string[] files = Directory.GetFiles(path);
Console.WriteLine("文件:");
foreach (string file in files) {Console.WriteLine(file);}
}
else{Console.WriteLine("目录不存在");}
}
catch (Exception e)
{//异常处理Console.WriteLine(e.Message);}
}
操作结果所图所示,DirectoryInfo 类和Directory 类功能相似,区别是DirectoryInfo 类的成员方法都是非静态的,使用方法前需要先创建一个DirectoryInfo 类的对象。当频繁操作某目录时,使用DirectoryInfo 类的效率会更高。
4.path类
通过 System.IO 命名空间中的Path 类,我们可以方便的处理路径。Path 类的部分字段和方法如图所示。Path 类中的很多字段是和操作系统关联的,在不同的操作系统中可能有不同的结果。比如DirectorySeparatorChar,在Windows 和Macintosh 操作系统中的值是“\”,在Unix 操作系统中为“/”;VolumeSeparatorChar 在Windows 和Macintosh 操作系统中为“:”,在Unix操作系统中为“/”;PathSeparator 在 Windows 操作系统中默认值是“;”,在 Unix 操作系统上为“:”。
using System.IO;
//输出分隔符
Console.WriteLine("DirectorySeparatorChar" + Path.DirectorySeparatorChar);
Console.WriteLine(" PathSeparator" + Path.PathSeparator);
Console.WriteLine(" VolumeSeparatorChar" + Path.VolumeSeparatorChar);
//输出路径信息
string path = @"D:\Program Files\Windows Media Player\wmplayer.exe";
Console.WriteLine(" GetFileName" + Path.GetFileName(path));
Console.WriteLine(" GetFileNameWithoutExt" + Path.GetFileNameWithoutExtension(path));
Console.WriteLine(" GetExtension" + Path.GetExtension(path));
Console.WriteLine(" GetDirectoryName" + Path.GetDirectoryName(path));
结果如图所示:
(1)Environment类
通过 System 命名空间中的Environment 类,可以方便地获取与系统相关的信息。Environment 类的部分属性和方法如图所示。
一个例子:
//显示系统信息
Console.WriteLine(" 处理器数量: " + Environment.ProcessorCount);
Console.WriteLine(" 操作系统版本: " + Environment.OSVersion);
Console.WriteLine("公共语言运行库版本: " + Environment.Version);
Console.WriteLine(" 系统目录: " + Environment.SystemDirectory);
Console.WriteLine(" 用户域名: " + Environment.UserDomainName);
Console.WriteLine(" 用户名: " + Environment.UserName);
Console.WriteLine(" 机器名: " + Environment.MachineName);
通过 Environment 类的GetFolderPath()方法可以获取系统特殊文件夹的路径,这些特殊文件夹由SpecialFolder 枚举列出。SpecialFolder 类的部分成员如图所示。
例子:
string d = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string p = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
string m = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic);
string f = Environment.GetFolderPath(Environment.SpecialFolder.Favorites);
//输出特殊目录路径
Console.WriteLine("我的文档:" + d);
Console.WriteLine("我的图片:" + p);
Console.WriteLine("我的音乐:" + m);
Console.WriteLine("我的收藏:" + f);
5.基于流的文件操作
数据以文件的形式存储在硬盘、光盘等存储介质上,读写数据的过程可以看作数据像水一样流入或流出存储介质,所以.NET 设计了一种叫做流(Stream)的类来读写文件。实际上一旦我们打开一个文件,它就和一个流关联起来。文件是数据源,流是传输数据的工具,通过这种专门的工具来传输数据,就可以使传输工具和数据源分离开来,从而更容易切换数据源,更容易实现不同环境下代码的重用。数据源除了文件以外,还可以是内存中数据、网络上的数据甚至是代码中的一个变量等等。与流相关的类都定义在 System.IO 命名空间中,它们大多数继承于抽象类Stream。如图展示了部分与流相关的类。
FileStream 类用于字节数据输入和输出,TextReader 和TextWriter 用于Unicode 字符的输入和输出,BinaryReader 和BinaryWriter 用于二进制数据的输入和输出。
(1)FileStream类
FileStream 是专门进行文件操作的流,使用它可对文件进行读取、写入、打开和关闭等操作,既支持同步读写操作,也支持异步(缓冲)读写操作。FileStream 类的部分属性和方法如图所示:
要使用流,必须先创建一个流的对象。请创建一个名为“StreamTest”的控制台项目,并添加如下代码。
string fileName = @"D:\filestream_test.data";
//若文件不存在,则创建文件,写入数据
if(!File.Exists(fileName))
{ FileInfo myFile = new FileInfo(fileName); //创建文件
FileStream fs = myFile.OpenWrite(); //获取与文件对应的流
byte[] datas = { 100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
fs.Write(datas, 0, datas.Length); //用流写入数据
Console.WriteLine("数据已写入。");
fs.Close(); //关闭流
}
else //若文件存在,则读取数据
{ //建立与文件关联的流
FileStream fs = new FileStream(fileName, FileMode.Open, File Access.Read);
byte[] datas = new byte[fs.Length];
fs.Read(datas, 0, datas.Length); //用流读取数据
Console.WriteLine(“读取数据:”);
foreach(byte data in datas)
{ Console.Write(data);}
fs.Close(); //关闭流
}
这里我们用两种方法获取与文件对应的流。第一种办法是通过FileInfo类的OpenWrite()方法获取,OpenWrite()方法打开相应的文件,返回一个与该文件相关的FileStream对象。
当使用FileStream类的构造函数时,需要提供打开方式(FileMode)、访问模式(FileAccess)、共享模式(FileShare)等信息,它们分别用FileMode枚举、FileAccess枚举和FileShare枚举表示,这些枚举的值很容易理解,更详细的信息请参看帮助文档。与FileStream类相关的枚举如图所示:
FileStream类的Write()方法和Read()方法。Write()方法的原型如下,参数中字节数组array 是数据源,offset 和count 表示把数据源中的从位置offset 开始的count 个字节写入文件。
Read()方法的原型如下:参数中字节数组array 是目标数组,用于存储读取到的数据,存储的位置是从位置offset开始的count 个字节。
返回值为实际读取到的字节数。如果开始读取时当前位置已在流的末尾,则返回值为0。如果流中当前可用的字节数没有请求的字节数那么多,则实际读取到的字节数会小于请求的字节数。还有一个需要注意的情况是,在处理网络流(如FTP)、设备流(如串口输入)等流时,即使尚未到达流的末尾,实际读取到的字节数仍有可能少于请求的字节数。如果是从控制台读取或者从本地文件读取,很少遇到这种情况。
综上可以看出,FileStream 类主要用于向文件中写入或读取以字节为单位的数据。
(2)关于流的异常
try-catch-fianlly结构,用流来操作文件也可能会出现异常,所以关于流的操作一般要放在try-catch-fianlly 结构中,以增强程序的健壮性。
string fileName = @"D:\filestream_test.data";
FileStream fs = null;
try
{…}
catch(Exception e)
{ Console.WriteLine(e.Message);}
finally
{ if (fs!=null) fs.Close(); }
注意,fs对象的声明代码要放在try语句之前,不然catch块和fianly块中无法识别。另外关闭流的操作应放在fianlly块中,以确保无论操作成功还是操作失败,流都被及时释放。
读取一个文件的内容时,真正有用的代码往往只有几行,然而为了健壮性,我们却要写上10多行的代码来处理异常和关闭文件。如何才能减少这种麻烦呢?一种办法是自己编写一个方法,把异常处理和关闭文件的代码封装进去。下面编写了一个处理文件的通用方法UniversalProcess(),只要告诉它文件路径和处理文件的具体代码,它就会自动完成异常处理和关闭文件的操作。
public delegate void MyFileProcessCode(FileStream file);
//封装文件处理
public static void UniversalProcess(string path,MyFileProcessCode doSomething)
{ FileStream fs=new FileStream(path,FileMode.Open,FileAccess.ReadWrite);
try { doSomething(fs);}
catch(Exception e) {Console.WriteLine(e.Message);}
finally{if(fs!=null) fs.Close();}
}
可以直接通过匿名函数和一个文件地址进行访问,例如:
UniversalProcess(@"D:\a.text", delegate(FileStream fs)
{
byte[] datas = { 100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
fs.Write(datas, 0, datas.Length);
});
6.using语句
为了防止出现同步问题,当一个程序读写文件时,操作系统通常都会阻止其他程序读写该文件,因此使用完毕后要及时关闭,否则就会导致其他程序不能使用该文件。除了可以在fianlly块中关闭文件(流)外,我们还可以用C#提供的using语句进行文件操作。
using(FileStream fs = File.OpenWrite(path))
{
byte[] datas = { 100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
fs.Write(datas, 0, datas.Length);
}
在上面的代码在using后的括号里创建了一个流对象fs,然后在using后的语句块中使用该流对象。当退出using语句时,系统会自动关闭流对象fs,使用起来非常方便。using语句适用于那些需要及时释放资源的代码,其一般形式为:
using(type obj = initialization)
{ //具体处理代码}
在using后的括号中创建一个对象obj,然后在后面的大括号中使用它。当退出using语句时,对象obj会被及时销毁。using语句实际上是对try语句的封装,等价于:
{ type obj=initialization;
try {//具体处理代码}
finally{ if (obj!=null) { ((IDisposable)obj).Dispose();}}
}
程序先执行try块中的代码,不管是否发生异常,系统都会自动执行finally块中的代码,调用obj对象的Dispose()方法,销毁对象,释放资源。Dispose()方法和Close()方法的功能基本上是一样的,都是用来销毁对象。实际上,Close()方法就是通过调用Dispose(),方法实现的。
由此看出 using 语句不光适用于处理关于流的操作,它也适用于任何包含Dispose()方法且需要及时销毁的的对象。using 语句使用起来非常方便,如果你查看帮助文档,就会发现涉及流的例子都是用using 语句编写的。
7.用流读写文本文件
文本文件主要通过 StreamReader 和StreamWriter 来实现读写,它们分别继承于抽象类TextReader 和TextWriter。StreamReader 类的部分属性和方法,StreamWriter 类的部分属性和方法,分别所图示。
要使用 StreamReader 和StreamWriter,也必须先创建它们的对象。
StreamWriter streamWriter = null;
StreamReader streamReader = null;
try
{
//若文件不存在,则创建文件,写入数据
if(!File.Exists(@"D:\text_test.txt"))
{
FileInfo myFile = new FileInfo(@"D:\text_test.txt"); //创建文件
streamWriter = myFile.CreateText(); //获取文件对应的流
string text = @"何处望神州,满眼风光北固楼。千古兴亡多少事,悠悠。不尽长江滚滚流。年少万兜鍪,坐断东南战未休。天下英雄谁敌手?曹刘。生子当如孙仲谋。";
streamWriter.Write(text); //用流写入数据
Console.WriteLine("数据已写入.");
}
else//若文件存在,则读取数据
{ //建立与文件关联的流
streamReader = new StreamReader(@"D:\text_test.txt");
string text = streamReader.ReadToEnd(); //用流读取数据
Console.WriteLine("读取数据:\n" + text);
}
}
catch (Exception e)
{ //异常处理
Console.WriteLine(e.Message);
}
finally
{ //关闭流
if(streamWriter!=null)streamWriter.Close();
if(streamReader!=null)streamReader.Close();
}
这里我们用两种方法获取与文件对应的流。第一种办法是通过FileInfo 类(或File 类)的方法获取,比如FileInfo 对象myFile的CreateText()方法返回一个StreamWriter 流:StreamWriter streamWriter =myFile.CreateText(); OpenText ()方法返回StreamReader 流:StreamReader streamReader=myFile.ReadText(); 。File 类的部分与流相关的方法如图所示。
第二种办法是用 StreamReader 类或StreamWriter 类的构造函数创建流。
StreamReader streamReader = new StreamReader(fileName);
8.用流读写二进制文件
通过 BinryReader 和BinaryWriter 类实现二进制读写。BinryReader 类的部分属性和方法,BinaryWriter 类的部分属性和方法,分别如图所示:
要使用 BinryReader 和BinaryWriter 类,也必须先创建它们的对象,但它们的对象总是通过FileStream 对象来创建的。
string path = @"D:\binary_test.txt";
FileStream fileStream = null;
try
{ //创建文件
fileStream = File.Create(path);
//写文件
BinaryWriter bw = new BinaryWriter(fileStream);
DateTime time = DateTime.Now;
bw.Write(time.Year);
bw.Write(time.Month);
bw.Write(time.Day);
bw.Write(time.Hour);
bw.Write(time.Minute);
bw.Write(time.Second);
//当前位置移动到开头
fileStream.Seek(0, SeekOrigin.Begin);
//读文件
BinaryReader br = new BinaryReader(fileStream);
int year = br.ReadInt32();
int month = br.ReadInt32();
int day = br.ReadInt32();
int hour = br.ReadInt32();
int minute = br.ReadInt32();
int second = br.ReadInt32();
Console.WriteLine("{0}-{1}-{2} {3}:{4}:{5}",
year, month, day, hour, minute, second);
}
catch (Exception e)
{
//异常处理
Console.WriteLine(e.Message);
}
finally
{
//释放资源
if(fileStream!=null)
fileStream.Close();
}
实际上,BinryReader 和BinaryWriter 本身并不是独立的流,它们是对其他流的包装,所以创建BinryReader 和BinaryWriter 对象时,需要传给它们一个FileStream 对象,对BinryReader 和BinaryWriter 的操作,就是对FileStream 的操作。