代码改变世界

路径,文件,目录,I/O常见操作汇总(三)

2007-01-03 19:11 Anders Cui 阅读(...) 评论(...) 编辑 收藏

    摘要:
    文件操作是程序中非常基础和重要的内容,而路径、文件、目录以及I/O都是在进行文件操作时的常见主题,这里想把这些常见的问题作个总结,对于每个问题, 尽量提供一些解决方案,即使没有你想要的答案,也希望能提供给你一点有益的思路,如果你有好的建议,恳请能够留言,使这些内容更加完善。
    主要内容:
    一、路径的相关操作, 如判断路径是否合法,路径类型,路径的特定部分,合并路径,系统文件夹路径等内容;
    二、相关通用文件对话框,这些对话框可以帮助我们操作文件系统中的文件和目录;
    三、文件和目录操作,如复制、移动、删除、重命名,文件的版本信息,文件判等、搜索,读写文件等;
    四、读写文件,对文件系统的监视;
    五、其它,如临时文件,随机文件名等;

    第一篇-路径的相关操作和通用文件对话框的使用

    第二篇-文件和目录的相关操作

    这一篇将介绍第四、五部分。

    文件读写相关类介绍:
    文件读写操作涉及的类主要是:
    MarshalByRefObject 类:允许在支持远程处理的应用程序中跨应用程序域边界访问对象;
    BinaryReader 类:用特定的编码将基元数据类型读作二进制值。
    BinaryWriter 类: 以二进制形式将基元类型写入流,并支持用特定的编码写入字符串。
    Stream 类: 提供字节序列的一般视图。
    FileStream类:公开以文件为主的 Stream,既支持同步读写操作,也支持异步读写操作。
    MemoryStream 类:创建其支持存储区为内存的流。
    BufferedStream 类:给另一流上的读写操作添加一个缓冲层。
    TextReader 类:表示可读取连续字符系列的阅读器。
    TextWriter 类:表示可以编写一个有序字符系列的编写器。
    StreamReader 类:实现一个 TextReader,使其以一种特定的编码从字节流中读取字符。
    StreamWriter 类:实现一个 TextWriter,使其以一种特定的编码向流中写入字符。
    StringReader 类:实现从字符串进行读取的 TextReader。
    StringWriter 类:实现一个用于将信息写入字符串的 TextWriter。该信息存储在基础StringBuilder中。
    在使用它们之前最好能了解它们的继承关系,有助于作出最合适的选择:

    另外还要注意一下FileInfo和File类的一些方法,如Create,CreateText,Open等,有时也会带来方便。
    这些类的内容比较繁多,更多内容还请参考MSDN。 

    下面是一些常见的问题及其解决方案:
    问题1:如何读写文本文件(并考虑不同的编码类型)
    解决方案:
    创建一个FileStream对象用以引用该文件。要写入文件,将FileStream对象封装在StreamWriter对象中,使用其重载了的Write方法;要读取文件,将FileStream对象封装在StreamReader对象中,使用其Read或ReadLine方法;
    .NET Framework允许通过StreamWriter和StreamReader类操作任何流来读写文本文件。当使用StreamWriter类写入数据时,调用它的Write方法,该方法在重载后可以支持所有常见的C#数据类型,包括字符串、字符、整数、浮点数以及十进制数等。但Write方法总会将的得到的数据转换为文本,如果希望将这些文本转换回原来的数据类型,应使用WriteLine方法,以确保每个值都处于单独的一行上。
    字符串的表现形式取决于你使用的编码,最常见的编码类型包括下面几种:ASCII,UTF-16,UTF-7,UTF-8。
    .NET Framework在System.Text命名空间中为每种编码类型提供了一个类。在使用StreamWriter和StreamReader类时,可以指定需要的编码类型,或者使用默认的UTF-8。
    而在读取文本文件时,则要使用StreamReader类的Read或ReadLine方法。Read方法读取单个字符或者指定个数的字符,返回类型为字符或字符数组;ReadLine方法则返回包含整行内容的字符串;ReadToEnd方法从当前位置读取至流的结尾。
    (更多内容还请参考MSDN)
    写入文本文件的示例:

    using (FileStream fs = new FileStream(fileName, FileMode.Create))
    {
        
// 创建一个StreamWriter对象,使用UTF-8编码格式
        using (StreamWriter writer = new StreamWriter(fs, Encoding.UTF8))
        {
            
// 分别写入十进制数,字符串和字符类型的数据
            writer.WriteLine(123.45M);
            writer.WriteLine(
"String Data");
            writer.WriteLine(
'A');
        }
    }
   
    读取文本文件的示例:
    // 以只读模式打开一个文本文件
    using (FileStream fs = new FileStream(fileName, FileMode.Open))
    {
        
using (StreamReader reader = new StreamReader(fs, Encoding.UTF8))
        {
            
string text = string.Empty;

            
while(!reader.EndOfStream)
            {
                text 
= reader.ReadLine();
                txtMessage.Text 
+= text + Environment.NewLine;
            }
        }
    }

    问题2:如何读写二进制文件(使用强数据类型)
    解决方案:
    创建一个FileStream对象用以引用该文件。要写入文件,将FileStream对象封装在BinaryWriter对象中,使用其重载了的Write方法;要读取文件,将FileStream对象封装在BinaryReader对象中,使用相应数据类型的Read方法。             
    .NET Framework允许通过BinaryWriter和BinaryReader类操作任何流来读写二进制数据。当使用BinaryWriter类写入数据时,调用它的Write方法,该方法在重载后可以支持所有常见的C#数据类型,包括字符串、字符、整数、浮点数以及十进制数等,然后数据会被编码为一系列字节写入文件,也可以配置该过程中的编码类型。
    在使用二进制文件时,一定要特别注意其中的数据类型。当你读取数据时,一定要使用BinaryReader类的某种强类型的Read方法。例如,要读取字符串,要使用ReadString方法。(BinaryWriter在写入二进制文件时总会记录字符串的长度以避免任何可能的错误)
    写入文件的示例:
    using (FileStream fs = new FileStream(fileName, FileMode.Create))
    {
        
using (BinaryWriter writer = new BinaryWriter(fs))
        {
            
// 写入十进制数,字符串和字符
            writer.Write(234.56M);
            writer.Write(
"String");
            writer.Write(
'!');
        }
    }

    读取文件的示例:
    // 以只读模式打开一个二进制文件
    using (FileStream fs = new FileStream(fileName, FileMode.Open))
    {
        
using (StreamReader sr = new StreamReader(fs))
        {
            MessageBox.Show(
"全部数据:" + sr.ReadToEnd());

            fs.Position 
= 0;
            
using (BinaryReader reader = new BinaryReader(fs))
            {
                
// 选用合适的数据类型读取数据
                string message = reader.ReadDecimal().ToString() + Environment.NewLine;
                message 
+= reader.ReadString() + Environment.NewLine;
                message 
+= reader.ReadChar().ToString();
                MessageBox.Show(message);
            }
        }
    }

    问题3:如何异步读取文件;
    解决方案:
    有时你需要读取一个文件但又不希望影响程序的执行。常见的情况是读取一个存储在网络驱动器上的文件。         
    FileStream提供了对异步操作的基本支持,即它的BeginRead和EndRead方法。使用这些方法,可以在.NET Framework线程池提供的线程中读取一个数据块,而无须直接与System.Threading命名空间中的线程类打交道。
    采用异步方式读取文件时,可以选择每次读取数据的大小。根据情况的不同,你可能会每次读取很小的数据(比如,你要将数据逐块拷贝至另一个文件),也可能是一个相对较大的数据(比如,在程序逻辑开始之前需要一定数量的数据)。在调用BeginRead时指定要读取数据块的大小,同时传入一个缓冲区(buffer)以存放数据。因为BeginRead和EndRead需要访问很多相同的信息,如FileStream,buffer,数据块大小等,因此将这些内容封装一个单独的类当中是一个好主意。
    下面这个类就是一个简单的示例。AsyncProcessor类提供了StartProcess方法,调用它开始读取,每次读取操作结束,OnCompletedRead回调函数会被触发,此时可以处理数据,如果还有剩余数据,则开始一个新的读取操作。默认情况下,AsyncProcessor类每次读取2KB数据。
    class AsyncProcessor
    {
        
private Stream inputStream;

        
// 每次读取块的大小
        private int bufferSize = 2048;

        
public int BufferSize
        {
            
get { return bufferSize; }
            
set { bufferSize = value; }
        }

        
// 容纳接收数据的缓存
        private byte[] buffer;

        
public AsyncProcessor(string fileName)
        {
            buffer 
= new byte[bufferSize];

            
// 打开文件,指定参数为true以提供对异步操作的支持
            inputStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, true);
        }

        
