C# Notizen 13 文件和流

.NET Framework将文件视为数据流。流是一系列用字节表示的数据分组。数据流有底层存储介质,这些存储介质通常称为支撑存储空间(backing store),它们为流提供了源。所幸的是,.NET Framework提供了File、Directory和Path类,这使得处理文件和目录更容易。
命名空间 System.IO 包含处理缓冲流和非缓冲流所需的所有类。缓冲流让操作系统创建内部缓冲区,并使用它以效率最高的增量读写数据。
本章介绍如何处理文件;使用File、Directory和Path类来查看和管理文件系统以及读写文件;还将介绍如何使用Stream类及其派生类来执行复杂的读写操作。

一、文件和目录
可将文件视为字节序列,它有明确的名称和固定的支撑存储介质。要操作文件,需要使用路径、磁盘存储空间、文件名和目录名。.NET Framework在命名空间System.IO中提供了多个类,能够轻松地处理文件。
1.1 使用路径
路径是一个字符串,指出了文件或目录的位置,可包含绝对位置信息,也可包含相对位置信息。绝对路径指定了完整的位置,而相对路径只指定了部分位置。使用相对路径查找指定的文件时,将以当前位置为起点。
ps:当前位置
每个进程都有进程级当前位置,这通常是加载进程的位置,但并非总是如此。
Path类提供了一些静态方法,让您能够以跨平台的方式处理路径字符串。虽然Path类的大多数成员都不与文件系统交互,但是验证指定路径字符串是否包含有效的字符。
下表列出了Path类中常用的方法:

方法 描述
ChangeExtension 修改扩展名
Combine 将多个字符串合并成路径
GetDirectoryName 获取指定路径中的目录
GetExtension 获取指定路径中的扩展名
GetFileName 获取指定路径中的文件名和扩展名
GetFlieNameWithoutExtension 获取指定路径中的文件名,但不包含扩展名
GetPathRoot 获取指定路径的根目录
GerRandomFileName 获取一个随机名称
GetTempFileName 穿件一个随机命名且唯一的临时文件,并返回该文件的完整路径
GetTempPath 获取指向临时文件夹的路径

1.2 特殊目录
Windows操作系统包含很多经常被应用程序使用的特殊文件夹。通常,这些文件夹是由操作系统指定的,但安装Windows操作系统时,用户也可显式地指定它们。因此,这些文件夹的名称和位置可能随计算机而异。
要确定这些特殊文件夹(如 Windows 文件夹)的路径,最合适的方式是使用方法Environment.GetFolderPath。这个方法接受一个Environment.SpecialFolder枚举值作为参数,该参数指出了要获取哪个特殊文件夹的路径。
下表列出了一些常用的Environment.SpecialFolder枚举值

枚举值 描述
ApplicationData 用于存储当前漫游用户使用的应用程序数据的目录
CommonApplicationData 用于存储所有用户共用的应用程序数据的目录
LocalApplicationData 用于存储当前非漫游用户使用的应用程序数据的目录
CommonDocuments 包含所有用户共用文档的文件系统目录
Desktop 逻辑桌面,而不是物理文件系统位置
DesktopDirectory 实际存储桌面文件对象的目录,不要将其与虚拟文件夹Desktop混为一谈
MyDocuments “我的文档”文件夹,与Personal等价
Personal 用作文档仓库的目录
System System目录
Windows Windows或SYSROOT目录,对应于环境变量%windir%或%SYSTEMROOT%

1.3 DirectoryInfo和FileInfo类
DirectoryInfo和FileInfo类都是从FileSystemInfo派生而来的,而FileSystemInfo类可表示文件或目录,包含用于操作文件和目录的方法。实例化 FileSystemInfo 类时,将缓存目录或文件信息,因此必须使用Refresh方法进行刷新,以确保信息是最新的。
DirectoryInfo 的实例成员提供了很多属性和执行常见操作(如复制、移动、创建和列举目录)的方法。
下表列出了DirectoryInfo类的常用方法和属性:

