IO 文件操作

  • 文章来源:

0. 前言

本章节是IO篇的第二集,我们在上一篇中介绍了C#中IO的基本概念和一些基本方法,接下来我们介绍一下操作文件的方法。在编程的世界中,操作文件是一个很重要的技能。

1. 文件、目录和路径

在开始操作之前,先大概讲解一下基本概念。在计算机系统中,文件是以硬盘为载体存储在计算机上的信息集合。文件通常会有一个后缀名,表示文件格式(当然,通常的另一个含义就是可能没有)。我们最常见到的图片文件,后缀有jpg/png/gif这些常见的;文本文件为txt等。
目录,不严谨的来讲可以用文件夹代替。不过严格来说,目录指的是文件所在的文件夹以及文件夹的位置这些信息的集合。
路径是指文件或文件夹所在的位置的字符串表示,有相对路径和绝对路径,有物理路径和网络路径等一系列这些划分。

  • 相对路径指的是,相对程序所在目录目标文件所在的目录路径
  • 绝对路径指的是从系统或者网站的目录起点开始文件所在的位置,也就是说无论程序在哪都能通过绝对路径访问到对应文件
  • 物理路径是指文件在磁盘的路径,划分依据与之前的两种并不一致,所以不是并列关系
  • 网络路径是指网络或文件是在网络服务上部署的,通过URI访问的路径信息

好了,基本概念介绍到这里,让我们来看看如何实现C#操作文件吧。

1.1 File和FileInfo

C# 提供了两个访问文件的入口,File和FileInfo这两个类。有人可能要迷惑了,为啥要提供两个呢,这两个又有啥子不一样的呢?别急,让我们来一起看一看吧。
我们先来观察一下两个类的声明方式有什么不一样的:

  1. public static class File;
  2. public sealed class FileInfo : System.IO.FileSystemInfo;

我们忽略突然冒出来的FileSystemInfo,只需要明白它是FileInfo的基类即可。
通过两个类的声明方式,可以看出File是一个工具类,而FileInfo则是文件对象。所以,File更多的用在快速操作文件并不需要长时间多次使用同一个文件的场景,而FileInfo则适合同一个文件的多次使用。

1.1.1 File工具类

我们先来看下File支持哪些操作:
a.文件读取

  1. public static byte[] ReadAllBytes (string path);
  2. public static string[] ReadAllLines (string path);
  3. public static string[] ReadAllLines (string path, System.Text.Encoding encoding);
  4. public static string ReadAllText (string path);
  5. public static string ReadAllText (string path, System.Text.Encoding encoding);
  6. public static System.Collections.Generic.IEnumerable<string> ReadLines (string path);

先从名称上分析方法应该是什么,应该具有哪些功能?

  • ReadAllBytes以二进制的形式一次性把文件全部读出来
  • ReadAllLines打开文本文件,将文件内容一行一行的全部读出来并返回
  • ReadAllText打开文件,并将文件所有内容一次性读出来
  • ReadLines 这是一个新的方法,根据返回值和方法名称,可以判断它应该与ReadAllLines有着类似的行为

    ReadLInes和ReadAllLines的区别:

    • ReadAllLines返回的是字符串数组,所以该方法会一次性将文件内容全部读出
    • ReadLines返回的是一个可枚举对象,根据之前在Linq系列和集合系列的知识,我们能判断出,这个方法不会立即返回数据

    所以我们很轻易的就能得出,ReadAllLines不会过久的持有文件对象,但是不适合操作大文件;ReadLines对于大文件的操作更擅长一些,但是可能会更久的持有文件

b.写入文件

  1. public static void AppendAllLines (string path, System.Collections.Generic.IEnumerable<string> contents);
  2. public static void AppendAllLines (string path, System.Collections.Generic.IEnumerable<string> contents, System.Text.Encoding encoding);
  3. public static void AppendAllText (string path, string contents);
  4. public static void AppendAllText (string path, string contents, System.Text.Encoding encoding);
  5. public static void WriteAllBytes (string path, byte[] bytes);
  6. public static void WriteAllLines (string path, string[] contents, System.Text.Encoding encoding);
  7. public static void WriteAllText (string path, string contents);
  8. public static void WriteAllText (string path, string contents, System.Text.Encoding encoding);