public void StartProcess()
        {
            
// 开始异步读取文件,填充缓存区
            inputStream.BeginRead(buffer, 0, buffer.Length, OnCompletedRead, null);
        }

        
private void OnCompletedRead(IAsyncResult asyncResult)
        {
            
// 已经异步读取一个 块 ,接收数据
            int bytesRead = inputStream.EndRead(asyncResult);

            
// 如果没有读取任何字节,则流已达文件结尾
            if (bytesRead > 0)
            {
                
// 暂停以模拟对数据块的处理
                Debug.WriteLine("  异步线程:已读取一块");
                Thread.Sleep(TimeSpan.FromMilliseconds(
20));

                
// 开始读取下一块
                inputStream.BeginRead(buffer, 0, buffer.Length, OnCompletedRead, null);
            }
            
else
            {
                
// 结束操作
                Debug.WriteLine("  异步线程:读取文件结束");
                inputStream.Close();
            }
        }
    }

    使用该类时可以这么写:
    // 开始在另一线程中异步读取文件
    AsyncProcessor asyncIO = new AsyncProcessor("test.txt");
    asyncIO.StartProcess();

    
// 在主程序中,做其它事情,这里简单地循环10秒
    DateTime startTime = DateTime.Now;
    
while (DateTime.Now.Subtract(startTime).TotalSeconds < 10)
    {
        Debug.WriteLine(
"主程序:正在进行");
        
// 暂停线程以模拟耗时的操作
        Thread.Sleep(TimeSpan.FromMilliseconds(100));
    }

    Debug.WriteLine(
"主程序:已完成");
    
    问题4:如何创建临时文件
    解决方案:
    有时需要在特定用户的临时目录下创建一个临时文件,这要求该文件具有唯一的名称,避免与其它程序生成的临时文件相冲突。我们会有多种选择。最简单的是,在程序所在目录内使用GUID或时间戳加上随机值作为文件名称。但Path类提供的方法还是可以为你节省工作量,这就是它的静态GetTempFileName方法,它在当前用户的临时目录下创建一个临时文件(文件名称一定是唯一的),临时目录通常类似于这样:C:\Documents and Settings\[username]\Local Settings\Temp。
    string tempFile = Path.GetTempFileName();

    
