一、前言
C#经常要对数据进行读写操作,而进行这些操作的基础是在流上进行的,即C#通过流来实现对数据的读取。C#中常用的流有两种:
1、输出流:当向某些外部目标写入数据时,就要用到输入流。
2、输入流:用于将数据读入程序可以访问的内存或变量中。
System.IO名称空间包含了用于在文件中读写数据的类,这里仅介绍用于文件输入输出的主要类,如下:
类 | 说明 |
File | 静态实用类,提供许多静态方法,用于移动、复制和删除文件。 |
Directory | 静态实用类,提供许多静态方法,用于移动、复制和删除目录。 |
Path | 实用类,用于处理路径名称。 |
FileInfo | 表示磁盘上的物理文件,该类包含处理此文件的方法。要完成对文件的读写工作,就必须创建Stream对象。 |
DirectoryInfo | 表示磁盘上的物理目录,该类包含处理此目录的方法。 |
FileSystemInfo | 用作FileInfo和DirectoryInfo的基类,可以使用多态性同时处理文件和目录。 |
FileStream | 表示可写或可读,或二者均可的文件。可以同步或异步地读写此文件。 |
StreamReader | 从流中读取字符数据,可以使用FileStream作为基类创建。 |
StreamWriter | 向流写入字符数据,可以使用FileStream作为基类创建。 |
FileSystemWatcher | FileSystemWatcher是本章要介绍的最复杂类。它用于监控文件和目录,提供了这些文件和目录发生变化时 应用程序可以捕获的事件。 |
DeflateStream——表示在写入时自动压缩数据或读取时自动解压缩的流,使用Deflate算法来实现压缩。
GZipStream——表示在写入时自动压缩数据或者在读取时自动解压缩的流,使用GZIP算法来实现压缩。
二、FiIe类和Directory类
File和Directory实用类提供了许多的静态方法,用于处理文件和目录,这些方法可以移动文件、查询和更新特性,还可以创建FileStream对象。如下为File类最常用的静态类方法:
方 法 | 说明 |
Copy() | 将文件从源位置复制到目标位置。 |
Create() | 在指定的路径上创建文件。 |
Delete() | 删除文件。 |
Open() | 返回指定路径上的FileStream对象。 |
Move() | 将指定的文件移到新位置。可在新位置为文件指定不同的名称。 |
Directory类最常用的静态类方法:
方法 | 说明 |
CreateDirectory() | 创建具有指定路径的目录。 |
Delete() | 删除指定的目录及其中的所有文件。 |
GetDirectories() | 返回表示指定目录下的目录名的string对象数组。 |
EnumerateDirectories() | 与GetDirectories()类似,但返回目录名的IEnumerable<string>集合。 |
GetFiles() | 返回在指定目录中的文件名的string对象数组。 |
EnumerateFiles() | 与GetFiles()类似,但返回文件名的IEnumerable<string>集合。 |
GetFileSystemEntries() | 返回指定目录中的文件和目录名的string对象数组。 |
EnumerateFileSystemEntries() | 与GetFilesSystemEntries()类似,但返回文件和目录名的IEnumerable<string>集合。 |
Move() | 将指定目录移到新位置,可在新位置为文件夹指定一个新名称。 |
FileInfo类:它不是静态类,没有静态方法,只有在实例化后才能使用,FileInfo对象表示磁盘或网络位置上的文件,提供文件路径就可以创建一个FileInfo对象:
FileInfo aFile = new FileInfo(@“C:\Log.txt”);注:@表示该字符串按照字面意思解释,而不解释为转义字符
FileInfo类提供了许多方法类似于File类,但由于File是静态方法,它需要一个字符串参数为每个方法调用指定的文件位置,下面代码完成相同工作:
FileInfo aFile = new FileInfo(“Data.txt”); if(File.Exists(“Data.txt”)) Console.WriteLine("File Exists");
if(aFile.Exists) Console,WriteLine("File Exists");
FileInfo和File的使用场景:
1、如果仅进行单一方法调用,则可以使用静态File类,因为不用实例化,所以调用要快些。
2、如果应用程序在文件上执行几种操作,则实例化FileInfo对象并使用其方法就更好一些。
下面介绍FileSystem的属性:
属性 | 说明 |
Attributes | 使用FileAttributes枚举,获取或者设置当前文件或目录的特性。 |
CreationTime, CreationTimeUtc | 获取当前文件的创建日期和时间,可以使用UTC和非UTC版本。 |
Extension | 提取文件的扩展名。这个属性是只读的。 |
Exists | 确定文件是否存在,这是一个只读的抽象属性,在FileInfo和DirectoryInfo中进行了重写。 |
FullName | 检索文件的完整路径,这个属性是只读的 |
LastAccessTime, LastAccessTimeUtc | 获取或设置上次访问当前文件的日期和时间,可以使用UTC和非UTC版本 |
LastWriteTime, LastWriteTimeUtc | 获取或设置上次写入当前文件的日期和时间,可以使用UTC和非UTC版本 |
Name | 检索文件的完整路径,这是一个只读抽象属性,在FileInfo和DirectoryInfo中进行了重写。 |
属性 | 说明 |
Directory | 检索一个DirectoryInfo对象,表示包含当前文件的目录。这个属性是只读的 |
DirectoryName | 返回文件目录的路径。这个属性是只读的 |
IsReadOnly | 文件只读特性的快捷方式。也可以通过Attributes来访问这个属性 |
Length | 获取文件的大小(以字节为单位),返回long值。这个属性是只读的 |
下面介绍DirectoryInfo:
Directory和DirectoryInfo的区别及用法跟File和FileInfo的区别及用法类似。以下是DirectoryInfo的专用属性:
属性 | 说明 |
Parent | 检索一个DirectoryInfo对象,表示包含当前目录的目录。这个属性是只读的 |
Root | 检索一个DirectoryInfo对象,表示包含当前目录的根目录,例如 C:\目录。这个属性是只读的 |
路径名:也成绝对路径名。就是显示的指定文件或目录来自于哪一个已知的位置,通俗讲就是完整路径。
相对路径:以当前工作目录为起点,这是相对路径名的默认设置。例如应用程序运行在C:\Development\FileDemo目录上,并使用相对路径LogFile.txt,该文件就是:C:\Development\FileDemo\LogFile.txt。若想上移目录,要使用..字符串,那么路径..\Log.txt表示C:\Development\Log.txt文件。如果上移两个目录,则是..\..\。如果有必要可使用Directory.GetCurrentDirectory()找出工作目录的当前位置,也可以使用Directory.SetCurrentDirectory()设置新路径。
FileStream对象:
FileStream对象表示指向磁盘或网络路径上的文件流。这个类提供了在文件中读写字节的方法。但经常使用StreamReader或StreamWrite执行这些功能,这是因为FileStream类操作的是字节和字节数组,而Stream类操作的是字符数据。字符数据易于使用,但是有些操作,如随机文件访问(访问文件中间某点的数据),就必须由FileStream对象执行。创建FileStream对象最简单的构造函数仅有两个参数,即文件名和FileMode枚举值。如下:
FileStream aFile = new FileStream(filename,FileMode.<Member>); FileMode枚举包含几个成员,指定了如何打开或创建文件。如表1-7:
成员 | 文件存在 | 文件不存在 |
Append | 打开文件,流指向文件的末尾处,只能与 枚举FileAccess.Write结合使用 | 创建一个新文件。只能与枚举 FileAccess.Write结合使用 |
Create | 删除该文件,然后创建新文件 | 创建新文件 |
CreateNew | 抛出异常 | 创建新文件 |
Open | 打开文件,流指向文件开头处 | 抛出异常 |
OpenOrCreate | 打开文件,流指向文件开头处 | 创建新文件 |
Truncate | 打开文件,清除其内容,流指向文件 开头处,保留文件的初始创建日期 | 抛出异常 |
另外一个常用的构造函数如下:
FileStream aFile = new FileStream(filename,FileMode.<Member>,FileAccess.<Member>);第三个参数是FileAcess枚举的一个成员,它指定了流的作用。如表1-8:
成员 | 说明 |
Read | 打开文件,用于只读 |
Write | 打开文件,用于只写 |
ReadWrite | 打开文件,用于读写 |
File和FileInfo类都提供了OpenRead()和OpenWrite()方法,更易于创建FileStream对象,前者打开了只读访问的文件,后者只允许写入文件。如下打开了用于只读访问的Data.tex文件:
FileStream aFile = File.OpenRead("Data.txt");
下面代码执行同样的功能:FileInfo aFileInfo = new aFileInfo("Data.txt"); FileStream aFile = aFileInfo.OpenRead();
1、文件位置
FileStream类维护内部文件指针,该指针指向文件中进行下一次读写操作的位置。大多情况下打开文件时,它就指向文件的开始位置,但可以通过Seek()方法修改位置。该方法有两个参数:第一个指定文件指针移动距离(以字节为单位)。第二个指定开始计算的起始位置,用SeekOrigin枚举的一个值表示。SeekOrigin枚举包含:Begin、Current和End。例如:aFile.Seek(8,SeekOrigin.Begin);将指针从文件开始位置移动8个字节。
2、读取数据
FileStream类只能处理原始字节(byte),这使得FileStream类可以用于任何数据文件,而不仅仅是文本文件,诸如图像声音等都可读取,但是FileStream对象不能直接将数据读入字符串,StreamReader类却可以。
FileStream.Read()方法是从FileStream对象所指向的文件中访问数据的主要手段,这个方法从文件中读取数据,再把数据写入一个字节数组,它有三个参数:第一个是传入的字节数组,用来接受FileStream对象中的数据;第二个是字节数组中开始写入数据的位置,它通常是0;第三个指定从文件中读取多少字节。
例子:
static void Main(string[] args)
{
byte[] byteData = new byte[200];
char[] charaData = new char[200];
FileStream aFile = new FileStream("../../Program.cs",FileMode.OpenOrCreate);
aFile.Seek(144,SeekOrigin.Begin);
aFile.Read(byteData,0,200);
Decoder d = Encoding.UTF8.GetDecoder();
d.GetChars(byteData,0,byteData.Length,charaData,0);
Console.WriteLine(charaData);
Console.ReadLine();
}
3、写入数据
写入过程与读取过程类似。如下例子:
static void Main(string[] args)
{
byte[] byteData;
char[] charaData;
FileStream aFile = new FileStream("Program.txt",FileMode.Create);
charaData = "My pink half of the drainpipe.".ToCharArray();
byteData = new byte[charaData.Length];
Encoder d = Encoding.UTF8.GetEncoder();
d.GetBytes(charaData, 0, charaData.Length, byteData, 0,true);
aFile.Seek(0,SeekOrigin.Begin);
aFile.Write(byteData,0,byteData.Length);
}
StreamWriter对象:
操作字节数组比较麻烦,所以通常使用StreamReader或StreamWriter来处理文件,如果不需要改变文件指针位置,这些类就很容易操作文件。StreamWriter类允许将字符和字符串写入到文件中,它处理底层的转换,向FileStream对象写入数据。创建StreamWriter对象的方法很多,如果已经有了FileStream对象,则可以使用此对象来创建:
FileStream aFile = new FileStream("Log.txt",FileMode.CreateNew);
StreamWriter sw = new StreamWriter(aFile);
也可以直接从文件中创建StreamWriter对象:
StreamWriter sw = new StreamWriter("Log.txt",true);这个构造函数的参数是文件名和一个Boolean值,这个Boolean值指定是追加文件,还是创建新文件:
a、如果是false,则创建一个新文件或者截取现有文件并打开它
b、如果是true,则打开文件,保留原来的数据,如果找不到,则创建一个新文件
StreamWriter对象不会提供像FileStream中FileMode、FileAccess类似的选项,只有使用Boolean来追加文件或创建新文件,因此总是对文件拥有读写权。若想使用高级参数,必须首先在FileStream构造函数中指定,然后在FileStream对象中创建StreamWriter。示例如下:
static void Main(string[] args)
{
FileStream aFile = new FileStream("Log.txt",FileMode.OpenOrCreate);
StreamWriter sw = new StreamWriter(aFile);
bool truth = true;
sw.WriteLine("Hell to you");
sw.WriteLine("It is now {0} and things are looking good.",DateTime.Now.ToLongDateString());
sw.Write("More than that");
sw.Write("it's {0} that C# is fun.",truth);
sw.Close();
}
注意:WriteLine()自动换行,Write()则每次会在上次的结尾处写入。
StreamReader对象:
用于读取数据,它的创建方式跟StreamWriter类似,最简单的创建方式如下:
FileStream aFile = new FileStream(“Log.txt”,FileMode.Open);StreamReader sr = new StreamReader(aFile);
也可用具体文件路径创建:StreamReader sr = new StreamReader(“Log.txt”);具体示例如下:
static void Main(string[] args)
{
string line;
FileStream aFile = new FileStream("Log.txt",FileMode.OpenOrCreate);
StreamReader sr = new StreamReader(aFile);
line = sr.ReadLine();
while(line != null)
{
Console.WriteLine(line);
line = sr.ReadLine();
}
sr.Close();
Console.ReadKey();
}
1、读取数据
ReadLine()方法不是在文件中访问数据的唯一方法。StreamReader类还包含许多读取数据的方法,其中最简单的是Read(),此方法将流的下一个字符作为正整数值返回,如果到达了流的结尾处,则返回-1,使用Convert类可以把这个值转换为字符,用该方法重新编写上面:
StreamReader sr = new StreamReader(aFile);
int charCode;
charCode = sr.Read();
while(charCode !=-1)
{
Console.WriteLine(Convert.ToChar(charCode));
charCode = sr.Read();
}
sr.Close();
对于小型文件,可使用一个非常简单的方法ReadToEnd(),此方法读取整个文件,并将其作为字符串返回。如:
StreamReader sr = new StreamReader(aFile);
line = sr.ReadToEnd();
Console.WriteLine(line);
sr.Close();
处理大型文件的另一个方法是.NET4中新增的静态方法File.ReadLines(),它返回IEnumerable<string>集合。可迭代这个集合中的字符串,一次读取文件中的一行。使用如下
foreach(string alternativeLine in File.ReadLines("Log.txt"))
{
Console.WriteLine(alternativeLine);
}
2、用分隔符分隔的文件
用分隔符分割的数据格式,常用String类的Split()方法将字符串转换为一个数组。具体事例如下:
private static List<Dictionary<string, string>> GetData(out List<string> columns)
{
string line;
string[] stringArray;
char[] charArray = new char[] { ',' };
List<Dictionary<string, string>> data =
new List<Dictionary<string, string>>();
columns = new List<string>();
FileStream aFile = new FileStream(@"..\..\SomeData.txt",FileMode.Open);
StreamReader sr = new StreamReader(aFile);
line = sr.ReadLine();
stringArray = line.Split(charArray);
for(int x = 0;x<=stringArray.GetUpperBound(0);x++)
{
columns.Add(stringArray[x]);
}
line = sr.ReadLine();
while(line!=null)
{
stringArray = line.Split(charArray);
Dictionary<string, string> dataRow = new Dictionary<string, string>();
for(int x = 0;x<=stringArray.GetUpperBound(0);x++)
{
dataRow.Add(columns[x],stringArray[x]);
}
data.Add(dataRow);
line = sr.ReadLine();
}
sr.Close();
return data;
}
static void Main(string[] args)
{
List<string> columns;
List<Dictionary<string, string>> myData = GetData(out columns);
foreach(string column in columns)
{
Console.Write("{0,-20}",column);
}
Console.WriteLine();
foreach(Dictionary<string,string> row in myData)
{
foreach(string column in columns)
{
Console.Write("{0,-20}", row[column]);
}
Console.WriteLine();
}
Console.ReadKey();
}
异步文件访问:
当一次性执行大量文件访问操作或者要处理非常大的文件,读写文件系统数据是很缓慢的,此时想在等待这些操作完成的同时去执行其他操作,这时就需要一步操作了,这种异步适用于FileStream、StreamWriter和StreamReader类,通常是带有Async后缀的,例如StreamReader类的ReaderLineAsync()方法。
读写压缩文件:
在处理文件时,使用压缩文件会节约大量的硬盘空间。System.IO.Compression名称空间就包含能在代码中压缩文件的类,这些类使用GZIP或者Deflate算法。但压缩文件并不只是把他们压缩一下就完事了,商业应用程序允许把多个文件放在一个压缩文件(通常称为存档文件)中。本节介绍的内容简单得多:只是把文本数据保存在压缩文件中。不能在外部实用程序中访问这个文件,但这个文件比未压缩版本要小得多。
System.IO.Compression名称空间中有两个压缩流类DeflateStream和GZipStream,它们的工作方式非常类似,对于这两个类,都要用已有的流初始化它们,对于文件,流就是FileStream对象。此后就可以把他们用于StreamReader和StreamWriter了。此外只需指定流是用于压缩(保存文件)还是解压缩(加载文件),类就知道要对传送给它的数据执行什么操作。示例如下:
static void SaveCompressedFile(string filename,string data)
{
FileStream fileStream = new FileStream(filename,FileMode.Create,FileAccess.Write);
GZipStream compressionStream = new GZipStream(fileStream,CompressionMode.Compress);
StreamWriter writer = new StreamWriter(compressionStream);
writer.Write(data);
writer.Close();
}
static string LoadCompressedFile(string filename)
{
FileStream fileStream = new FileStream(filename,FileMode.Open,FileAccess.Read);
GZipStream compressionStream = new GZipStream(fileStream,CompressionMode.Decompress);
StreamReader reader = new StreamReader(compressionStream);
string data = reader.ReadToEnd();
reader.Close();
return data;
}
static void Main(string[] args)
{
string filename = "compressedFile.txt";
string sourceString = Console.ReadLine();
StringBuilder sourceStringMultiplier = new StringBuilder(sourceString.Length*100);
for(int i = 0; i<100; i++)
{
sourceStringMultiplier.Append(sourceString);
}
sourceString = sourceStringMultiplier.ToString();
Console.WriteLine("Source data is {0} bytes long.",sourceString.Length);
SaveCompressedFile(filename, sourceString);
Console.WriteLine("\nData saved to {0}.",filename);
FileInfo compressedFileData = new FileInfo(filename);
Console.WriteLine("Compressed file is {0} bytes long.",compressedFileData.Length);
string recoveredString = LoadCompressedFile(filename);
recoveredString = recoveredString.Substring(0,recoveredString.Length/100);
Console.WriteLine("\nRecovered data: {0}",recoveredString);
Console.ReadKey();
}
序列化对象:
应用程序经常需要在硬盘上存储数据。前面介绍了逐段构建文本和数据文件,但这通常并非是最简单的方式。有时最好以对象的形式存储数据。
1、.NETFramework在System.Runtime.Serialization和System.Runtime.Serialization.Formatter名称空间中提供了序列化对象的基础架构,后者包含的名称空间中有一些具体的类实现了这个基础架构。在框架中,有一个重要的实现可用:System.Runtime.Serialization,Formatter.Binary。这个名称空间包含的BinaryFormatter类能够将对象序列化为二进制数据,也可以将二进制数据序列化为对象。
方法 | 说明 |
void Serialize(Stream stream,object source) | 把source序列化为stream |
object Deserialize(Stream stream) | 反序列化stream中的数据,返回得到的对象 |
IFormatter serializer = new BinaryFormatter(); serializer.Serialize(myStream,myObject);
反序列化同样简单:
IFormatter serializer = new BinaryFormatter(); MyObjectType myNewObject = serializer.Deserialize(myStream) as MyObjectType;
下面将实际应用这些代码:
新添加一个Product类:
public class Product
{
public long Id;
public string Name;
public double Price;
[NonSerialized]
string Notes;
public Product(long id, string name, double price, string notes)
{
Id = id;
Name = name;
Price = price;
Notes = notes;
}
public override string ToString()
{
return string.Format("{0}: {1} (${2:F2}) {3}", Id, Name, Price, Notes);
}
}
然后在Progress类的Main方法中添加:
static void Main(string[] args)
{
List<Product> products = new List<Product>();
products.Add(new Product(1,"Spily Pung",1000.0,"Goods stuff."));
products.Add(new Product(2, "Gloop Galloop Soup", 25.0, "Tasty."));
products.Add(new Product(4, "Hat Sauce", 12.0, "One for the kids."));
Console.WriteLine("Products to save:");
foreach(Product product in products)
{
Console.WriteLine(product);
}
Console.WriteLine();
IFormatter serializer = new BinaryFormatter();
FileStream saveFile = new FileStream("Products.bin", FileMode.Create, FileAccess.Write);
serializer.Serialize(saveFile,products);
saveFile.Close();
FileStream loadFile = new FileStream("Products.bin",FileMode.Open,FileAccess.Read);
List<Product> saveProducts = serializer.Deserialize(loadFile) as List<Product>;
loadFile.Close();
Console.WriteLine("Products loaded:");
foreach(Product product in saveProducts)
{
Console.WriteLine(product);
}
}
本示例创建一个Product对象集合,把集合保存到磁盘上,然后重新加载它。但第一次运行抛出异常,因为Product对象没有标记为“可序列化”。.NET Framework要求把对象标记为可序列化,才能序列化它们。这有许多原因,包括:
1、一些对象序列化的效果不佳。例如,他们需要引用只有他们本身位于内存中时才存在的本地数据。
2、一些对象包含敏感的数据,这些数据不应该以不安全的方式保存或传输到另一个进程中。
Serializable这个特性并没有由派生类继承,他必须应用于要进行序列化的每个类。NoSerialized这个属性任何成员都可使用,使用之后将不被序列化。
监控文件系统:
.NET Freamwork使用FileSystemWatcher来对文件进行监控,其过程非常简单:首先必须设置一些属性,指定监控的位置、内容以及引发应用程序要处理的事件的时间。然后给FileSystemWatcher提供定制事件处理程序的地址,当发生重要事件时,FileSystemWatcher就可以调用这些事件处理程序,最后打开FileSystemWatcher,等待事件。在启用FileSystemWatcher之前必须设置的属性如下:
属性 | 说明 |
Path | 设置要监控的文件位置或目录 |
NotifyFilter | 这是NotifyFilters枚举值的组合,NotifyFilters枚举值指定了在被监控的文件内要监控哪些内容。 这些表示要监控的文件或文件夹的属性。如果指定的属性发生了变化,就引发事件。可能的枚 举值是Attributes、CreationTime、DirectoryName、FileName、LastAccess、LastWrite、 Security和Size。注意,可通过二元OR运算符来合并这些枚举值 |
Filter | 指定要监控哪些文件的过滤器。例如,*.txt |
实例化FileSystemWatcher对象并添加监听
FileSystemWatcher watcher = new FileSystemWatcher();
watcher.Deleted += (s, e) => AddMessage("File: {0} Deleted",e.FullPath);
watcher.Renamed += (s, e) => AddMessage("File renamed from {0} to {1}",e.OldName,e.FullPath);
watcher.Changed += (s, e) => AddMessage("File: {0} {1}",e.FullPath,e.ChangeType.ToString());
watcher.Created += (s, e) => AddMessage("File: {0} Created",e.FullPath);
设置监听位置及其他监听条件:
watcher.Path = System.IO.Path.GetDirectoryName("Progress.txt");
watcher.Filter = System.IO.Path.GetFileName("Progress.txt");
watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size;
watcher.EnableRaisingEvents = true;
System.IO.Path用来处理和提取文件位置字符串中的信息,这里首先使用它通过GetDirectoryName()方法提取用户在文本框中输入的目录名称,NotifyFilter是过滤器。