来,我们简单看一下这几个方法具体作用:

  • AppendAllLines:追加行到文件末尾
  • AppendAllText :将字符串内容追加到文件末尾
  • WriteBytes:将字节数组写到文件里,如果文件有内容就覆盖原有内容
  • WriteAllLines:按行写入文件中,如果文件有内容则覆盖原有内容
  • WriteAllText:将内容写入文件,如果文件有内容则覆盖原有内容

在使用File写入文件的时候,如果文件不存在则会自动创建文件。
c. 复制文件
File类提供了简单易用的复制文件功能,只需要指定源文件和新文件即可:

  1. public static void Copy (string sourceFileName, string destFileName);
  2. public static void Copy (string sourceFileName, string destFileName, bool overwrite);

这两个方法对的作用就是将 sourceFileName复制为destFileName。第一个方法不允许复制为已存在的文件,也就是说如果destFileName已存在则报错。第二个方法则通过overwrite指定是否覆盖。
d.移动文件
与复制文件相同的使用方式,File提供了移动文件的方法:

  1. public static void Move (string sourceFileName, string destFileName);
  2. public static void Move (string sourceFileName, string destFileName, bool overwrite);

注意事项与复制文件一致。
e.删除文件

  1. public static void Delete (string path);

1.1.2 FileInfo 对象类

FileInfo提供了文件的创建、复制、删除、移动和打开等属性和实例方法。我们先来看看,如果创建一个FileInfo:

  1. public FileInfo (string fileName);

通过指定文件路径,来换取一个FileInfo对象,如果fileName指定的是目录则会提示错误。
好,现在我们已经可以获取一个FileInfo对象实例了,那么一起来看看FileInfo支持哪些内容吧:
a. 先来看看文件的基本属性

  1. public override bool Exists { get; }

文件是否存在,等效于File.Existss(string path)。

  1. public string DirectoryName { get; }

获取文件所在目录的完整路径(绝对路径)。

  1. public System.IO.DirectoryInfo Directory { get; }

获取文件所在目录的目录类型实例。

  1. public long Length { get; }

获取文件的大小,单位是字节。

  1. public override string Name { get; }

获取文件名,包括文件的扩展名。
b. 文件的操作
对于FileInfo实例来说,对于文件的操作大多都是基于流来完成的(这部分请留意下一篇内容),这里先看一下它的实例方法:

  1. public System.IO.StreamWriter AppendText ();//创建一个流适配器,在适配器里追加文本到文件中
  2. public System.IO.FileInfo CopyTo (string destFileName);//将现有文件复制到新文件,并返回新文件的实例,不支持覆盖
  3. public System.IO.FileInfo CopyTo (string destFileName, bool overwrite);//根据orverwrite确定是否覆盖
  4. public System.IO.FileStream Create ();//创建当前对象代表的文件,并返回一个文件流
  5. public System.IO.StreamWriter CreateText ();//与AppendText类似,但会覆盖文件原有内容
  6. public override void Delete ();//删除文件
  7. public void MoveTo (string destFileName);// 将文件移动到新文件,不支持覆盖已存在文件
  8. public void MoveTo (string destFileName, bool overwrite);// 根据overwrite确定是否覆盖
  9. public System.IO.FileStream Open (System.IO.FileMode mode);// 根据模式打开文件
  10. public System.IO.FileStream Open (System.IO.FileMode mode, System.IO.FileAccess access);//指定权限和模式,打开文件
  11. public System.IO.FileStream OpenRead ();//打开一个只能读取的文件流
  12. public System.IO.StreamReader OpenText ();//打开一个读流适配器
  13. public System.IO.FileStream OpenWrite ();// 打开一个只能写的流

最新版C#的API,取消了通过FileInfo获取文件的格式名的属性以及其他的很多属性,只保留了文中提到的几个属性。

1.2 Directory和DirectoryInfo

与之前的类似,Directory也是个工具类,DirectoryInfo则代表目录实例。

1.2.1 Directory

先来个简单的:
a. 创建目录:

  1. public static System.IO.DirectoryInfo CreateDirectory (string path);