using (FileStream fs = new FileStream(tempFile, FileMode.Open))
    {
        
using (BinaryWriter writer = new BinaryWriter(fs))
        {
            
// 写入数据
            writer.Write("临时文件信息");
        }
    }

    
// Do something

    
// 最后删除临时文件
    File.Delete(tempFile);

    问题5:如何获得随机文件名
    解决方案:
    使用Path.GetRandomFileName方法,它与GetTempFileName方法的不同之处在于它仅仅返回一个字符串但不会创建文件。

    问题6:监视文件系统的变化
    解决方案:
    如果指定路径内的文件发生改变(比如文件被修改或创建),你希望能对此作出反应。
    如果程序与其它多个程序或业务处理相关,常常需要创建一个程序,并且只有文件系统发生变化时它才处于活动状态。你可以创建一个这样的程序,让它定期区检测指定目录,此时会发现有件事情让你苦恼:检测得越频繁,就会浪费越多的系统资源;而检测得越少,那么检测到变化的时间就会越长。
    这时可以使用FileSystemWatcher组件,指定要进行监视的目录或文件,并处理其Created,Deleted,Renamed,Changed事件。
    要使用FileSystemWatcher组件,首先要创建它的一个实例,然后设置下列属性:
    Path:指定要监视的目录;
    Filter:指定要监视的文件类型,如”*.txt”;
    NotifyFilter:指定要监视的变化类型;
    FileSystemWatcher会引发四个关键的事件:Created,Deleted,Renamed,Changed。这些事件都在其FileSystemEventArgs参数中提供了相关文件的信息:如文件名,路径,改变类型,Renamed事件中还可以了解到改变前的文件名和路径。如果要禁用这些事件,则将它的EnableRaisingEvents属性设置为false。Created,Deleted,Renamed三个事件比较容易处理,但Changed事件就得当心了,你需要设置它的NotifyFilter属性以指示监视那些类型的变化。否则,程序会在文件被修改时淹没在不断发生的事件中(缓存区溢出)。

    // 设置相关属性
    watcher.Path = appPath;
    watcher.Filter 