成员 描述
Create 创建目录
CreateSubdirectory 在指定路径中创建子目录
Delete 删除当前目录,并可指定是否要删除其中的所有文件和子目录
EnumerateDirectories 获取一个可枚举集合,其中包含当前目录中的目录信息
EnumerateFiles 获取一个可枚举集合,其中包含当前目录中的文件信息
EnumerateFileSystemInfos 获取一个可枚举集合,其中包含当前目录中的文件和目录信息
Exists 指出磁盘中是否存在当前目录
FullName 获取当前目录的完整路径
MoveTo 将当前目录(包括其中的所有文件和子目录)移到指定的位置
Name 获取当前目录的名称
Parent 获取当前目录的父目录
Refresh 刷新缓存的目录信息
Root 获取路径的根目录部分

如下程序演示了如何使用DirectoryInfo类执行一些常见的操作:

public class DirectoryInfoExample
{
    public static void Main()
    {
        string tempPath = Path.GetTempFileName();

        DirectoryInfo directoryInfo = new DirectoryInfo(tempPath);
        try
        {
            if (directoryInfo.Exists)
            {
                Console.WriteLine("The directory already exists.");
            }
            else
            {
                directoryInfo.Create();
                Console.WriteLine("The directory was successfully created.");
                directoryInfo.Delete();
                Console.WriteLine("The directory was deleted.");
            }
        }
        catch (IOException e)
        {
            Console.WriteLine("An error occurred:{0}", e.Message);
        }
    }
}

FileInfo类包含的实例成员提供了大量属性和执行常见文件操作(如复制、移动、创建和打开文件)的方法。
下表列出了常用的方法和属性:

成员 描述
AppendText 创建一个StreamWriter,用于在当前文件末尾追加文本
Attributes 获取或设置当前文件的特性
CopyTo 将当前文件复制到新文件
Create 创建文件
CreateText 创建或打开一个文件,以便向其中写入文本
Delete 删除当前文件
Directory 获取父目录
DirectoryName 获取父目录的名称
Exists 判断磁盘中是否存在当前文件
Extension 当前文件的扩展名
FullName 获取当前文件的完整路径
IsReadOnly 获取或设置一个值,这个值决定了当前文件是否是只读的
Length 当前文件的长度
MoveTo 将当前文件移到指定的位置
Name 获取当前文件的名称
Open 打开一个文件
OpenRead 打开一个现有的文件以便读取
OpenText 打开一个现有的文本文件以便读取
OpenWrite 打开一个现有的文件以便写入
Refresh 刷新缓存的文件信息
Replace 使用当前文件的内容替换指定文件的内容

如下程序演示了如何使用FileInfo类执行一些常见的操作:

public class FileInfoExample
{
    public static void Main()
    {
        string tempFile = Path.GetTempFileName();

        FileInfo fileInfo = new FileInfo(tempFile);
        try
        {
            if (!fileInfo.Exists)
            {
                using (StreamWriter writer = fileInfo.CreateText())
                {
                    writer.WriteLine("Line 1");
                    writer.WriteLine("Line 2");
                }
            }

            fileInfo.CopyTo(Path.GetTempFileName());
            fileInfo.Delete();
        }
        catch (IOException e)
        {
            Console.WriteLine("An error occurred: {0}", e.Message);
        }
    }
}

ps:流是可释放的
使用完流后,务必调用Close方法释放其占用的资源。也可将流放在一条using语句中,这是一种更好的方法,可确保流会被正确地关闭。

1.4 Directory和File类
如果不想创建DirectoryInfo和FileInfo类的实例,那么可以使用Directory和File类。这些类只提供了静态方法,用于执行DirectoryInfo和FileInfo支持的目录和文件操作。

下表列出了Directory类的常用方法:

成员 描述
CreateDirectory 创建指定路径中的所有目录
Delete 删除指定的目录
EnumerateDirectories 获取一个可枚举集合,其中包含指定路径中的目录名
EnumerateFiles 获取一个可枚举集合,其中包含指定路径中的文件名
EnumerateFileSystemEntries 获取一个可枚举集合,其中包好指定路径中所有文件和子目录的名称
Exists 指出指定的路径是否存在于磁盘中
GetCurrentDirectory 获取当前工作目录
GetDirectoryRoot 获取当前路径的卷信息、根信息或两者
GetLogicalDrives 获取当前计算机中的逻辑驱动器的名称
GetParent 获取指定路径的父目录
Move 将文件或目录(包括其中的所有文件和子目录)移到指定位置

如下程序执行的一些操作,但使用的是Directory类,而不是DirectoryInfo类:

public class DirectoryInfoExample
{
    public static void Main()
    {
        string tempPath = Path.GetTempFileName();

        try
        {
            if (DirectoryInfo.Exists(tempPath))
            {
                Console.WriteLine("The directory already exists.");
            }
            else
            {
                DirectoryInfo.CreateDircectory(path);
                Console.WriteLine("The directory was successfully created.");
                DirectoryInfo.Delete(path);
                Console.WriteLine("The directory was deleted.");
            }
        }
        catch (IOException e)
        {
            Console.WriteLine("An error occurred:{0}", e.Message);
        }
    }
}

Directory 和 DirectoryInfo 类之间的一个重要差别体现在方法 EnumerateFiles、EnumerateDirectories和EnumerateFileSystemEntries。在Directory类中,这些方法返回一个包含目录和文件名的 IEnumerable<string>,而在 DirectoryInfo 类中,这些方法分别返回IEnumerable<FileInfo>、IEnumerable<DirectoryInfo>和IEnumerable<FileSystemInfo>。

下表列出了File类的常用方法

成员 描述
AppendAllLines 在文件末尾追加文本行,然后关闭文件
AppendAllText 将指定的字符串追加到文件末尾,如果文件不存在,就创建它
AppendText 创建一个StreamWriter,用于将文本追加到当前文件末尾
Copy 将现有文件复制到新文件
Create 创建一个文件
CreateText 创建或打开一个文件,以便写入文本
Delete 删除指定的文件
Exists 确定指定的文件是否存在于磁盘中
GetAttributes 获取指定文件的特性
Move 将指定文件移到新位置
OpenRead 打开一个现有的文件以便读取
OpenText 打开一个现有的文本文件以便读取
OpenWrite 打开一个现有的文件以便写入
ReadAllBytes 打开一个二进制文件,将其内容读取到一个字节数组中,然后关闭该文件
ReadAllLines 打开一个文本文件,将所有行都读取到一个字符串中,然后关闭该文件
ReadAllText 打开一个文本文件,将所有行都读取到一个字符串中,然后关闭该文件
ReadLines 读取文件中的行
Replace 使用其他文件的内容替换指定文件的内容
SetAttributes 设置指定文件的特性
WriteAllBytes 创建一个新文件,将指定字节写入其中,然后关闭该文件
WriteAllLines 创建一个新的文本文件,将一个或多个字符串写入其中,然后关闭该文件
WriteAllText 创建一个新文件,将指定字符串写入其中,然后关闭该文件

以下示例使用了使用File类

public class FileExample
{
    public static void Main()
    {
        string tempFile = Path.GetTempFileName();

        try
        {
            if (!File.Exists(tempFile))
            {
                using (StreamWriter writer = File.CreateText(tempFile))
                {
                    writer.WriteLine("Line 1");
                    writer.WrtieLine("Line 2");
                }
            }

            File.Copy(tempFile, Path.GetTempFileName());
            File.Delete(tempFile);
        }
        catch (IOException e)
        {
            Console.WriteLine("An error occurred: {0}", e.Message);
        }
    }
}

二、读写数据
要处理文件中的数据,无论是读取还是写入,都可以使用 Stream 类表示的流。.NET Framework中所有基于类的流都是从这个类派生而来的。
下表列出了Stream类的常用成员:

成员 描述
CanRead 之处当前流是否支持读取
CanWrite 之处当前流是否支持写入
Close 关闭当前流
CopyTo 将当前流的内容复制到另一个流中
Flush 清空所有缓冲区,并将缓冲区的数据都写入支持存储介质
Read 从当前流中读取字节序列
Write 将一系列字节写入当前流中