如果目录已存在,则跳过创建,直接返回指定路径的DirectoryInfo实例
b.是否存在:

  1. public static bool Exists (string path);

返回是否存在这个目录。
c.返回目录下的所有文件

  1. public static string[] GetFiles (string path);

d. 返回目录下的所有子目录:

  1. public static string[] GetDirectories (string path);
  2. public static string[] GetDirectories (string path, string searchPattern);
  3. public static string[] GetDirectories (string path, string searchPattern, System.IO.EnumerationOptions enumerationOptions);
  4. public static string[] GetDirectories (string path, string searchPattern, System.IO.SearchOption searchOption);

除了上文提到的 GetDirectories 方法可以直接返回目录下所有子目录以外,还有一组方法也可以枚举出当前目录下的子目录:

  1. public static System.Collections.Generic.IEnumerable<string> EnumerateDirectories (string path);

枚举 path 目录下的所有子目录。

  1. public static System.Collections.Generic.IEnumerable<string> EnumerateDirectories (string path, string searchPattern);

searchPattern,搜索名称字符串,可以包含有效文本路径和通配符(* 和 ?)的组合,但不支持正则表达式。

  1. public static System.Collections.Generic.IEnumerable<string> EnumerateDirectories (string path, string searchPattern, System.IO.EnumerationOptions enumerationOptions);
  2. public static System.Collections.Generic.IEnumerable<string> EnumerateDirectories (string path, string searchPattern, System.IO.SearchOption searchOption);

这两个方法放在一起讲,这两个是对上一个方法的增强和补充。其中 EnumerationOptions 是类,可以配置查询的条件;SearchOption 是个枚举,选择只查询当前目录的子目录名称还是继续深入查询子孙目录。
e.查看目录下的所有文件-补充
与子目录查询相同,Directory也支持这么几组查询方法:

  1. public static string[] GetFiles (string path);
  2. public static string[] GetFiles (string path, string searchPattern);
  3. public static string[] GetFiles (string path, string searchPattern, System.IO.EnumerationOptions enumerationOptions);
  4. public static string[] GetFiles (string path, string searchPattern, System.IO.SearchOption searchOption);

从参数上看,可以看出来这是返回子目录下的文件列表。其中使用 searchPattern查询名称,enumerationOptions 作为查询条件,searchOption 作为查询的深度。
同样,查询文件也可以使用枚举方法:

  1. public static System.Collections.Generic.IEnumerable<string> EnumerateFiles (string path);
  2. public static System.Collections.Generic.IEnumerable<string> EnumerateFiles (string path, string searchPattern);
  3. public static System.Collections.Generic.IEnumerable<string> EnumerateFiles (string path, string searchPattern, System.IO.EnumerationOptions enumerationOptions);
  4. public static System.Collections.Generic.IEnumerable<string> EnumerateFiles (string path, string searchPattern, System.IO.SearchOption searchOption);

f.获取当前目录

  1. public static string GetCurrentDirectory ();

在程序中调用这个方法可以获取程序执行时的目录,如果是在调试阶段,目录是指程序的主方法所在目录;如果在发布之后,也就是运行阶段,该目录指程序所在目录。
g.获取上级目录

  1. public static System.IO.DirectoryInfo GetParent (string path);

获取传入目录的上级目录信息。
h.目录移动

  1. public static void Move (string sourceDirName, string destDirName);

sourceDirName 移动到 destDirName,其中destDirName所代表的目录不能纯在。这个方法有个很有意思的特点,它也支持移动文件。也就是说,如果sourceDirNanme指向的是一个文件,那么destDirName也必须是一个文件类型的路径字符串。
i.删除目录

  1. public static void Delete (string path);//删除 path所代表的目录,如果目录非空则提示无法删除
  2. public static void Delete (string path, bool recursive);// recursive指示是否同时删除子目录和文件

以上是Directory类的一些常用方法,当然还有更多的内容留待小伙伴一起发掘。传送门==>https://docs.microsoft.com/zh-cn/dotnet/api/system.io.directory?view=netcore-3.1

1.2.2 DirectoryInfo

