无论何时运行上面的SRS程序,我们声明和初始化的任意对像(或值类型)都会“存活”在内存中。当程序结束运行后,分配程序的所有内存将会被操作系统回收,应用程序创建的所有内部状态都会失去,除非以某种方式把它们保存起来——而且是永久性地保存起来。
利用不同的API,C#提供了多种可供选择的数据持久化方案。
1、在System.Data、System.Data.Odbc和System.Data.OleDb命名空间中的编程元素,允许我们将数据保存在一个ODBC或OLE DB数据库中。
2、可以将对象输出成一种特殊的二进制形式,这种形式被称作C#序列化对象(serialized object),适用于在网络中传输。
3、也可以将信息存储为相当直截了当的、“人类可读的”ASCII码数据形式,可以是下面的其中一种:
有层次结构的数据,例如XML语言标准。在这种数据里面放入信息——“内容”——用自定义的标签来描述信息如何存放。
简单地以制表符或逗号作为分隔符的面向记录(record-oriented)的数据;在SRS应用程序中,我们展示了最基础的持久化形式——面向记录的ASCII码持久化文件;然而,同样的设计原理对其他的持久化性有效。例如:
把保存对象的具体过程封装在一个对象的方法中,客户代码无需了解细节;
保证保存对象的方法的灵活性,这样,在修改代码的具体实现时,不会影响到整个应用程序。
当发生异常时正确且漂亮地处理错误;因为数据的持久化需要与外部文件系统、数据库系统,和/或网络打交道,在此过程中很可能会出现失败的情况。
当然,文件持久化和硬币一样有两面:将对象的状态写到文件,然后再读出来。下面简单讨论这两种操作的C#是实现手法。
FileStream类
FileStream对象时C#对象的一个类型,它懂得如何打开文件,并向文件中每次写一个字节数据。
FileStream类的构造器的开头如下所示:
public FileStream(string filename,int mode)
例如,
FileStream fs = new FileStream("data.dat",FileMode.Open);
其中mode是FileMode类中定义的几个常量:
FileMode.Open:以读或写得模式打开一个文件;如果文件不存在或者无法打开,将抛出一个FileNotFoundException异常;
FileMode.Create:打开一个用于写得新文件;如果指定文件名的文件已经存在,或者因为其他原因无法创建文件——例如文件夹是写保护的——将会抛出一个IOException异常;
FileMode.Append:打开一个用于写的已存在的文件,新增数据会添加到文件的最后;如果指定文件无法找到,将会被抛出一个FileNotFoundException异常;
FileMode.CreateOrAppend:如果发现指定文件名的文件存在,则打开文件往里面些数据;否则,在文件夹中创建一个新文件。如果文件因为某些原因无法创建——例如文件夹被写保护——IOException异常将会被抛出。
从文件中读取数据
从一个ASCII 文件中逐个读取记录的基本手段,与两种C#对象类型有关——FileStream对象和StreamReader对象。
1、首先,创建一个FileStream类型对象,前面介绍过,它懂得怎样打开文件及每次从文件中读取一个字节。
2、接下来,将Filestream对象作为参数传递给一个StreamReader构造器,StreamReader是一种有效地包装了FileStream 类的复杂对象类型。StreamReader类的ReadLine方法懂得如何收集,或者说缓冲(buffer)FileStream依次读入的单个字符,直到行结束符为止,此时StreamReader将完整的一行/一条记录数据交回给客户代码。
StreamReader类也被定义在System.IO命名空间中。
下例演示了如何从一个文件中读取数据;
using System.IO;
public class IOExample
{
static void Main()
{
//声明用来读数据的对象引用
FileStream fs;
StreamReader srIn;
//读得过程需要放在try-catch块中
try{
//创建一个FileStream...
fs= new FileStream("data.dat",FileMode.Open);
//....在此FileStream基础上创建StreamReader.
srIn = new StreamReader(fs);
//从文件中读出第一行
string line = srIn.ReadLine();
// 只要此行不空,继续读!
while(line!=null)
{
//伪代码
Process the most recently read line
//读另一行(当文本结束后被设为空)
line = srIn.ReadLine();
}
// 关闭StreamReader,这样FileStream也会关闭
srIn.Close();
}
catch(IOException ioe){
执行异常操作...细节从略
}
}
}
对上例的解释:
1、因为在文件I/O操作是极有可能发生错误——要开发的文件可能不存在,要写得数据文件可能只读,等等——必须将代码放在一个try代码中,并提供处理潜在IOException异常的捕获代码;
2、我们打算从一个已存在文件中读数据,因此把FileMode.Open常量传入FileStream构造器:
fs = new FileStream("data.dat",FileMode.Open);
3、将FileStream作为参数传递给StreamReader的一个新实体,从而能够用StreamReader类中的ReadLine方法从文件中一次读取一行:
srIn= new StreamReader(fs);
4、使用StreamReader的ReadLine方法,从文件中每次读取一行数据/记录:
string line = stIn.ReadLine();
只要该方法没有返回一个null 值 ——null值表示到达文件尾——就可以继续从文件中读取一条合法数据:
//只要次行不为空,继续读
while (line !=null) {...}
5、在while循环中,必须记得读取文件的下一条记录,这样才能保证不进入死循环:
line = stIn.ReadLine();
6、最后,必须记得关闭StreamReader对象,这样同时会关闭FileStream对象;stIn.Close();
在读取完成后关闭StreamReader 非常重要,因为:
这样做,文件才不会对后续访问保持打开/上锁状态;
这样做,整个应用程序才不会超过(与平台有关的)最大可打开文件数限制。
这样做,能够释放不使用对象,让它们进入垃圾回收过程。
向文件写数据
往一个ASCII文件中写记录的一种C#实现手段,和读文件类似。
1、再次创建一个FileStream对象:在构造器中指明合适的打开模式;FileStream可以用来覆盖原文件(FileMode.Open),在已存在文件尾部添加(FileMode.Append),创建一个新文件(FileMode.Create),或者合并“打开”和“创建”模式(FileMode.OpenOrCreate)。
2、接着,将FileStream对象作为参数传入StreamWriter构造器。StreamWriter是一个包装FileStream的复杂对象类型。StreamWriter类的WriteLine方法知道如何传递一条完整的记录/一行数据,然后通过其封装的FileStream对象每次向文件写一个字节。
StreamWriter类从Textwriter类继承了WriteLine方法。它的工作方式与你已经熟知的Console.WriteLine方法类似,唯一的区别是前者会将文本输出到文件中,而后者显示在命令窗口。Console.WriteLine还定义了一个Write方法,和Console.Write方法类似。
StreamWriter类被定义在System.IO命名空间中。
using System.IO;
public class IOExample2
{
static void Main(){
FileStream fs;
StreamWriter sw;
//写操作应该放在try-catch代码中
try{
//创建一个FileStream..
fs = new FileStream("data.dat",FileMode.OpenOrCreate);
//...在此FileStream基础上创建StreamWriter.
sw = new StreamWriter(fs);
//伪代码
while(still want to print more){
sw.WriteLine(Whatever string data we wish to output);
sw.Close();
}
catch(IOException ioe)
{
//执行异常操作。
}
}
}
}