2.1 二进制文件
不确定文件内容的类型时,通常最好将其视为二进制文件,这种文件不过是一个字节流。要从二进制文件中读取数据,可使用File类的静态方法OpenRead,它返回一个FileStream:

FileStream input = File.OpenRead(Path.GetTempFileName());

然后,可对该FileStream调用Read方法,将数据读取到指定的缓冲区中。缓冲区(buffer)是一个字节数组,可用于存储 Read 方法返回的数据。传递缓冲区、要读取的字节数以及数据存储位置相对缓冲区开头的偏移量后,Read方法将从支持存储区读取指定数量的字节,将其存储到缓冲区,并返回实际读取的字节数:

byte[] buffer = new byte[1024];
int bytesRead = input.Read(buffer, 0, 1024);

当然,从流中读取数据并非能执行的唯一操作。将二进制数据写入流也是一种常见的操作,完成这种操作的方式与读取数据类似。首先使用File类的OpenWrite方法打开一个二进制文件,以便写入,然后对返回的FileStream调用方法Write,将缓冲区中的数据写入支持存储区。调用 Write 方法时,需要传递如下参数:包含要写入的数据的缓冲区、从离缓冲区开头多远的地方开始读取以及写入多少个字节,如下所示。

FileStream output = File.OpenWrite(Path.GetTempFileName());
output.Write(buffer, 0, bytesRead);

如下代码是一个完整的示例,它从一个二进制文件中读取数据,并将数据写入另一个二进制文件。这个示例不断地读写字节,直到Read方法返回0,这表明再没有字节可供读取了。

public class BinaryReaderWriter
{
    const int BufferSize = 1024;

    public static void Main()
    {
        string tempPath = Path.GetTempFileName();
        string tempPath2 = Path.GetTempFileName();

        if (File.Exsists(tempPath))
        {
            using (FileStream input = FileExample.OpenRead(tempPath))
            {
                byte[] buffer = new byte[BufferSize];
                int bytesRead;

                using (FileStream output = File.OpenWrite(tempPath2))
                {
                    while ((bytesRead = input.Read(buffer, 0, BufferSize)) > 0)
                    {
                        output.Write(buffer, 0, bytesRead);
                    }
                }
            }
        }
    }
}

2.2 缓冲流
在前一个示例中使用FileStream时,需要指定用于读取数据的缓冲区以及该缓冲区的大小。在很多情况下,如果让操作系统判断要读取多少个字节,效率可能更高。
BufferedStream 让操作系统创建内部缓冲区,并以它认为效率最高的增量填充缓冲区。它仍以您提供的增量对您提供的缓冲区进行填充,但填充时使用内部缓冲区的数据,而不直接使用支持存储区中的数据。要创建缓冲流,可使用Stream创建一个新的BufferedStream实例,如下所示:

public class BinaryReaderWriter
{
    const int BufferSize = 1024;

    public static void Main()
    {
        string tempPath = Path.GetTempFileName();
        string tempPath2 = Path.GetTempFileName();

        if (File.Exsists(tempPath))
        {
            using (BufferedStream input = new BufferedStream(File.OpenRead(tempPath)))
            {
                byte[] buffer = new byte[BufferSize];
                int bytesRead;

                using (BufferedStream output = new BufferedStream(File.OpenWrite(tempPath2)))
                {
                    while ((bytesRead = input.Read(buffer, 0, Buffersize)) > 0)
                    {
                        output.Write(buffer, 0, bytesRead);
                    }
                }
            }
        }
    }
}

2.3 文本文件
Stream实例不仅可指向二进制文件,还可指向文本文件,并可使用其Read和Write方法来读写数据。读写字节数组,而不是字符串,这样比较不方便。为简化文本文件的处理工作,.NET Framework提供了StreamReader和StreamWriter类。
StreamReader提供了一个Read方法,它每次读取一个字符;还提供了一个ReadLine方法,它每次读取一行字符,并返回一个字符串。行是一系列字符,以换行符(\n)、回车(\r)或换行和回车(\r\n)结尾。到达输入流末尾后,ReadLine将返回null;否则返回一行字符,但不包含行尾字符。要写入文本数据,可使用StreamWriter类的WriteLine方法。
如下是一个读写文本数据的示例:

public class TextReaderWriter
{
    public static void Main()
    {
        string tempPath = Path.GetTempFileName();
        string tempPath2 = Path.GetTempFileName();

        if (File.Exsists(tempPath))
        {
            using (StreamReader reader = new File.OpenText(tempPath))
            {
                string buffer = null;
                using (StreamWriter writer = new StreamWriter(tempPath2))
                {
                    while ((buffer = reader.ReadLine()) !=null)
                    {
                        writer.WriteLine(buffer);
                    }
                }
            }
        }
    }
}

2.4 使用File类读写数据
鉴于从文件(无论是文本还是二进制文件)读写数据是一种常见任务,因此File类提供了多个方法,使得这项任务比直接使用流更方便。
要读写二进制数据,可分别使用方法ReadAllBytes和WriteAllBytes。这些方法打开文件,读取或写入字节,然后关闭文件。
如下程序使用的是方法ReadAllBytes和WriteAllBytes:

public class TextReaderWriter
{
    public static void Main()
    {
        string tempPath = Path.GetTempFileName();
        string tempPath2 = Path.GetTempFileName();

        if (File.Exsists(tempPath))
        {
            byte[] data = File.ReadAllBytes(tempPath);
            File.WriteAllBytes(tempPath2, data);
        }
    }
}

读写文本数据也很容易。要读取文本数据,可使用方法ReadAllLines或ReadAllText;要写入文本数据,可使用方法WriteAllLines或WriteAllText。方法ReadAllLines将文件中的所有行都读入一个字符串数组中,其中每一行都是该数组的一个元素;而 ReadAllText 将所有行都读入一个字符串中。
方法WriteAllLines将字符串数组的每个元素都写入文件,而WriteAllText将一个字符串的内容写入文件。如果指定的文件存在,那么这两个方法都覆盖它;否则,创建一个新文件。要在现有文件末尾追加文本,可使用方法 AppendAllLines 或 AppendAllText;如果需要打开一个流,就可使用方法AppendText。

如下程序使用方法ReadAllLines和WriteAllLines

public class TextReaderWriterFile
{
    public static void Main()
    {
        string tempPath = Path.GetTempFileName();
        string tempPath2 = Path.GetTempFileName();

        if (File.Exsists(tempPath))
        {
            string[] data = File.ReadAllLines(tempPath);
            File.WriteAllLines(tempPath2, data);
        }
    }
}

使用方法ReadAllLines或ReadAllText的缺点是,首先必须将整个文件读入内存。为解决这个问题,可使用方法ReadLines,它返回一个IEnumerable<string>集合。由于这个方法返回一个 IEnumerable<string>集合,因此可在返回整个集合前就开始枚举它。
如下程序使用了WriteAllLines和ReadLines:

public class TextReaderWriterFile
{
    public static void Main()
    {
        string tempPath = Path.GetTempFileName();
        string tempPath2 = Path.GetTempFileName();

        if (File.Exsists(tempPath))
        {
            File.WriteAllLines(tempPath, File.ReadLines(tempPath2));
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 转自 http://www.ibm.com/developerworks/cn/education/java/j-...
    抓兔子的猫阅读 2,298评论 0 22
  • tags:io categories:总结 date: 2017-03-28 22:49:50 不仅仅在JAVA领...
    行径行阅读 2,172评论 0 3
  • IO简单概述 IO解决问题 : 解决设备与设备之间的数据传输问题(硬盘 -> 内存 内存 -> 硬盘) 读和写文...
    奋斗的老王阅读 3,430评论 0 53
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,220评论 11 349
  • 版本更新其实很简单的,比第一次上线步骤差不多.但是对于很多没做过更新的小伙伴来说,第一次去更新还是有点胆怯.这是我...
    飞鱼YP阅读 1,466评论 2 13