之前的篇幅我们介绍了Directory的工具类所支持的方法,接下来我们看一下 DirectoryInfo有哪些属性和方法吧。

  1. public DirectoryInfo (string path);

初始化的方式很简单,直接传递一个目录的路径字符串,就可以获取一个目录信息类了。
接下来看看,DirectoryInfo支持的属性:

  1. public override bool Exists { get; }// 目录是否存在
  2. public override string Name { get; }// 目录名称,不是路径
  3. public System.IO.DirectoryInfo Parent { get; }//如果有上级目录,则返回上级目录,如果没有则返回 null
  4. public System.IO.DirectoryInfo Root { get; }//获取目录的根目录

我们路过了DirectoryInfo的属性,看到了它一部分特点,那么我们该怎么使用呢?

  1. public void Create ();

创建目录信息所代表的目录,如果目录已存在,则不会有任何变化 。如果这个目录的父目录也不存在,则自动创建父目录

  1. public System.IO.DirectoryInfo CreateSubdirectory (string path);

创建 pathi指定的子目录。

  1. public override void Delete ();

如果当前目录是空目录,调用可直接删除,如果非空则会提示错误。

  1. public void Delete (bool recursive);

根据参数 recursive指定是否删除当前目录的子目录。

  1. public System.IO.DirectoryInfo[] GetDirectories ();
  2. public System.IO.DirectoryInfo[] GetDirectories (string searchPattern);
  3. public System.IO.DirectoryInfo[] GetDirectories (string searchPattern, System.IO.EnumerationOptions enumerationOptions);
  4. public System.IO.DirectoryInfo[] GetDirectories (string searchPattern, System.IO.SearchOption searchOption);

获取子目录的数组,参数与 Directory 的同名方法一致。

  1. public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories ();
  2. public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories (string searchPattern);
  3. public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories (string searchPattern, System.IO.EnumerationOptions enumerationOptions);
  4. public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories (string searchPattern, System.IO.SearchOption searchOption);

返回一个子目录信息的可枚举集合。

  1. public System.IO.FileInfo[] GetFiles ();
  2. public System.IO.FileInfo[] GetFiles (string searchPattern);
  3. public System.IO.FileInfo[] GetFiles (string searchPattern, System.IO.EnumerationOptions enumerationOptions);
  4. public System.IO.FileInfo[] GetFiles (string searchPattern, System.IO.SearchOption searchOption);

嗯,依旧类似的写法,获取文件信息的数组

  1. public System.Collections.Generic.IEnumerable<System.IO.FileInfo> EnumerateFiles ();
  2. public System.Collections.Generic.IEnumerable<System.IO.FileInfo> EnumerateFiles (string searchPattern);
  3. public System.Collections.Generic.IEnumerable<System.IO.FileInfo> EnumerateFiles (string searchPattern, System.IO.EnumerationOptions enumerationOptions);
  4. public System.Collections.Generic.IEnumerable<System.IO.FileInfo> EnumerateFiles (string searchPattern, System.IO.SearchOption searchOption);

返回文件的可枚举集合。

  1. public void MoveTo (string destDirName);

把当前目录移动到对应的目录。

1.3. Path

Path的中文名称有路径的意思,所以Path类就是路径类,C#把Path设置为工具类,路径的实例被区分为文件和目录了。以下是它的定义:

  1. public static class Path

路径是描述文件和目录的位置的字符串,路径并不一定指向硬盘上,换句话说就是路径不一定是物理路径也有可能是虚拟路径或者网络路径。在不同的操作系统和平台上,路径有着不同的表现,所以Path类是对不同平台行为的统一抽象。具体的路径表示需要参照具体的系统表示形式。
那么我们先来看看Path为我们提供了哪些内容,让我们一睹为快:

1.3.1 字段

  1. public static readonly char AltDirectorySeparatorChar;
  2. public static readonly char DirectorySeparatorChar;

这两个是特定系统下的目录分隔符,其中AltDirectorySeparatorChar表示正斜线(/),DirectorySeparatorChar 表示反斜线(\)。为什么说是特定系统下的目录分隔符呢,因为Windows环境对两种分隔符都支持,但是Unix和类Unix系统只支持 / 作为目录分隔符。所以如果系统需要跨平台支持,则最好使用 AltDirectorySeparatorChar作为目录分隔符来使用。

  1. public static readonly char PathSeparator;