= "*.txt";
    watcher.IncludeSubdirectories 
= true;

    
// 添加事件处理函数
    watcher.Created += new FileSystemEventHandler(OnChanged);
    watcher.Deleted 
+= new FileSystemEventHandler(OnChanged);
    watcher.Changed 
+= new FileSystemEventHandler(OnChanged);
    watcher.Renamed 
+= new RenamedEventHandler(OnRenamed);

    void OnRenamed(object sender, RenamedEventArgs e)
    {
        
string renamedFormat = "File: {0} 被重命名为 :{1}";
        txtChangedInfo.Text 
= string.Format(renamedFormat, e.OldFullPath, e.FullPath);
    }

    
void OnChanged(object sender, FileSystemEventArgs e)
    {
        
// 显示通知信息
        txtChangedInfo.Text = "文件: " + e.FullPath + "发生改变" + Environment.NewLine;
        txtChangedInfo.Text 
+= "改变类型: " + e.ChangeType.ToString();
    }

    问题7:如何使用独立存储文件
    解决方案:
    有时你需要将数据存储在文件中,但对本地硬盘驱动器却没有必要的权限(FileIOPermission)。这时要用到System.IO.IsolatedStorage命名空间中的类,这些类允许你的程序在特定用户的目录下将数据写入文件而不需要直接访问硬盘驱动器的权限:

    // 创建当前用户的独立存储
    using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly())
    {
        
// 创建一个文件夹
        store.CreateDirectory("MyFolder");

        
// 创建一个独立存储文件
        using (Stream fs = new IsolatedStorageFileStream("MyFile.txt", FileMode.Create, store))
        {
            StreamWriter writer 
= new StreamWriter(fs);
            writer.WriteLine(
"Test Line!");
            writer.Flush();
        }

        Debug.WriteLine(
"当前大小:" + store.CurrentSize.ToString() + Environment.NewLine);
        Debug.WriteLine(
"范围:" + store.Scope.ToString() + Environment.NewLine);
        
string[] files = store.GetFileNames("*.*");
        
if (files.Length > 0)
        {
            Debug.WriteLine(
"当前文件:" + Environment.NewLine);
            
foreach (string file in files)
            {
                Debug.WriteLine(file 
+ Environment.NewLine);
            }
        }
    }

    注意:本文部分内容为作示例都作了简化,所以肯定会有不合理之处,仅希望能为您提供一些线索和思路。在使用前还请多多参考相关资料。

 

                                                                                         Anders Cui

   参考:

  • Apress-Visual C# 2005 Recipes A Problem Solution Approach
  • MSDN