15. 流和I/O_03_文件和目录
Working with ZIP Files
System.IO.Compression 中的 ZipArchive 和 ZipFile 类支持 ZIP 压缩格式。 ZIP 格式相对于 DeflateStream 和 GZipStream 的优势在于它充当多个文件的容器,并且与使用 Windows 资源管理器创建的 ZIP 文件兼容。
ZipArchive 处理流,而 ZipFile 处理更常见的文件处理场景。 (ZipFile 是 ZipArchive 的静态帮助类。)
ZipFile 的 CreateFromDirectory 方法将指定目录中的所有文件添加到一个 ZIP 文件中:
ZipFile.CreateFromDirectory (@"d:\\MyFolder", @"d:\\archive.zip");
ExtractToDirectory 则相反,将一个 ZIP 文件提取到一个目录中:
ZipFile.ExtractToDirectory (@"d:\\archive.zip", @"d:\\MyFolder");
压缩时,可以指定是否优化文件大小或速度以及是否在存档中包含源目录的名称。在我们的示例中启用后一个选项将在名为 MyFolder 的存档中创建一个子目录,压缩文件将进入该子目录。
ZipFile 有一个用于读取/写入单个条目的 Open 方法。这将返回一个 ZipArchive 对象(您也可以通过使用 Stream 对象实例化 ZipArchive 来获得该对象)。调用 Open 时,您必须指定文件名并指明是要读取、创建还是更新存档。然后,您可以通过 Entries 属性枚举现有条目或通过调用 GetEntry 查找特定文件:
using (ZipArchive zip = ZipFile.Open (@"d:\\zz.zip", ZipArchiveMode.Read))
foreach (ZipArchiveEntry entry in zip.Entries)
Console.WriteLine (entry.FullName + " " + entry.Length);
ZipArchiveEntry 也有一个 Delete 方法、一个 ExtractToFile 方法(这实际上是 ZipFileExtensions 类中的一个扩展方法)和一个返回可读文件的 Open 方法/可写流。您可以通过在 ZipArchive 上调用 CreateEntry(或 CreateEntryFromFile 扩展方法)来创建新条目。以下代码创建了存档 d:\zz.zip,并在其中添加了 foo.dll,位于名为 bin\X86 的存档目录结构下:
byte[] data = File.ReadAllBytes (@"d:\\foo.dll");
using (ZipArchive zip = ZipFile.Open (@"d:\\zz.zip", ZipArchiveMode.Update))
zip.CreateEntry (@"bin\\X64\\foo.dll").Open().Write (data, 0, data.Length);
您可以通过使用 MemoryStream 构造 ZipArchive 来完全在内存中执行相同的操作。
File and Directory Operations
System.IO 命名空间提供了一组用于执行“实用”文件和目录操作的类型,例如复制和移动、创建目录以及设置文件属性和权限。对于大多数功能,您可以在两个类中选择一个,一个提供静态方法,另一个提供实例方法:
静态类
File 和 Directory
实例方法类(用文件或目录名构造)
FileInfo 和 DirectoryInfo
此外,还有一个静态类称为路径
Path
。这对文件或目录没有任何作用;相反,它为文件名和目录路径提供了字符串操作方法。 Path 还有助于处理临时文件。
The File Class
File 是一个静态类,其方法都接受一个文件名。文件名可以是相对于当前目录的,也可以是完全限定的目录。下面是它的方法(都是公共的和静态的):
bool Exists (string path); // Returns true if the file is present
void Delete (string path);
void Copy (string sourceFileName, string destFileName);
void Move (string sourceFileName, string destFileName);
void Replace (string sourceFileName, string destinationFileName, string destinationBackupFileName);
FileAttributes GetAttributes (string path);
void SetAttributes (string path, FileAttributes fileAttributes);
void Decrypt (string path);
void Encrypt (string path);
DateTime GetCreationTime (string path); // UTC versions are
DateTime GetLastAccessTime (string path); // also provided.
DateTime GetLastWriteTime (string path);
void SetCreationTime (string path, DateTime creationTime);
void SetLastAccessTime (string path, DateTime lastAccessTime);
void SetLastWriteTime (string path, DateTime lastWriteTime);
FileSecurity GetAccessControl (string path);
FileSecurity GetAccessControl (string path, AccessControlSections includeSections);
void SetAccessControl (string path, FileSecurity fileSecurity);
如果目标文件已经存在,则 Move 抛出异常;替换没有。这两种方法都允许文件被重命名以及移动到另一个目录。
如果文件被标记为只读,删除将抛出 UnauthorizedAccessException;您可以通过调用 GetAttributes 提前告知。如果操作系统拒绝您的进程对该文件的删除权限,它也会抛出该异常。以下是 GetAttributes 返回的 FileAttribute 枚举的所有成员:
Archive, Compressed, Device, Directory, Encrypted,
Hidden, IntegritySystem, Normal, NoScrubData, NotContentIndexed,
Offline, ReadOnly, ReparsePoint, SparseFile, System, Temporary
此枚举中的成员是可组合的。以下是如何在不影响其余文件属性的情况下切换单个文件属性:
string filePath = "test.txt";
FileAttributes fa = File.GetAttributes (filePath);
if ((fa & FileAttributes.ReadOnly) != 0)
// Use the exclusive-or operator (^) to toggle the ReadOnly flag
fa ^= FileAttributes.ReadOnly;
File.SetAttributes (filePath, fa);
// Now we can delete the file, for instance:
File.Delete (filePath);
Compression and encryption attributes
压缩和加密文件属性对应于 Windows 资源管理器中文件或目录属性对话框中的压缩和加密复选框。这种类型的压缩和加密是透明的,因为操作系统在幕后完成所有工作,允许您读取和写入纯数据。
您不能使用 SetAttributes 来更改文件的压缩或加密属性——如果您尝试,它会默默地失败!后一种情况的解决方法很简单:您改为调用 File 类中的 Encrypt() 和 Decrypt() 方法。有了压缩,就更复杂了;一种解决方案是在 System.Management 中使用 Windows Management Instrumentation (WMI) API。以下方法压缩目录,如果成功则返回 0(如果失败则返回 WMI 错误代码):
static uint CompressFolder (string folder, bool recursive)
string path = "Win32_Directory.Name='" + folder + "'";
using (ManagementObject dir = new ManagementObject (path))
using (ManagementBaseObject p = dir.GetMethodParameters ("CompressEx"))
p ["Recursive"] = recursive;
using (ManagementBaseObject result = dir.InvokeMethod ("CompressEx", p, null))
return (uint) result.Properties ["ReturnValue"].Value;
要解压缩,请将 CompressEx 替换为 UncompressEx。
透明加密依赖于从登录用户的密码中植入的密钥。该系统对经过身份验证的用户执行的密码更改具有鲁棒性,但如果通过管理员重置密码,则加密文件中的数据将无法恢复。
您可以通过 Win32 互操作确定卷是否支持压缩和加密:
using System;
using System.IO;
using System.Text;
using System.ComponentModel;
using System.Runtime.InteropServices;
class SupportsCompressionEncryption
const int SupportsCompression = 0x10;
const int SupportsEncryption = 0x20000;
[DllImport ("Kernel32.dll", SetLastError = true)]
extern static bool GetVolumeInformation (string vol, StringBuilder name,
int nameSize, out uint serialNum, out uint maxNameLen, out uint flags,
StringBuilder fileSysName, int fileSysNameSize);
static void Main()
uint serialNum, maxNameLen, flags;
bool ok = GetVolumeInformation (@"C:\\", null, 0, out serialNum,
out maxNameLen, out flags, null, 0);
if (!ok)
throw new Win32Exception();
bool canCompress = (flags & SupportsCompression) != 0;
bool canEncrypt = (flags & SupportsEncryption) != 0;
File security
FileSecurity 类允许您查询和更改分配给用户和角色的操作系统权限(命名空间System.Security.AccessControl)。
在这个例子中,我们列出了一个文件的现有权限,然后将写入权限分配给“用户”组:
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
void ShowSecurity (FileSecurity sec)
AuthorizationRuleCollection rules = sec.GetAccessRules (true, true, typeof (NTAccount));
foreach (FileSystemAccessRule r in rules.Cast<FileSystemAccessRule>()
.OrderBy (rule => rule.IdentityReference.Value))
// e.g., MyDomain/Joe
Console.WriteLine ($" {r.IdentityReference.Value}");
// Allow or Deny: e.g., FullControl
Console.WriteLine ($" {r.FileSystemRights}: {r.AccessControlType}");
var file = "sectest.txt";
File.WriteAllText (file, "File security test.");
var sid = new SecurityIdentifier (WellKnownSidType.BuiltinUsersSid, null);
string usersAccount = sid.Translate (typeof (NTAccount)).ToString();
Console.WriteLine ($"User: {usersAccount}");
FileSecurity sec = new FileSecurity (file, AccessControlSections.Owner
| AccessControlSections.Group
| AccessControlSections.Access);
Console.WriteLine ("AFTER CREATE:");
ShowSecurity(sec); // BUILTIN\\Users doesn't have Write permission
sec.ModifyAccessRule (AccessControlModification.Add,
new FileSystemAccessRule (usersAccount, FileSystemRights.Write, AccessControlType.Allow),
out bool modified);
Console.WriteLine ("AFTER MODIFY:");
ShowSecurity (sec); // BUILTIN\\Users has Write permission
我们稍后在第 707 页的“特殊文件夹”中给出另一个示例。
The Directory Class
静态目录类提供了一组类似于 File 类中的那些方法——用于检查目录是否存在 (Exists)、移动目录 (Move)、删除目录 (Delete)、获取/设置创建时间或上次访问时间以及获取/设置安全权限。此外,Directory 公开了以下静态方法:
string GetCurrentDirectory ();
void SetCurrentDirectory (string path);
DirectoryInfo CreateDirectory (string path);
DirectoryInfo GetParent (string path);
string GetDirectoryRoot (string path);
string[] GetLogicalDrives(); // Gets mount points on Unix
// The following methods all return full paths:
string[] GetFiles (string path);
string[] GetDirectories (string path);
string[] GetFileSystemEntries (string path);
IEnumerable<string> EnumerateFiles (string path);
IEnumerable<string> EnumerateDirectories (string path);
IEnumerable<string> EnumerateFileSystemEntries (string path);
Enumerate* 和 Get* 方法被重载以接受 searchPattern (string) 和 searchOption (enum) 参数。如果指定 SearchOption.SearchAllSubDirectories,则会执行递归子目录搜索。 *FileSystemEntries 方法结合了 *Files 和 *Directories 的结果。
如果目录不存在,则创建目录的方法如下:
if (!Directory.Exists (@"d:\\test"))
Directory.CreateDirectory (@"d:\\test");
FileInfo and DirectoryInfo
File 和 Directory 上的静态方法便于执行单个文件或目录操作。如果您需要连续调用一系列方法,FileInfo 和 DirectoryInfo 类提供了一个对象模型,使这项工作更容易。
FileInfo 以实例形式提供大部分 File 的静态方法——带有一些附加属性,例如 Extension、Length、IsReadOnly 和 Directory——用于返回 DirectoryInfo 对象。例如:
static string TestDirectory =>
RuntimeInformation.IsOSPlatform (OSPlatform.Windows)
? @"C:\\Temp"
: "/tmp";
Directory.CreateDirectory (TestDirectory);
FileInfo fi = new FileInfo (Path.Combine (TestDirectory, "FileInfo.txt"));
Console.WriteLine (fi.Exists); // false
using (TextWriter w = fi.CreateText())
w.Write ("Some text");
Console.WriteLine (fi.Exists); // false (still)
fi.Refresh();
Console.WriteLine (fi.Exists); // true
Console.WriteLine (fi.Name); // FileInfo.txt
Console.WriteLine (fi.FullName); // c:\\temp\\FileInfo.txt (Windows)
// /tmp/FileInfo.txt (Unix)
Console.WriteLine (fi.DirectoryName); // c:\\temp (Windows)
// /tmp (Unix)
Console.WriteLine (fi.Directory.Name); // temp
Console.WriteLine (fi.Extension); // .txt
Console.WriteLine (fi.Length); // 9
fi.Encrypt();
fi.Attributes ^= FileAttributes.Hidden; // (Toggle hidden flag)
fi.IsReadOnly = true;
Console.WriteLine (fi.Attributes); // ReadOnly,Archive,Hidden,Encrypted
Console.WriteLine (fi.CreationTime); // 3/09/2019 1:24:05 PM
fi.MoveTo (Path.Combine (TestDirectory, "FileInfoX.txt"));
DirectoryInfo di = fi.Directory;
Console.WriteLine (di.Name); // temp or tmp
Console.WriteLine (di.FullName); // c:\\temp or /tmp
Console.WriteLine (di.Parent.FullName); // c:\\ or /
di.CreateSubdirectory ("SubFolder");
下面是如何使用 DirectoryInfo 枚举文件和子目录:
DirectoryInfo di = new DirectoryInfo (@"e:\\photos");
foreach (FileInfo fi in di.GetFiles ("*.jpg"))
Console.WriteLine (fi.Name);
foreach (DirectoryInfo subDir in di.GetDirectories())
Console.WriteLine (subDir.FullName);
Path
静态 Path 类定义了处理路径和文件名的方法和字段。
假设此设置代码:
string dir = @"c:\\mydir"; // or /mydir
string file = "myfile.txt";
string path = @"c:\\mydir\\myfile.txt"; // or /mydir/myfile.txt
Directory.SetCurrentDirectory (@"k:\\demo"); // or /demo
我们可以使用以下表达式演示 Path 的方法和字段:
Combine 特别有用:它允许您组合目录和文件名——或两个目录——而无需首先检查是否存在尾随路径分隔符,并且它会自动为操作系统使用正确的路径分隔符。它提供最多接受四个目录和/或文件名的重载。
GetFullPath 将相对于当前目录的路径转换为绝对路径。它接受诸如 ..\..\file.txt 之类的值。 GetRandomFileName 返回一个真正唯一的 8.3 个字符的文件名,而不实际创建任何文件。 GetTempFileName 使用自动递增计数器生成一个临时文件名,该计数器每 65,000 个文件重复一次。然后它在本地临时目录中创建一个同名的零字节文件。
Special Folders
Path 和 Directory 中缺少的一件事是找到诸如我的文档、程序文件、应用程序数据等文件夹的方法。这是由 System.Environment 类中的 GetFolderPath 方法提供的:
string myDocPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
Environment.SpecialFolder 是一个枚举,其值包含 Windows 中的所有特殊目录,例如 AdminTools、ApplicationData、Fonts、History、SendTo、StartMenu 等。除了 .NET 运行时目录外,这里涵盖了所有内容,您可以通过以下方式获得该目录:
System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()
在 Windows 系统上特别有价值的是 ApplicationData,您可以在其中存储与用户一起通过网络传输的设置(如果在网络域上启用了漫游配置文件); LocalApplicationData,用于非漫游数据(特定于登录用户); CommonApplicationData,由计算机的每个用户共享。将应用程序数据写入这些文件夹被认为比使用 Windows 注册表更可取。在这些文件夹中存储数据的标准协议是使用您的应用程序名称创建一个子目录:
string localAppDataPath = Path.Combine (
Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData),
"MyCoolApplication");
if (!Directory.Exists (localAppDataPath))
Directory.CreateDirectory (localAppDataPath);
使用 CommonApplicationData 时有一个可怕的陷阱:如果用户以管理权限启动您的程序,然后您的程序在 CommonApplicationData 中创建文件夹和文件,那当在受限的 Windows 登录下运行时,用户可能缺少稍后替换这些文件的权限。 (在权限受限的帐户之间切换时也存在类似的问题。)您可以通过在设置过程中创建所需的文件夹(将权限分配给每个人)来解决这个问题。另一个写入配置和日志文件的地方是应用程序的基本目录,您可以使用 AppDomain.CurrentDomain.BaseDirectory 获取该目录。但是,不建议这样做,因为操作系统可能会拒绝您的应用程序在初始安装(没有管理提升)后写入此文件夹的权限。
Querying Volume Information
您可以使用 DriveInfo 类查询计算机上的驱动器:
DriveInfo c = new DriveInfo ("C"); // Query the C: drive.
// On Unix: /
long totalSize = c.TotalSize; // Size in bytes.
long freeBytes = c.TotalFreeSpace; // Ignores disk quotas.
long freeToMe = c.AvailableFreeSpace; // Takes quotas into account.
foreach (DriveInfo d in DriveInfo.GetDrives()) // All defined drives.
// On Unix: mount points
Console.WriteLine (d.Name); // C:\\
Console.WriteLine (d.DriveType); // Fixed
Console.WriteLine (d.RootDirectory); // C:\\
if (d.IsReady) // If the drive is not ready, the following two
// properties will throw exceptions:
Console.WriteLine (d.VolumeLabel); // The Sea Drive
Console.WriteLine (d.DriveFormat); // NTFS
静态 GetDrives 方法返回所有映射的驱动器,包括 CD-ROM、媒体卡和网络连接。 DriveType 是具有以下值的枚举:
Unknown, NoRootDirectory, Removable, Fixed, Network, CDRom, Ram
Catching Filesystem Events
FileSystemWatcher 类允许您监视目录(以及可选的子目录)的活动。 FileSystemWatcher 具有在创建、修改、重命名和删除文件或子目录时以及它们的属性更改时触发的事件。无论执行更改的用户或进程如何,这些事件都会触发。下面是一个示例:
Watch (GetTestDirectory(), "*.txt", true);
void Watch (string path, string filter, bool includeSubDirs)
using (var watcher = new FileSystemWatcher (path, filter))
watcher.Created += FileCreatedChangedDeleted;
watcher.Changed += FileCreatedChangedDeleted;
watcher.Deleted += FileCreatedChangedDeleted;
watcher.Renamed += FileRenamed;
watcher.Error += FileError;
watcher.IncludeSubdirectories = includeSubDirs;
watcher.EnableRaisingEvents = true;
Console.WriteLine ("Listening for events - press <enter> to end");
Console.ReadLine();
// Disposing the FileSystemWatcher stops further events from firing.
void FileCreatedChangedDeleted (object o, FileSystemEventArgs e)
=> Console.WriteLine ("File {0} has been {1}", e.FullPath, e.ChangeType);
void FileRenamed (object o, RenamedEventArgs e)
=> Console.WriteLine ("Renamed: {0}->{1}", e.OldFullPath, e.FullPath);
void FileError (object o, ErrorEventArgs e)
=> Console.WriteLine ("Error: " + e.GetException().Message);