这个字段返回在环境变量中分隔路径字符串的平台特定的分隔符。Windows中返回一个分号(;),其他平台可能会有不一样的表现。

  1. public static readonly char VolumeSeparatorChar;

这个表示卷分隔符,是个很有意思的特定。对于Linux系统来说并没有类似于Windows一样的卷,所以该字段会返回一个/ ,而Windows中例如:
D:\Temp\ 这个目录则会返回冒号(:)。

1.3.2 方法

介绍完了字段,我们来看看Path给我们提供了哪些方法吧。
先从最常用的说起吧:

  1. public static string Combine (params string[] paths);
  2. public static string Combine (string path1, string path2);
  3. public static string Combine (string path1, string path2, string path3);
  4. public static string Combine (string path1, string path2, string path3, string path4);

这一组方法用来拼接路径,除第一个参数外,每个参数都应当是相对于之前参数拼接结果路径的相对路径。如果后续出现了绝对路径,那之前计算出的路径信息则会全部抛弃,重新计算。
以下是一个示例:

  1. string[] paths = {@"d:\archives", "2001", "media", "images"};
  2. string fullPath = Path.Combine(paths);
  3. Console.WriteLine(fullPath);
  4. paths = new string[] {@"d:\archives\", @"2001\", "media", "images"};
  5. fullPath = Path.Combine(paths);
  6. Console.WriteLine(fullPath);
  7. paths = new string[] {"d:/archives/", "2001/", "media", "images"};
  8. fullPath = Path.Combine(paths);
  9. Console.WriteLine(fullPath);
  10. // Windows系统下的执行结果
  11. // d:\archives\2001\media\images
  12. // d:\archives\2001\media\images
  13. // d:/archives/2001/media\images
  14. //
  15. // 类Unix系统的执行结果
  16. // d:\archives/2001/media/images
  17. // d:\archives\/2001\/media/images
  18. // d:/archives/2001/media/images

继续下一个方法:

  1. public static string GetFullPath (string path, string basePath);
  2. public static string GetFullPath (string path);

获取相对路径的绝对路径,其中 path 是相对路径,basePath是绝对路径。如果指定basePath,则从basePath根据path计算全路径。

  1. public static string GetRelativePath (string relativeTo, string path);

返回从一个路径到另一个路径的相对路径,其中relativeTo是源路径,path为目标路径。其中relativeTo始终是目录,或者被认为是目录。

  1. public static string GetDirectoryName (string path);

返回路径path里的目录信息,例如:”C:\Directory\SubDirectory\test.txt” ,返回”C:\Directory\SubDirectory”,如果path是目录,则返回其上级目录的路径字符串。

  1. public static string Join (string path1, string path2, string path3, string path4);
  2. public static string Join (string path1, string path2, string path3);
  3. public static string Join (params string[] paths);

与Combine方法差不多,不过Join方法是把所以参数均按照相对目录来拼接。
说完了目录的一些操作,我们看看Path对文件路径提供了哪些支持:

  1. public static string GetFileName (string path);

获取路径里的文件名,例如说:“C:\mydir\myfile.ext”,返回结果就是“myfile.ext”,也就是说这个方法会返回携带后缀名的文件名。因为文件名本身就包含后缀名。

  1. public static string GetFileNameWithoutExtension (string path);

返回不带后缀名的文件名,与GetFileName类似,但是不好含文件格式后缀。

  1. public static bool HasExtension (string path);

确定是否包含后缀名,也称格式名或者扩展名。

  1. public static string GetExtension (string path);

返回所代表的文件的后缀名。

  1. public static string ChangeExtension (string path, string extension);

修改文件的后缀名。
这些是Path的常用方法,大家有个印象就好。

1.3 FileSystemInfo

文件系统信息,这是FileInfo和DirectoryInfo的两个类的基类,它定义了文件系统中文件和目录共有的一些属性和方法。接下来让我们简单看一看。
先来看一下类的声明:

  1. public abstract class FileSystemInfo : MarshalByRefObject, System.Runtime.Serialization.ISerializable

一个abstract类,这个标记意味着这个类是一个抽象类,抽象类不能直接实例化,所以我们可能不会自己去直接实例化一个FileSystemInfo了。
所以我们先略过FileSystemInfo的构造函数,直接看属性和方法。

  1. public System.IO.FileAttributes Attributes { get; set; }

获取或者设置当前文件或目录的特性,这个特性是一个枚举,而且是一个位标记的枚举类型。

名称 含义
Archive 32 此文件标记为包含在增量备份操作中。 每当修改文件时,Windows 会设置该属性,并且在增量备份期间处理文件时,备份软件应进行清理该属性。
Compressed 2048 此文件是压缩文件。
Device 64 留待将来使用。
Directory 16 此文件是一个目录。 Directory 在 Windows、Linux 和 macOS 上受支持。
Encrypted 16384 此文件或目录已加密。 对于文件来说,表示文件中的所有数据都是加密的。 对于目录来说,表示新创建的文件和目录在默认情况下是加密的。
Hidden 2 文件是隐藏的,因此没有包括在普通的目录列表中。 Hidden 在 Windows、Linux 和 macOS 上受支持。
IntegrityStream 32768 文件或目录包括完整性支持数据。 在此值适用于文件时,文件中的所有数据流具有完整性支持。 此值将应用于一个目录时,所有新文件和子目录在该目录中和默认情况下应包括完整性支持。
Normal 128 该文件是没有特殊属性的标准文件。 仅当其单独使用时,此特性才有效。 Normal 在 Windows、Linux 和 macOS 上受支持。
NoScrubData 131072 文件或目录从完整性扫描数据中排除。 此值将应用于一个目录时,所有新文件和子目录在该目录中和默认情况下应不包括数据完整性。
NotContentIndexed 8192 将不会通过操作系统的内容索引服务来索引此文件。
Offline 4096 此文件处于脱机状态, 文件数据不能立即供使用。
ReadOnly 1 文件为只读文件。 ReadOnly 在 Windows、Linux 和 macOS 上受支持。 在 Linux 和 macOS 上,更改 ReadOnly 标记是权限操作。
ReparsePoint 1024 文件包含一个重新分析点,它是一个与文件或目录关联的用户定义的数据块。 ReparsePoint 在 Windows、Linux 和 macOS 上受支持。
SparseFile 512 此文件是稀疏文件。 稀疏文件一般是数据通常为零的大文件。
System 4 此文件是系统文件。 即,该文件是操作系统的一部分或者由操作系统以独占方式使用。
Temporary 256 文件是临时文件。 临时文件包含当执行应用程序时需要的,但当应用程序完成后不需要的数据。 文件系统尝试将所有数据保存在内存中,而不是将数据刷新回大容量存储,以便可以快速访问。 当临时文件不再需要时,应用程序应立即删除它。

通过以下方式进行判断:

  1. FileSystemInfo fsi;
  2. bool isXXX = (fsi.Attributes & FileAttributes.XXX) == FileAttributes.XXX;
  1. public DateTime CreationTime { get; set; }
  2. public DateTime CreationTimeUtc { get; set; }

返回文件/目录的创建时间,其中UTC指协调世界时 。

  1. public string Extension { get; }

获取文件的文件后缀名(扩展名),带点号(.)。

  1. public virtual string FullName { get; }
  2. public abstract string Name { get; }

都是返回文件或目录的名称,不过FullName返回的是全路径名称,Name只返回了文件名。

  1. public DateTime LastAccessTime { get; set; }
  2. public DateTime LastAccessTimeUtc { get; set; }

获取或设置文件最后一次访问的时间,该属性的返回值并不是严格意义上的最后一次访问时间,因为部分系统不会及时更新。

  1. public DateTime LastWriteTime { get; set; }
  2. public DateTime LastWriteTimeUtc { get; set; }

最后一次修改时间,可以自己设置或修改,类似与LastAccessTime,可能不是正确的值。

2. 总结

到目前为止,常用的文件API已经介绍完毕。接下来将为大家演示各种流的使用,以及各种流的操作场景。


  • 本文作者:GeekPower - Felix Sun
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!